Skip to content

Instantly share code, notes, and snippets.

@sk89q
Created April 28, 2013 19:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sk89q/5478026 to your computer and use it in GitHub Desktop.
Save sk89q/5478026 to your computer and use it in GitHub Desktop.
--[[
Node Panel Display
by sk89q
]]--
dofile("npanel_conf.lua")
dofile("serpent.lua")
dofile("util.lua")
local function validateColor(allowNone)
return function(input)
if allowNone and input == "" then return nil end
if not colors[input] then
return "Invalid color! Examples: red, blue, black, lightBlue, etc."
end
end
end
local Block = prototype({
__construct = function(self, x, y, text, color)
self.x = x
self.y = y
self.text = text
self.color = color or colors.lightBlue
self.conn = {}
self.cable = nil
self.state = nil
self:setText(text)
end,
setText = function(self, text)
self.text = text
-- calculate width of the block
self._width = #self.text + 1
if self._width % 2 ~= 0 then
self._width = self._width + 1
end
end,
onClick = function(self)
if self.cable then -- bundled cable
-- toggle the latch (external to the system)
self.state = not self.state
redstone.setBundledOutput(bundledSide, self.cable)
os.sleep(0.2)
redstone.setBundledOutput(bundledSide, 0)
return "Toggled " .. self.text .. " to " .. (self.state and "ON" or "OFF")
else
return false
end
end,
contains = function(self, x, y)
return x >= -self._width / 2 + self.x and x <= self._width / 2 + self.x and
y >= self.y - 1 and y <= self.y + 1
end,
connect = function(self, to, color)
local color = color or colors.white
table.insert(self.conn, {to = to, color = color})
table.insert(to.conn, {to = self, color = color})
end,
disconnect = function(self, to)
for k, other in ipairs(self.conn) do
if other.to == to then
-- remove own connection
table.remove(self.conn, k)
-- remove the other's reference too
for k, other in ipairs(to.conn) do
if other.to == self then
table.remove(to.conn, k)
break
end
end
return -- exit
end
end
end,
disconnectAll = function(self)
for _, other in ipairs(self.conn) do
-- remove the other's reference
for k, o in ipairs(other.to.conn) do
if o.to == self then
table.remove(other.to.conn, k)
break
end
end
end
self.conn = {}
end,
isConnected = function(self, to)
for k, other in ipairs(self.conn) do
if other.to == to then return true end
end
return false
end,
setCable = function(self, color)
self.cable = color
-- set the state field if only we have a color set
if color then
self.state = self.state or false
else
self.state = nil
end
return self
end,
apply = function(self, object)
prototyptify(self, object)
end,
draw = function(self)
term.setBackgroundColor(self._selected and colors.blue or self.color)
term.setTextColor(colors.white)
drawLine(-self._width / 2 + self.x + 1, self.y - 1, self._width / 2 + self.x - 1, self.y - 1, "-")
drawLine(-self._width / 2 + self.x + 1, self.y + 1, self._width / 2 + self.x - 1, self.y + 1, "-")
drawPixel(-self._width / 2 + self.x, self.y - 1, "+")
drawPixel(self._width / 2 + self.x, self.y - 1, "+")
drawPixel(-self._width / 2 + self.x, self.y + 1, "+")
drawPixel(self._width / 2 + self.x, self.y + 1, "+")
drawPixel(-self._width / 2 + self.x, self.y, "|")
drawPixel(self._width / 2 + self.x, self.y, "|")
term.setCursorPos(-self._width / 2 + self.x + 1, self.y)
term.write(self.text)
for i = 0, self._width - #self.text - 2 do
term.write(" ")
end
-- draw state
if self.state ~= nil then
term.setCursorPos(-self._width / 2 + self.x + 1, self.y - 1)
if self.state then
term.setBackgroundColor(colors.green)
term.setTextColor(colors.yellow)
term.write("ON")
else
term.setBackgroundColor(colors.red)
term.setTextColor(colors.yellow)
term.write("OFF")
end
end
end
})
local Diagram = prototype({
__construct = function(self)
self.blocks = {}
self.seen = {}
end,
add = function(self, block)
-- only add if we haven't seen this block
if not self.seen[block] then
self.seen[block] = true
table.insert(self.blocks, block)
for _, c in ipairs(block.conn) do
self:add(c.to) -- add this block too
end
end
end,
remove = function(self, block)
self.seen[block] = nil
-- remove from self.blocks
for k, v in ipairs(self.blocks) do
if v == block then
table.remove(self.blocks, k)
return
end
end
end,
save = function(self, path)
local data = serpent.dump(self.blocks, {
valtypeignore = {["function"] = true},
compact = true
})
-- save to file
local f = fs.open(path, "w")
f.write(data)
f.close()
end,
load = function(self, path)
-- save to file
local f = fs.open(path, "r")
if f then
local data = f.readAll()
f.close()
self.blocks = loadstring(data)()
-- need to re-initialize
for _, block in ipairs(self.blocks) do
Block:apply(block)
end
else -- no data to load then
self.blocks = {}
end
end,
intersect = function(self, x, y, seen, block)
for _, block in ipairs(self.blocks) do
if block:contains(x, y) then
return block
end
end
end
})
local BasePane = prototype({
titleColor = colors.cyan,
__construct = function(self, app)
self.app = app
end,
readInput = function(self, name, validate)
local input
self:drawHint("Please type the input above")
while true do
self:drawStatus(name)
redstone.setOutput(enableSide, true)
input = trim(read())
redstone.setOutput(enableSide, false)
if type(validate) == 'function' then
local msg = validate(input)
if msg then -- uh oh, error!
self:drawHint(msg)
else
break -- ok
end
elseif validate then -- just verify non-blank
if msg == "" then
self:drawHint("Non-empty string required")
else
break -- ok
end
else
break -- no validation needed
end
end
-- ok!
self:drawHint("")
return input
end,
drawStatus = function(self, status)
local w, h = term.getSize()
drawLine(1, h - 1, w, h - 1, " ", colors.black)
drawText(2, h - 1, status, colors.black, colors.yellow)
end,
drawHint = function(self, status)
local w, h = term.getSize()
drawLine(1, h, w, h, " ", self.titleColor)
drawText(2, h, status, self.titleColor, colors.white)
end,
drawTitle = function(self, title)
drawText(2, 1, " " .. title .. " ", self.titleColor, colors.white)
end,
drawDiagram = function(self)
self:drawConnections()
self:drawBlocks()
end,
drawBlocks = function(self)
for _, block in ipairs(self.app.diagram.blocks) do
block:draw()
end
end,
drawConnection = function(self, x1, y1, x2, y2, color)
drawLine(x1, y1, x2, y2, x1 == x2 and "|" or "-", color, colors.black)
end,
drawConnections = function(self)
for _, block in ipairs(self.app.diagram.blocks) do
for k, v in ipairs(block.conn) do
self:drawConnection(block.x, block.y, v.to.x, v.to.y, v.color)
end
end
end,
draw = function(self)
-- do nothing
end,
onPress = function(self, key)
-- do nothing
end,
onClick = function(self, button, x, y)
-- do nothing
end,
onDrag = function(self, button, x, y)
-- do nothing
end,
})
local Display = prototype(BasePane, {
draw = function(self)
self.app:drawAll(function()
local w, h = term.getSize()
term.setBackgroundColor(colors.black)
term.clear()
self:drawTitle("SYSTEM OVERVIEW")
self:drawStatus("Touch elements to toggle them off and on")
self:drawHint("[E]dit Mode")
self:drawDiagram()
end)
end,
onPress = function(self, key)
if key:lower() == "e" then
self.app:show(self.app.editor)
end
end,
onClick = function(self, button, x, y)
local clicked = self.app.diagram:intersect(x, y)
if clicked then
local status = clicked:onClick()
if status then
self.app:drawAll(function()
clicked:draw()
self:drawStatus(status)
self.app:save() -- better save state
end)
self.app.notebox.playNote(3, 24)
end
end
end,
})
local Editor = prototype(BasePane, {
titleColor = colors.red,
selected = nil,
lastSelected = nil,
draw = function(self)
self.app:drawAll(function()
local w, h = term.getSize()
term.setBackgroundColor(colors.black)
term.clear()
self:drawTitle("BLOCK EDITOR")
self:drawStatusBar()
self:drawDiagram()
end)
end,
drawStatusBar = function(self)
if self.selected then
self:drawHint("[I]nsert [D]elete [M]odify [C]onnect [S]ave")
else
self:drawHint("[I]nsert [S]ave")
end
end,
select = function(self, block)
-- swap selection
local last = nil
if self.selected then
self.selected._selected = false
last = self.selected
end
self.selected = block
if block then
block._selected = true
end
self.lastSelected = last
return last
end,
onClick = function(self, button, x, y)
local clicked = self.app.diagram:intersect(x, y)
if clicked then
local last = self:select(clicked)
-- redraw
self.app:drawAll(function()
clicked:draw()
if last then
last:draw() -- unselected the last one, so redraw
end
self:drawStatus(clicked.text .. " selected" ..
(clicked.cable and (" [color: " .. getColor(clicked.cable) .. "]") or ""))
self:drawStatusBar()
end)
end
end,
onDrag = function(self, button, x, y)
if self.selected then
if x ~= self.selected.x or y ~= self.selected.y then
self.selected.x = x
self.selected.y = y
self:draw()
end
end
end,
onPress = function(self, key)
local lkey = key:lower()
-- save/quit
if lkey == "s" then
self:select(nil)
self.app:show(self.app.display)
self.app:save()
-- insert
elseif lkey == "i" then
local x, y = 2, 2
local name = self:readInput("Name: ", true) -- just non-blank
local block = Block(x, y, name)
self.app.diagram:add(block)
self:select(block)
self:draw() -- redraw all
-- connect
elseif lkey == "c" and self.selected then -- only if selected
if self.lastSelected == self.selected then
self:drawHint("Can't connect to itself, silly")
elseif self.lastSelected then
if self.selected:isConnected(self.lastSelected) then -- disconnect
self.selected:disconnect(self.lastSelected, color)
else -- connect
local color = colors[self:readInput("Color: ", validateColor())]
self.selected:connect(self.lastSelected, color)
end
self:draw() -- redraw all
else
self:drawHint("Select two blocks in sequence first")
end
-- delete
elseif lkey == "d" and self.selected then -- only if selected
local confirm = self:readInput("Are you sure? [y/n] ")
if confirm:lower() == "y" then
self.selected:disconnectAll()
self.app.diagram:remove(self.selected)
self:select(nil)
end
self:draw()
-- modify
elseif lkey == "m" and self.selected then -- only if selected
-- set name
local name = self:readInput("Name [blank for no change]: ")
if name ~= "" then
self.selected:setText(name)
end
-- bundled cable
local color = colors[self:readInput(
"Bundled cable [blank for none] color: ",
validateColor(true))] -- allow none
self.selected:setCable(color)
self:draw() -- redraw all
end
end,
})
local Application = prototype({
__construct = function(self)
self.diagram = Diagram()
self.notebox = peripheral.wrap(noteboxSide)
self.terminals = { peripheral.wrap("top"), term.native }
self:load()
self.display = Display(self)
self.editor = Editor(self)
end,
load = function(self)
self.diagram:load(diagramFile)
end,
save = function(self)
self.diagram:save(diagramFile)
end,
drawExternal = function(self, func)
for _, m in ipairs(self.terminals) do
if m ~= term.native then -- ignore native
term.redirect(m)
func()
end
end
term.redirect(term.native)
end,
drawAll = function(self, func)
local enabled = self:isEnabled() -- only draw if enabled
for _, m in ipairs(self.terminals) do
if m == term.native or enabled then
term.redirect(m)
func()
end
end
term.redirect(term.native)
end,
show = function(self, view)
self.view = view
self.view:draw()
end,
isEnabled = function(self)
return redstone.getInput(enableSide)
end,
processEvents = function(self)
local active = true
local wasOn = self:isEnabled()
self:show(self.display)
-- clear on start if needed
if not wasOn then
self:drawExternal(function()
term.setBackgroundColor(colors.black)
term.clear()
end)
end
while active do
local id, p1, p2, p3 = os.pullEvent()
local on = self:isEnabled()
if id == "redstone" then
if wasOn ~= on then
wasOn = on
self:drawExternal(function()
-- redraw if turned on
if on then
self.view:draw()
else
-- clear when off
term.setBackgroundColor(colors.black)
term.clear()
end
end)
end
elseif id == "key" then
local key = p1
if key == 221 then
active = false
end
elseif id == "char" then
local ch = p1
self.view:onPress(ch)
elseif id == "mouse_click" then
local button, x, y = p1, p2, p3
self.view:onClick(button, x, y)
elseif id == "mouse_drag" then
local button, x, y = p1, p2, p3
self.view:onDrag(button, x, y)
elseif id == "monitor_touch" and on then -- only if on!
local side, x, y = p1, p2, p3
self.view:onClick(0, x, y)
end
end
self:drawAll(function()
term.setBackgroundColor(colors.black)
term.setTextColor(colors.white)
term.clear()
term.setCursorPos(1, 1)
print("Node Panel exited")
end)
end,
loadExample = function(self)
local boiler = Block(25, 14, "Boiler"):setCable(colors.red)
local engines = Block(10, 6, "Engines"):setCable(colors.white)
local refinery = Block(10, 14, "Refinery"):setCable(colors.black)
local mjStore = Block(25, 6, "MJ Store")
local euStore = Block(40, 6, "EU Store")
local factory = Block(40, 14, "Factory")
boiler:connect(mjStore, colors.orange)
engines:connect(mjStore, colors.orange)
refinery:connect(engines, colors.yellow)
mjStore:connect(euStore, colors.purple)
euStore:connect(factory, colors.purple)
self.diagram:add(boiler)
end
})
local app = Application()
app:processEvents()
bundledSide = "bottom"
noteboxSide = "right"
diagramFile = "layout.txt"
enableSide = "back"
local n, v = "serpent", 0.22 -- (C) 2012 Paul Kulchenko; MIT License
local c, d = "Paul Kulchenko", "Serializer and pretty printer of Lua data types"
local snum = {[tostring(1/0)]='1/0 --[[math.huge]]',[tostring(-1/0)]='-1/0 --[[-math.huge]]',[tostring(0/0)]='0/0'}
local badtype = {thread = true, userdata = true}
local keyword, globals, G = {}, {}, (_G or _ENV)
for _,k in ipairs({'and', 'break', 'do', 'else', 'elseif', 'end', 'false',
'for', 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat',
'return', 'then', 'true', 'until', 'while'}) do keyword[k] = true end
for k,v in pairs(G) do globals[v] = k end -- build func to name mapping
for _,g in ipairs({'coroutine', 'io', 'math', 'string', 'table', 'os'}) do
for k,v in pairs(G[g]) do globals[v] = g..'.'..k end end
local function s(t, opts)
local name, indent, fatal = opts.name, opts.indent, opts.fatal
local sparse, custom, huge = opts.sparse, opts.custom, not opts.nohuge
local space, maxl = (opts.compact and '' or ' '), (opts.maxlevel or math.huge)
local comm = opts.comment and (tonumber(opts.comment) or math.huge)
local seen, sref, syms, symn = {}, {}, {}, 0
local function gensym(val) return (tostring(val):gsub("[^%w]",""):gsub("(%d%w+)",
function(s) if not syms[s] then symn = symn+1; syms[s] = symn end return syms[s] end)) end
local function safestr(s) return type(s) == "number" and (huge and snum[tostring(s)] or s)
or type(s) ~= "string" and tostring(s) -- escape NEWLINE/010 and EOF/026
or ("%q"):format(s):gsub("\010","n"):gsub("\026","\\026") end
local function comment(s,l) return comm and (l or 0) < comm and ' --[['..tostring(s)..']]' or '' end
local function globerr(s,l) return globals[s] and globals[s]..comment(s,l) or not fatal
and safestr(select(2, pcall(tostring, s))) or error("Can't serialize "..tostring(s)) end
local function safename(path, name) -- generates foo.bar, foo[3], or foo['b a r']
local n = name == nil and '' or name
local plain = type(n) == "string" and n:match("^[%l%u_][%w_]*$") and not keyword[n]
local safe = plain and n or '['..safestr(n)..']'
return (path or '')..(plain and path and '.' or '')..safe, safe end
local alphanumsort = type(opts.sortkeys) == 'function' and opts.sortkeys or function(o, n)
local maxn, to = tonumber(n) or 12, {number = 'a', string = 'b'}
local function padnum(d) return ("%0"..maxn.."d"):format(d) end
table.sort(o, function(a,b)
return (o[a] and 0 or to[type(a)] or 'z')..(tostring(a):gsub("%d+",padnum))
< (o[b] and 0 or to[type(b)] or 'z')..(tostring(b):gsub("%d+",padnum)) end) end
local function val2str(t, name, indent, insref, path, plainindex, level)
local ttype, level = type(t), (level or 0)
local spath, sname = safename(path, name)
local tag = plainindex and
((type(name) == "number") and '' or name..space..'='..space) or
(name ~= nil and sname..space..'='..space or '')
if seen[t] then -- if already seen and in sref processing,
if insref then return tag..seen[t] end -- then emit right away
table.insert(sref, spath..space..'='..space..seen[t])
return tag..'nil'..comment('ref', level)
elseif badtype[ttype] then
seen[t] = spath
return tag..globerr(t, level)
elseif ttype == 'function' then
seen[t] = insref or spath
local ok, res = pcall(string.dump, t)
local func = ok and ((opts.nocode and "function() --[[..skipped..]] end" or
"loadstring("..safestr(res)..",'@serialized')")..comment(t, level))
return tag..(func or globerr(t, level))
elseif ttype == "table" then
if level >= maxl then return tag..'{}'..comment('max', level) end
seen[t] = insref or spath -- set path to use as reference
if getmetatable(t) and getmetatable(t).__tostring
then return tag..val2str(tostring(t),nil,indent,false,nil,nil,level+1)..comment("meta", level) end
if next(t) == nil then return tag..'{}'..comment(t, level) end -- table empty
local maxn, o, out = #t, {}, {}
for key = 1, maxn do table.insert(o, key) end
for key in pairs(t) do if not o[key] then table.insert(o, key) end end
if opts.sortkeys then alphanumsort(o, opts.sortkeys) end
for n, key in ipairs(o) do
local value, ktype, plainindex = t[key], type(key), n <= maxn and not sparse
if opts.valignore and opts.valignore[value] -- skip ignored values; do nothing
or opts.keyallow and not opts.keyallow[key]
or opts.keynounderscore and type(key) == "string" and string.sub(key, 1, 1) == "_"
or opts.valtypeignore and opts.valtypeignore[type(value)] -- skipping ignored value types
or sparse and value == nil then -- skipping nils; do nothing
elseif ktype == 'table' or ktype == 'function' or badtype[ktype] then
if not seen[key] and not globals[key] then
table.insert(sref, 'placeholder')
sref[#sref] = 'local '..val2str(key,gensym(key),indent,gensym(key)) end
table.insert(sref, 'placeholder')
local path = seen[t]..'['..(seen[key] or globals[key] or gensym(key))..']'
sref[#sref] = path..space..'='..space..(seen[value] or val2str(value,nil,indent,path))
else
table.insert(out,val2str(value,key,indent,insref,seen[t],plainindex,level+1))
end
end
local prefix = string.rep(indent or '', level)
local head = indent and '{\n'..prefix..indent or '{'
local body = table.concat(out, ','..(indent and '\n'..prefix..indent or space))
local tail = indent and "\n"..prefix..'}' or '}'
return (custom and custom(tag,head,body,tail) or tag..head..body..tail)..comment(t, level)
else return tag..safestr(t) end -- handle all other types
end
local sepr = indent and "\n" or ";"..space
local body = val2str(t, name, indent) -- this call also populates sref
local tail = #sref>0 and table.concat(sref, sepr)..sepr or ''
return not name and body or "do local "..body..sepr..tail.."return "..name..sepr.."end"
end
local function merge(a, b) if b then for k,v in pairs(b) do a[k] = v end end; return a; end
serpent = { _NAME = n, _COPYRIGHT = c, _DESCRIPTION = d, _VERSION = v, serialize = s,
dump = function(a, opts) return s(a, merge({name = '_', compact = true, sparse = true}, opts)) end,
line = function(a, opts) return s(a, merge({sortkeys = true, comment = true}, opts)) end,
block = function(a, opts) return s(a, merge({indent = ' ', sortkeys = true, comment = true}, opts)) end }
function prototype(parent, t)
if not t then
t = parent
parent = nil
end -- when there is no inheritance
local t = t or {}
local mt = { __index = t }
setmetatable(t, {
__call = function(_, ...)
local obj = setmetatable({}, mt)
if obj.__construct then obj:__construct(unpack(arg)) end
return obj
end,
__index = parent
})
return t
end
function prototyptify(cls, obj)
local mt = {}
mt.__index = cls
setmetatable(obj, mt)
return obj
end
function drawPixel(xPos, yPos, text)
term.setCursorPos(xPos, yPos)
term.write(text or " ")
end
-- from ComputerCraft
function drawLine(startX, startY, endX, endY, text, bgColor, textColor)
if bgColor then term.setBackgroundColor(bgColor) end
if textColor then term.setTextColor(textColor) end
startX = math.floor(startX)
startY = math.floor(startY)
endX = math.floor(endX)
endY = math.floor(endY)
if startX == endX and startY == endY then
drawPixel(startX, startY, text)
return
end
local minX = math.min(startX, endX)
if minX == startX then
minY = startY
maxX = endX
maxY = endY
else
minY = endY
maxX = startX
maxY = startY
end
local xDiff = maxX - minX
local yDiff = maxY - minY
if xDiff > math.abs(yDiff) then
local y = minY
local dy = yDiff / xDiff
for x=minX,maxX do
drawPixel(x, math.floor(y + 0.5), text)
y = y + dy
end
else
local x = minX
local dx = xDiff / yDiff
if maxY >= minY then
for y=minY,maxY do
drawPixel(math.floor(x + 0.5), y, text)
x = x + dx
end
else
for y=minY,maxY,-1 do
drawPixel(math.floor(x + 0.5), y, text)
x = x - dx
end
end
end
end
function drawText(x, y, text, bgColor, textColor)
term.setCursorPos(x, y)
term.setBackgroundColor(bgColor or colors.black)
term.setTextColor(textColor or colors.black)
term.write(text)
end
function getColor(num)
for k, v in pairs(colors) do
if v == num then
return k
end
end
return "?"
end
function trim(s)
return s:match"^%s*(.*)":match"(.-)%s*$"
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment