aboutsummaryrefslogtreecommitdiff
path: root/Programs/NPaintPro.bup
diff options
context:
space:
mode:
Diffstat (limited to 'Programs/NPaintPro.bup')
-rw-r--r--Programs/NPaintPro.bup/Contents/Info.meta1
-rw-r--r--Programs/NPaintPro.bup/Contents/Resources/resources_here.txt1
-rw-r--r--Programs/NPaintPro.bup/Contents/bits-UI/npaintpro.lua2827
3 files changed, 2829 insertions, 0 deletions
diff --git a/Programs/NPaintPro.bup/Contents/Info.meta b/Programs/NPaintPro.bup/Contents/Info.meta
new file mode 100644
index 0000000..8d1c8b6
--- /dev/null
+++ b/Programs/NPaintPro.bup/Contents/Info.meta
@@ -0,0 +1 @@
+
diff --git a/Programs/NPaintPro.bup/Contents/Resources/resources_here.txt b/Programs/NPaintPro.bup/Contents/Resources/resources_here.txt
new file mode 100644
index 0000000..8d1c8b6
--- /dev/null
+++ b/Programs/NPaintPro.bup/Contents/Resources/resources_here.txt
@@ -0,0 +1 @@
+
diff --git a/Programs/NPaintPro.bup/Contents/bits-UI/npaintpro.lua b/Programs/NPaintPro.bup/Contents/bits-UI/npaintpro.lua
new file mode 100644
index 0000000..56f7ef0
--- /dev/null
+++ b/Programs/NPaintPro.bup/Contents/bits-UI/npaintpro.lua
@@ -0,0 +1,2827 @@
+--[[
+ NPaintPro
+ By NitrogenFingers
+]]--
+
+--The screen size
+local w,h = term.getSize()
+--Whether or not the program is currently waiting on user input
+local inMenu = false
+--Whether or not a drop down menu is active
+local inDropDown = false
+--Whether or not animation tools are enabled (use -a to turn them on)
+local animated = false
+--Whether or not the text tools are enabled (use -t to turn them on)
+local textual = false
+--Whether or not "blueprint" display mode is on
+local blueprint = false
+--Whether or not the "layer" display is on
+local layerDisplay = false
+--Whether or not the interface is presently hidden
+local interfaceHidden = false
+--Whether or not the "direction" display is on
+local printDirection = false
+--The tool/mode npaintpro is currently in. Default is "paint"
+--For a list of modes, check out the help file
+local state = "paint"
+--Whether or not the program is presently running
+local isRunning = true
+--The rednet address of the 3D printer, if one has been attached
+local printer = nil
+
+--The list of every frame, containing every image in the picture/animation
+--Note: nfp files always have the picture at frame 1
+local frames = { }
+--How many frames are currently in the given animation.
+local frameCount = 1
+--The Colour Picker column
+local column = {}
+--The offset of visible colours in the picker column, if the screen cannot fit all 16
+local columnoffset = 0
+--The currently selected left and right colours
+local lSel,rSel = colours.white,nil
+--The amount of scrolling on the X and Y axis
+local sx,sy = 0,0
+--The alpha channel colour
+--Change this to change default canvas colour
+local alphaC = colours.black
+--The currently selected frame. Default is 1
+local sFrame = 1
+--The contents of the image buffer- contains contents, width and height
+local buffer = nil
+--The position, width and height of the selection rectangle
+local selectrect = nil
+
+--Whether or not text tools are enabled for this document
+local textEnabled = false
+--The X and Y positions of the text cursor
+local textCurX, textCurY = 1,1
+
+--The currently calculated required materials
+local requiredMaterials = {}
+--Whether or not required materials are being displayed in the pallette
+local requirementsDisplayed = false
+--A list of the rednet ID's all in-range printers located
+local printerList = { }
+--A list of the names of all in-range printers located. Same as the printerList in reference
+local printerNames = { }
+--The selected printer
+local selectedPrinter = 1
+--The X,Y,Z and facing of the printer
+local px,py,pz,pfx,pfz = 0,0,0,0,0
+--The form of layering used
+local layering = "up"
+
+--The animation state of the selection rectangle and image buffer
+local rectblink = 0
+--The ID for the timer
+local recttimer = nil
+--The radius of the brush tool
+local brushsize = 3
+--Whether or not "record" mode is activated (animation mode only)
+local record = false
+--The time between each frame when in play mode (animation mode only)
+local animtime = 0.3
+
+--The current "cursor position" in text mode
+local cursorTexX,cursorTexY = 1,1
+
+--A list of hexidecimal conversions from numbers to hex digits
+local hexnums = { [10] = "a", [11] = "b", [12] = "c", [13] = "d", [14] = "e" , [15] = "f" }
+--The NPaintPro logo (divine, isn't it?)
+local logo = {
+"fcc 3 339";
+" fcc 9333 33";
+" fcc 933 333 33";
+" fcc 933 33 33";
+" fcc 933 33 33";
+" c88 333 93333";
+" 888 333 9333";
+" 333 3 333 939";
+}
+--The Layer Up and Layer Forward printing icons
+local layerUpIcon = {
+ "0000000";
+ "0088880";
+ "0888870";
+ "07777f0";
+ "0ffff00";
+ "0000000";
+}
+local layerForwardIcon = {
+ "0000000";
+ "000fff0";
+ "00777f0";
+ "0888700";
+ "0888000";
+ "0000000";
+}
+--The available menu options in the ctrl menu
+local mChoices = {"Save","Exit"}
+--The available modes from the dropdown menu- tables indicate submenus (include a name!)
+local ddModes = { { "paint", "brush", "pippette", "flood", "move", "clear", "select", name = "painting" }, { "alpha to left", "alpha to right", "hide interface", name = "display" }, "help", { "print", "save", "exit", name = "file" }, name = "menu" }
+--The available modes from the selection right-click menu
+local srModes = { "cut", "copy", "paste", "clear", "hide", name = "selection" }
+--The list of available help topics for each mode 127
+local helpTopics = {
+ [1] = {
+ name = "Paint Mode",
+ key = nil,
+ animonly = false,
+ textonly = false,
+ message = "The default mode for NPaintPro, for painting pixels."
+ .." Controls here that are not overridden will apply for all other modes. Leaving a mode by selecting that mode "
+ .." again will always send the user back to paint mode.",
+ controls = {
+ { "Arrow keys", "Scroll the canvas" },
+ { "Left Click", "Paint/select left colour" },
+ { "Right Click", "Paint/select right colour" },
+ { "Z Key", "Clear image on screen" },
+ { "Tab Key", "Hide selection rectangle if visible" },
+ { "Q Key", "Set alpha mask to left colour" },
+ { "W Key", "Set alpha mask to right colour" },
+ { "Number Keys", "Swich between frames 1-9" },
+ { "</> keys", "Move to the next/last frame" },
+ { "R Key", "Removes every frame after the current frame"}
+ }
+ },
+ [2] = {
+ name = "Brush Mode",
+ key = "b",
+ animonly = false,
+ textonly = false,
+ message = "Brush mode allows painting a circular area of variable diameter rather than a single pixel, working in "..
+ "the exact same way as paint mode in all other regards.",
+ controls = {
+ { "Left Click", "Paints a brush blob with the left colour" },
+ { "Right Click", "Paints a brush blob with the right colour" },
+ { "Number Keys", "Changes the radius of the brush blob from 2-9" }
+ }
+ },
+ [3] = {
+ name = "Pippette Mode",
+ key = "p",
+ animonly = false,
+ textonly = false,
+ message = "Pippette mode allows the user to click the canvas and set the colour clicked to the left or right "..
+ "selected colour, for later painting.",
+ controls = {
+ { "Left Click", "Sets clicked colour to the left selected colour" },
+ { "Right Click", "Sets clicked colour to the right selected colour" }
+ }
+ },
+ [4] = {
+ name = "Move Mode",
+ key = "m",
+ animonly = false,
+ textonly = false,
+ message = "Mode mode allows the moving of the entire image on the screen. This is especially useful for justifying"..
+ " the image to the top-left for animations or game assets.",
+ controls = {
+ { "Left/Right Click", "Moves top-left corner of image to selected square" },
+ { "Arrow keys", "Moves image one pixel in any direction" }
+ }
+ },
+ [5] = {
+ name = "Flood Mode",
+ key = "f",
+ animonly = false,
+ textonly = false,
+ message = "Flood mode allows the changing of an area of a given colour to that of the selected colour. "..
+ "The tool uses a flood4 algorithm and will not fill diagonally. Transparency cannot be flood filled.",
+ controls = {
+ { "Left Click", "Flood fills selected area to left colour" },
+ { "Right Click", "Flood fills selected area to right colour" }
+ }
+ },
+ [6] = {
+ name = "Select Mode",
+ key = "s",
+ animonly = false,
+ textonly = false,
+ message = "Select mode allows the creation and use of the selection rectangle, to highlight specific areas on "..
+ "the screen and perform operations on the selected area of the image. The selection rectangle can contain an "..
+ "image on the clipboard- if it does, the image will flash inside the rectangle, and the rectangle edges will "..
+ "be light grey instead of dark grey.",
+ controls = {
+ { "C Key", "Copy: Moves selection into the clipboard" },
+ { "X Key", "Cut: Clears canvas under the rectangle, and moves it into the clipboard" },
+ { "V Key", "Paste: Copys clipboard to the canvas" },
+ { "Z Key", "Clears clipboard" },
+ { "Left Click", "Moves top-left corner of rectangle to selected pixel" },
+ { "Right Click", "Opens selection menu" },
+ { "Arrow Keys", "Moves rectangle one pixel in any direction" }
+ }
+ },
+ [7] = {
+ name = "Corner Select Mode",
+ key = nil,
+ animonly = false,
+ textonly = false,
+ message = "If a selection rectangle isn't visible, this mode is selected automatically. It allows the "..
+ "defining of the corners of the rectangle- one the top-left and bottom-right corners have been defined, "..
+ "NPaintPro switches to selection mode. Note rectangle must be at least 2 pixels wide and high.",
+ controls = {
+ { "Left/Right Click", "Defines a corner of the selection rectangle" }
+ }
+ },
+ [8] = {
+ name = "Play Mode",
+ key = "space",
+ animonly = true,
+ textonly = false,
+ message = "Play mode will loop through each frame in your animation at a constant rate. Editing tools are "..
+ "locked in this mode, and the coordinate display will turn green to indicate it is on.",
+ controls = {
+ { "</> Keys", "Increases/Decreases speed of the animation" },
+ { "Space Bar", "Returns to paint mode" }
+ }
+ },
+ [9] = {
+ name = "Record Mode",
+ key = "\\",
+ animonly = true,
+ textonly = false,
+ message = "Record mode is not a true mode, but influences how other modes work. Changes made that modify the "..
+ "canvas in record mode will affect ALL frames in the animation. The coordinates will turn red to indicate that "..
+ "record mode is on.",
+ controls = {
+ { "", "Affects:" },
+ { "- Paint Mode", "" },
+ { "- Brush Mode", "" },
+ { "- Cut and Paste in Select Mode", ""},
+ { "- Move Mode", ""}
+ }
+ },
+ [10] = {
+ name = "Hide Interface",
+ key = "~",
+ animonly = false,
+ textonly = false,
+ message = "Hides the sidebar and colour picker so only the image is visible."..
+ " The program can be started with the interface hidden using the -d command line option."..
+ " When hidden, if a file is animated it will automatically go to play mode.\n"..
+ "Note that all other input is locked until the display is revealed again in this"..
+ " mode.",
+ controls = {
+ { "</> Keys", "Increases/Decreases speed of the animation" },
+ { "~ Key", "Shows interface"}
+ }
+ },
+ [11] = {
+ name = "Help Mode",
+ key = "h",
+ animonly = false,
+ textonly = false,
+ message = "Displays this help screen. Clicking on options will display help on that topic. Clicking out of the screen"..
+ " will leave this mode.",
+ controls = {
+ { "Left/Right Click", "Displays a topic/Leaves the mode" }
+ }
+ },
+ [12] = {
+ name = "File Mode",
+ key = nil,
+ animonly = false,
+ textonly = false,
+ message = "Clicking on the mode display at the bottom of the screen will open the options menu. Here you can"..
+ " activate all of the modes in the program with a simple mouse click. Pressing left control will open up the"..
+ " file menu automatically.",
+ controls = {
+ { "leftCtrl", "Opens the file menu" },
+ { "leftAlt", "Opens the paint menu" }
+ }
+ },
+ [13] = {
+ name = "Text Mode",
+ key = "t",
+ animonly = false,
+ textonly = true,
+ message = "In this mode, the user is able to type letters onto the document for display. The left colour "..
+ "pallette value determines what colour the text will be, and the right determines what colour the background "..
+ "will be (set either to nil to keep the same colours as already there).",
+ controls = {
+ { "Backspace", "Deletes the character on the previous line" },
+ { "Arrow Keys", "Moves the cursor in any direction" },
+ { "Left Click", "Moves the cursor to beneath the mouse cursor" }
+ }
+ },
+ [14] = {
+ name = "Textpaint Mode",
+ key = "y",
+ animonly = false,
+ textonly = true,
+ message = "Allows the user to paint any text on screen to the desired colour with the mouse. If affects the text colour"..
+ " values rather than the background values, but operates identically to paint mode in all other regards.",
+ controls = {
+ { "Left Click", "Paints the text with the left colour" },
+ { "Right Click", "Paints the text with the right colour" }
+ }
+ },
+ [15] = {
+ name = "About NPaintPro",
+ keys = nil,
+ animonly = false,
+ textonly = false,
+ message = "NPaintPro: The feature-bloated paint program for ComputerCraft by Nitrogen Fingers.",
+ controls = {
+ { "Testers:", " "},
+ { " ", "Faubiguy"},
+ { " ", "TheOriginalBIT"}
+ }
+ }
+}
+--The "bounds" of the image- the first/last point on both axes where a pixel appears
+local toplim,botlim,leflim,riglim = nil,nil,nil,nil
+--The selected path
+local sPath = nil
+
+
+--Screen Size Parameters- decided dynamically further down the program
+--Whether or not the help screen is available
+local helpAvailable = true
+--Whether or not the main menu is available
+local mainAvailable = true
+--Whether or not selection box dropdowns are available
+local boxdropAvailable = true
+--Whether or not a manual file descriptor option is available (part of the title)
+local filemakerAvailable = true
+
+--[[
+ Section: Helpers
+]]--
+
+--[[Converts a colour parameter into a single-digit hex coordinate for the colour
+ Params: colour:int = The colour to be converted
+ Returns:string A string conversion of the colour
+]]--
+local function getHexOf(colour)
+ if not colour or not tonumber(colour) then
+ return " "
+ end
+ local value = math.log(colour)/math.log(2)
+ if value > 9 then
+ value = hexnums[value]
+ end
+ return value
+end
+
+--[[Converts a hex digit into a colour value
+ Params: hex:?string = the hex digit to be converted
+ Returns:string A colour value corresponding to the hex, or nil if the character is invalid
+]]--
+local function getColourOf(hex)
+ local value = tonumber(hex, 16)
+ if not value then return nil end
+ value = math.pow(2,value)
+ return value
+end
+
+--[[Finds the largest width and height of the text in a given menu. Should conform to the format
+ of all standard menus (number indexed values and a 'name' field).
+ This is done recursively. It's just easier that way.
+ Params: menu:table = the table being tested for the max width and height
+ Returns:number,number = the max width and height of the text or names of any menu or submenu.
+]]--
+local function findMaxWH(menu)
+ local wmax,hmax = #menu.name, #menu
+ for _,entry in pairs(menu) do
+ if type(entry) == "table" then
+ local nw,nh = findMaxWH(entry)
+ wmax = math.max(wmax,nw)
+ hmax = math.max(hmax,nh)
+ else
+ wmax = math.max(wmax,#entry)
+ end
+ end
+ return wmax,hmax
+end
+
+--[[Determines what services are available depending on the size of the screen. Certain features
+ may be disabled with screen real estate does not allow for it.
+ Params: none
+ Returns:nil
+]]--
+local function determineAvailableServices()
+ --Help files were designed to fit a 'standard' CC screen, of 51 x 19. The height of the screen
+ --needs to match the number of available options plus white space, but for consistency with
+ --the files themselves, a natural size of 51 is required for the screen width as well.
+ if w < 51 or h < #helpTopics+3 then helpAvailable = false end
+ if not helpAvailable then table.remove(ddModes,3) end
+ --These hard-coded values mirror the drawLogo values, with extra consideration for the
+ --additional menu options
+ if h < 14 or w < 24 then filemakerAvailable = false end
+
+ --Menus can't cover the picker and need 2 spaces for branches. 4 whitespace on X total.
+ --Menus need a title and can't eclipse the footer. 2 whitespace on Y total.
+ local wmin,hmin = findMaxWH(ddModes)
+ if w < wmin+4 or h < hmin+2 then mainAvailable = false end
+ wmin,hmin = findMaxWH(srModes)
+ if w < wmin+4 or h < hmin+2 then boxdropAvailable = false end
+end
+
+--[[Finds the biggest and smallest bounds of the image- the outside points beyond which pixels do not appear
+ These values are assigned to the "lim" parameters for access by other methods
+ Params: forAllFrames:bool = True if all frames should be used to find bounds, otherwise false or nil
+ Returns:nil
+]]--
+local function updateImageLims(forAllFrames)
+ local f,l = sFrame,sFrame
+ if forAllFrames == true then f,l = 1,framecount end
+
+ toplim,botlim,leflim,riglim = nil,nil,nil,nil
+ for locf = f,l do
+ for y,_ in pairs(frames[locf]) do
+ if type(y) == "number" then
+ for x,_ in pairs(frames[locf][y]) do
+ if frames[locf][y][x] ~= nil then
+ if leflim == nil or x < leflim then leflim = x end
+ if toplim == nil or y < toplim then toplim = y end
+ if riglim == nil or x > riglim then riglim = x end
+ if botlim == nil or y > botlim then botlim = y end
+ end
+ end
+ end
+ end
+ end
+
+ --There is just... no easier way to do this. It's horrible, but necessary
+ if textEnabled then
+ for locf = f,l do
+ for y,_ in pairs(frames[locf].text) do
+ for x,_ in pairs(frames[locf].text[y]) do
+ if frames[locf].text[y][x] ~= nil then
+ if leflim == nil or x < leflim then leflim = x end
+ if toplim == nil or y < toplim then toplim = y end
+ if riglim == nil or x > riglim then riglim = x end
+ if botlim == nil or y > botlim then botlim = y end
+ end
+ end
+ end
+ for y,_ in pairs(frames[locf].textcol) do
+ for x,_ in pairs(frames[locf].textcol[y]) do
+ if frames[locf].textcol[y][x] ~= nil then
+ if leflim == nil or x < leflim then leflim = x end
+ if toplim == nil or y < toplim then toplim = y end
+ if riglim == nil or x > riglim then riglim = x end
+ if botlim == nil or y > botlim then botlim = y end
+ end
+ end
+ end
+ end
+ end
+end
+
+--[[Determines how much of each material is required for a print. Done each time printing is called.
+ Params: none
+ Returns:table A complete list of how much of each material is required.
+]]--
+function calculateMaterials()
+ updateImageLims(animated)
+ requiredMaterials = {}
+ for i=1,16 do
+ requiredMaterials[i] = 0
+ end
+
+ if not toplim then return end
+
+ for i=1,#frames do
+ for y = toplim, botlim do
+ for x = leflim, riglim do
+ if type(frames[i][y][x]) == "number" then
+ requiredMaterials[math.log10(frames[i][y][x])/math.log10(2) + 1] =
+ requiredMaterials[math.log10(frames[i][y][x])/math.log10(2) + 1] + 1
+ end
+ end
+ end
+ end
+end
+
+
+--[[Updates the rectangle blink timer. Should be called anywhere events are captured, along with a timer capture.
+ Params: nil
+ Returns:nil
+]]--
+local function updateTimer(id)
+ if id == recttimer then
+ recttimer = os.startTimer(0.5)
+ rectblink = (rectblink % 2) + 1
+ end
+end
+
+--[[Constructs a message based on the state currently selected
+ Params: nil
+ Returns:string A message regarding the state of the application
+]]--
+local function getStateMessage()
+ local msg = " "..string.upper(string.sub(state, 1, 1))..string.sub(state, 2, #state).." mode"
+ if state == "brush" then msg = msg..", size="..brushsize end
+ return msg
+end
+
+--[[Calls the rednet_message event, but also looks for timer events to keep then
+ system timer ticking.
+ Params: timeout:number how long before the event times out
+ Returns:number the id of the sender
+ :number the message send
+]]--
+local function rsTimeReceive(timeout)
+ local timerID
+ if timeout then timerID = os.startTimer(timeout) end
+
+ local id,key,msg = nil,nil
+ while true do
+ id,key,msg = os.pullEvent()
+
+ if id == "timer" then
+ if key == timerID then return
+ else updateTimer(key) end
+ end
+ if id == "rednet_message" then
+ return key,msg
+ end
+ end
+end
+
+--[[Draws a picture, in paint table format on the screen
+ Params: image:table = the image to display
+ xinit:number = the x position of the top-left corner of the image
+ yinit:number = the y position of the top-left corner of the image
+ alpha:number = the color to display for the alpha channel. Default is white.
+ Returns:nil
+]]--
+local function drawPictureTable(image, xinit, yinit, alpha)
+ if not alpha then alpha = 1 end
+ for y=1,#image do
+ for x=1,#image[y] do
+ term.setCursorPos(xinit + x-1, yinit + y-1)
+ local col = getColourOf(string.sub(image[y], x, x))
+ if not col then col = alpha end
+ term.setBackgroundColour(col)
+ term.write(" ")
+ end
+ end
+end
+
+--[[
+ Section: Loading
+]]--
+
+--[[Loads a non-animted paint file into the program
+ Params: path:string = The path in which the file is located
+ Returns:nil
+]]--
+local function loadNFP(path)
+ sFrame = 1
+ frames[sFrame] = { }
+ if fs.exists(path) then
+ local file = io.open(path, "r" )
+ local sLine = file:read()
+ local num = 1
+ while sLine do
+ table.insert(frames[sFrame], num, {})
+ for i=1,#sLine do
+ frames[sFrame][num][i] = getColourOf(string.sub(sLine,i,i))
+ end
+ num = num+1
+ sLine = file:read()
+ end
+ file:close()
+ end
+end
+
+--[[Loads a text-paint file into the program
+ Params: path:string = The path in which the file is located
+ Returns:nil
+]]--
+local function loadNFT(path)
+ sFrame = 1
+ frames[sFrame] = { }
+ frames[sFrame].text = { }
+ frames[sFrame].textcol = { }
+
+ if fs.exists(path) then
+ local file = io.open(path, "r")
+ local sLine = file:read()
+ local num = 1
+ while sLine do
+ table.insert(frames[sFrame], num, {})
+ table.insert(frames[sFrame].text, num, {})
+ table.insert(frames[sFrame].textcol, num, {})
+
+ --As we're no longer 1-1, we keep track of what index to write to
+ local writeIndex = 1
+ --Tells us if we've hit a 30 or 31 (BG and FG respectively)- next char specifies the curr colour
+ local bgNext, fgNext = false, false
+ --The current background and foreground colours
+ local currBG, currFG = nil,nil
+ term.setCursorPos(1,1)
+ for i=1,#sLine do
+ local nextChar = string.sub(sLine, i, i)
+ if nextChar:byte() == 30 then
+ bgNext = true
+ elseif nextChar:byte() == 31 then
+ fgNext = true
+ elseif bgNext then
+ currBG = getColourOf(nextChar)
+ bgNext = false
+ elseif fgNext then
+ currFG = getColourOf(nextChar)
+ fgNext = false
+ else
+ if nextChar ~= " " and currFG == nil then
+ currFG = colours.white
+ end
+ frames[sFrame][num][writeIndex] = currBG
+ frames[sFrame].textcol[num][writeIndex] = currFG
+ frames[sFrame].text[num][writeIndex] = nextChar
+ writeIndex = writeIndex + 1
+ end
+ end
+ num = num+1
+ sLine = file:read()
+ end
+ file:close()
+ end
+end
+
+--[[Loads an animated paint file into the program
+ Params: path:string = The path in which the file is located
+ Returns:nil
+]]--
+local function loadNFA(path)
+ frames[sFrame] = { }
+ if fs.exists(path) then
+ local file = io.open(path, "r" )
+ local sLine = file:read()
+ local num = 1
+ while sLine do
+ table.insert(frames[sFrame], num, {})
+ if sLine == "~" then
+ sFrame = sFrame + 1
+ frames[sFrame] = { }
+ num = 1
+ else
+ for i=1,#sLine do
+ frames[sFrame][num][i] = getColourOf(string.sub(sLine,i,i))
+ end
+ num = num+1
+ end
+ sLine = file:read()
+ end
+ file:close()
+ end
+ framecount = sFrame
+ sFrame = 1
+end
+
+--[[Saves a non-animated paint file to the specified path
+ Params: path:string = The path to save the file to
+ Returns:nil
+]]--
+local function saveNFP(path)
+ local sDir = string.sub(sPath, 1, #sPath - #fs.getName(sPath))
+ if not fs.exists(sDir) then
+ fs.makeDir(sDir)
+ end
+
+ local file = io.open(path, "w")
+ updateImageLims(false)
+ if not toplim then
+ file:close()
+ return
+ end
+ for y=1,botlim do
+ local line = ""
+ if frames[sFrame][y] then
+ for x=1,riglim do
+ line = line..getHexOf(frames[sFrame][y][x])
+ end
+ end
+ file:write(line.."\n")
+ end
+ file:close()
+end
+
+--[[Saves a text-paint file to the specified path
+ Params: path:string = The path to save the file to
+ Returns:nil
+]]--
+local function saveNFT(path)
+ local sDir = string.sub(sPath, 1, #sPath - #fs.getName(sPath))
+ if not fs.exists(sDir) then
+ fs.makeDir(sDir)
+ end
+
+ local file = io.open(path, "w")
+ updateImageLims(false)
+ if not toplim then
+ file:close()
+ return
+ end
+ for y=1,botlim do
+ local line = ""
+ local currBG, currFG = nil,nil
+ for x=1,riglim do
+ if frames[sFrame][y] and frames[sFrame][y][x] ~= currBG then
+ line = line..string.char(30)..getHexOf(frames[sFrame][y][x])
+ currBG = frames[sFrame][y][x]
+ end
+ if frames[sFrame].textcol[y] and frames[sFrame].textcol[y][x] ~= currFG then
+ line = line..string.char(31)..getHexOf(frames[sFrame].textcol[y][x])
+ currFG = frames[sFrame].textcol[y][x]
+ end
+ if frames[sFrame].text[y] then
+ local char = frames[sFrame].text[y][x]
+ if not char then char = " " end
+ line = line..char
+ end
+ end
+ file:write(line.."\n")
+ end
+ file:close()
+end
+
+--[[Saves a animated paint file to the specified path
+ Params: path:string = The path to save the file to
+ Returns:nil
+]]--
+local function saveNFA(path)
+ local sDir = string.sub(sPath, 1, #sPath - #fs.getName(sPath))
+ if not fs.exists(sDir) then
+ fs.makeDir(sDir)
+ end
+
+ local file = io.open(path, "w")
+ updateImageLims(true)
+ if not toplim then
+ file:close()
+ return
+ end
+ for i=1,#frames do
+ for y=1,botlim do
+ local line = ""
+ if frames[i][y] then
+ for x=1,riglim do
+ line = line..getHexOf(frames[i][y][x])
+ end
+ end
+ file:write(line.."\n")
+ end
+ if i < #frames then file:write("~\n") end
+ end
+ file:close()
+end
+
+--[[Runs a special pre-program dialogue to determine the filename and filepath. Done if
+ there's room, and a file name hasn't been specified
+ Params: none
+ Returns:bool= true if file is created; false otherwise
+]]--
+local function runFileMaker()
+ local newFName = ""
+ local fileType = 1
+ if animated then fileType = 2
+ elseif textEnabled then fileType = 3 end
+
+ local tlx,tly = math.floor(w/2 - #logo[1]/2), math.floor(h/2 + #logo/2 + 1)
+
+ --This is done on top of the logo, so it backpedals a bit.
+ term.setCursorPos(tlx, tly)
+ term.clearLine()
+ term.write("Name: ")
+ term.setCursorPos(tlx, tly + 1)
+ term.clearLine()
+ term.write("Filetype: Sprite")
+ term.setCursorPos(tlx + 12, tly + 2)
+ term.write("Animation")
+ term.setCursorPos(tlx + 12, tly + 3)
+ term.write("Text")
+
+ while true do
+ term.setCursorPos(tlx + 6, tly)
+ term.setBackgroundColour(colours.lightGrey)
+ term.setTextColour(colours.grey)
+ term.write(newFName..string.rep(" ", 15-#newFName))
+ term.setBackgroundColour(colours.white)
+ term.setTextColour(colours.black)
+ local extension = ".nfp"
+ if fileType == 2 then extension = ".nfa"
+ elseif fileType == 3 then extension = ".nft" end
+ term.write(extension)
+
+ term.setBackgroundColour(colours.lightGrey)
+ term.setTextColour(colours.grey)
+ for i=1,3 do
+ term.setCursorPos(tlx + 24, tly + i)
+ if i==fileType then term.write("X")
+ else term.write(" ") end
+ end
+
+ local fPath = shell.resolve(newFName..extension)
+ term.setCursorPos(tlx, tly + 3)
+ local fileValid = true
+ if (fs.exists(fPath) and fs.isDir(fPath)) or newFName == "" then
+ term.setBackgroundColour(colours.white)
+ term.setTextColour(colours.red)
+ term.write("Invalid ")
+ fileValid = false
+ elseif fs.exists(fPath) then
+ term.setBackgroundColour(colours.grey)
+ term.setTextColour(colours.lightGrey)
+ term.write(" Edit ")
+ else
+ term.setBackgroundColour(colours.grey)
+ term.setTextColour(colours.lightGrey)
+ term.write(" Create ")
+ end
+
+ term.setTextColour(colours.grey)
+ term.setCursorPos(tlx + 6 + #newFName, tly)
+ term.setCursorBlink(true)
+
+ local id,p1,p2,p3 = os.pullEvent()
+ if id == "key" then
+ if p1 == keys.backspace and #newFName > 0 then
+ newFName = string.sub(newFName, 1, #newFName-1)
+ elseif p1 == keys.enter and fileValid then
+ sPath = fPath
+ return true
+ end
+ elseif id == "char" and p1 ~= "." and p1 ~= " " and #newFName < 15 then
+ newFName = newFName..p1
+ elseif id == "mouse_click" then
+ --The option boxes. Man, hardcoding is ugly...
+ if p2 == tlx + 24 then
+ for i=1,3 do
+ if p3 == tly+i then fileType = i end
+ end
+ end
+ if p3 == tly + 3 and p2 >= tlx and p2 <= tlx + 8 and fileValid then
+ sPath = fPath
+ return true
+ end
+ end
+ end
+end
+
+--[[Initializes the program, by loading in the paint file. Called at the start of each program.
+ Params: none
+ Returns:nil
+]]--
+local function init()
+ if textEnabled then
+ loadNFT(sPath)
+ table.insert(ddModes, 2, { "text", "textpaint", name = "text"})
+ elseif animated then
+ loadNFA(sPath)
+ table.insert(ddModes, #ddModes, { "record", "play", name = "anim" })
+ table.insert(ddModes, #ddModes, { "go to", "remove", name = "frames"})
+ table.insert(ddModes[2], #ddModes[2], "blueprint on")
+ table.insert(ddModes[2], #ddModes[2], "layers on")
+ else
+ loadNFP(sPath)
+ table.insert(ddModes[2], #ddModes[2], "blueprint on")
+ end
+
+ for i=0,15 do
+ table.insert(column, math.pow(2,i))
+ end
+end
+
+--[[
+ Section: Drawing
+]]--
+
+
+--[[Draws the rather superflous logo. Takes about 1 second, before user is able to move to the
+ actual program.
+ Params: none
+ Returns:bool= true if the file select ran successfully; false otherwise.
+]]--
+local function drawLogo()
+ term.setBackgroundColour(colours.white)
+ term.clear()
+ if h >= 12 and w >= 24 then
+ drawPictureTable(logo, w/2 - #logo[1]/2, h/2 - #logo/2, colours.white)
+ term.setBackgroundColour(colours.white)
+ term.setTextColour(colours.black)
+ local msg = "NPaintPro"
+ term.setCursorPos(w/2 - #msg/2, h/2 + #logo/2 + 1)
+ term.write(msg)
+ msg = "By NitrogenFingers"
+ term.setCursorPos(w/2 - #msg/2, h/2 + #logo/2 + 2)
+ term.write(msg)
+ elseif w >= 15 then
+ local msg = "NPaintPro"
+ term.setCursorPos(math.ceil(w/2 - #msg/2), h/2)
+ term.setTextColour(colours.cyan)
+ term.write(msg)
+ msg = "NitrogenFingers"
+ term.setCursorPos(math.ceil(w/2 - #msg/2), h/2 + 1)
+ term.setTextColour(colours.black)
+ term.write(msg)
+ else
+ local msg = "NPP"
+ term.setCursorPos(math.ceil(w/2 - #msg/2), math.floor(h/2))
+ term.setTextColour(colours.cyan)
+ term.write(msg)
+ msg = "By NF"
+ term.setCursorPos(math.ceil(w/2 - #msg/2), math.ceil(h/2))
+ term.setTextColour(colours.black)
+ term.write(msg)
+ end
+ os.pullEvent()
+end
+
+--[[Clears the display to the alpha channel colour, draws the canvas, the image buffer and the selection
+ rectanlge if any of these things are present.
+ Params: none
+ Returns:nil
+]]--
+local function drawCanvas()
+ --We have to readjust the position of the canvas if we're printing
+ turtlechar = "@"
+ if state == "active print" then
+ if layering == "up" then
+ if py >= 1 and py <= #frames then
+ sFrame = py
+ end
+ if pz < sy then sy = pz
+ elseif pz > sy + h - 1 then sy = pz + h - 1 end
+ if px < sx then sx = px
+ elseif px > sx + w - 2 then sx = px + w - 2 end
+ else
+ if pz >= 1 and pz <= #frames then
+ sFrame = pz
+ end
+
+ if py < sy then sy = py
+ elseif py > sy + h - 1 then sy = py + h - 1 end
+ if px < sx then sx = px
+ elseif px > sx + w - 2 then sx = px + w - 2 end
+ end
+
+ if pfx == 1 then turtlechar = ">"
+ elseif pfx == -1 then turtlechar = "<"
+ elseif pfz == 1 then turtlechar = "V"
+ elseif pfz == -1 then turtlechar = "^"
+ end
+ end
+
+ --Picture next
+ local topLayer, botLayer
+ if layerDisplay then
+ topLayer = sFrame
+ botLayer = 1
+ else
+ topLayer,botLayer = sFrame,sFrame
+ end
+
+ --How far the canvas draws. If the interface is visible, it stops short of that.
+ local wlim,hlim = 0,0
+ if not interfaceHidden then
+ wlim = 2
+ hlim = 1
+ end
+
+ for currframe = botLayer,topLayer,1 do
+ for y=sy+1,sy+h-hlim do
+ if frames[currframe][y] then
+ for x=sx+1,sx+w-wlim do
+ term.setCursorPos(x-sx,y-sy)
+ if frames[currframe][y][x] then
+ term.setBackgroundColour(frames[currframe][y][x])
+ if textEnabled and frames[currframe].textcol[y][x] and frames[currframe].text[y][x] then
+ term.setTextColour(frames[currframe].textcol[y][x])
+ term.write(frames[currframe].text[y][x])
+ else
+ term.write(" ")
+ end
+ else
+ tileExists = false
+ for i=currframe-1,botLayer,-1 do
+ if frames[i][y][x] then
+ tileExists = true
+ break
+ end
+ end
+
+ if not tileExists then
+ if blueprint then
+ term.setBackgroundColour(colours.blue)
+ term.setTextColour(colours.white)
+ if x == sx+1 and y % 4 == 1 then
+ term.write(""..((y/4) % 10))
+ elseif y == sy + 1 and x % 4 == 1 then
+ term.write(""..((x/4) % 10))
+ elseif x % 2 == 1 and y % 2 == 1 then
+ term.write("+")
+ elseif x % 2 == 1 then
+ term.write("|")
+ elseif y % 2 == 1 then
+ term.write("-")
+ else
+ term.write(" ")
+ end
+ else
+ term.setBackgroundColour(alphaC)
+ if textEnabled and frames[currframe].textcol[y][x] and frames[currframe].text[y][x] then
+ term.setTextColour(frames[currframe].textcol[y][x])
+ term.write(frames[currframe].text[y][x])
+ else
+ term.write(" ")
+ end
+ end
+ end
+ end
+ end
+ else
+ for x=sx+1,sx+w-wlim do
+ term.setCursorPos(x-sx,y-sy)
+
+ tileExists = false
+ for i=currframe-1,botLayer,-1 do
+ if frames[i][y] and frames[i][y][x] then
+ tileExists = true
+ break
+ end
+ end
+
+ if not tileExists then
+ if blueprint then
+ term.setBackgroundColour(colours.blue)
+ term.setTextColour(colours.white)
+ if x == sx+1 and y % 4 == 1 then
+ term.write(""..((y/4) % 10))
+ elseif y == sy + 1 and x % 4 == 1 then
+ term.write(""..((x/4) % 10))
+ elseif x % 2 == 1 and y % 2 == 1 then
+ term.write("+")
+ elseif x % 2 == 1 then
+ term.write("|")
+ elseif y % 2 == 1 then
+ term.write("-")
+ else
+ term.write(" ")
+ end
+ else
+ term.setBackgroundColour(alphaC)
+ term.write(" ")
+ end
+ end
+ end
+ end
+ end
+ end
+
+ --Then the printer, if he's on
+ if state == "active print" then
+ local bgColour = alphaC
+ if layering == "up" then
+ term.setCursorPos(px-sx,pz-sy)
+ if frames[sFrame] and frames[sFrame][pz-sy] and frames[sFrame][pz-sy][px-sx] then
+ bgColour = frames[sFrame][pz-sy][px-sx]
+ elseif blueprint then bgColour = colours.blue end
+ else
+ term.setCursorPos(px-sx,py-sy)
+ if frames[sFrame] and frames[sFrame][py-sy] and frames[sFrame][py-sy][px-sx] then
+ bgColour = frames[sFrame][py-sy][px-sx]
+ elseif blueprint then bgColour = colours.blue end
+ end
+
+ term.setBackgroundColour(bgColour)
+ if bgColour == colours.black then term.setTextColour(colours.white)
+ else term.setTextColour(colours.black) end
+
+ term.write(turtlechar)
+ end
+
+ --Then the buffer
+ if selectrect then
+ if buffer and rectblink == 1 then
+ for y=selectrect.y1, math.min(selectrect.y2, selectrect.y1 + buffer.height-1) do
+ for x=selectrect.x1, math.min(selectrect.x2, selectrect.x1 + buffer.width-1) do
+ if buffer.contents[y-selectrect.y1+1][x-selectrect.x1+1] then
+ term.setCursorPos(x+sx,y+sy)
+ term.setBackgroundColour(buffer.contents[y-selectrect.y1+1][x-selectrect.x1+1])
+ term.write(" ")
+ end
+ end
+ end
+ end
+
+ --This draws the "selection" box
+ local add = nil
+ if buffer then
+ term.setBackgroundColour(colours.lightGrey)
+ else
+ term.setBackgroundColour(colours.grey)
+ end
+ for i=selectrect.x1, selectrect.x2 do
+ add = (i + selectrect.y1 + rectblink) % 2 == 0
+ term.setCursorPos(i-sx,selectrect.y1-sy)
+ if add then term.write(" ") end
+ add = (i + selectrect.y2 + rectblink) % 2 == 0
+ term.setCursorPos(i-sx,selectrect.y2-sy)
+ if add then term.write(" ") end
+ end
+ for i=selectrect.y1 + 1, selectrect.y2 - 1 do
+ add = (i + selectrect.x1 + rectblink) % 2 == 0
+ term.setCursorPos(selectrect.x1-sx,i-sy)
+ if add then term.write(" ") end
+ add = (i + selectrect.x2 + rectblink) % 2 == 0
+ term.setCursorPos(selectrect.x2-sx,i-sy)
+ if add then term.write(" ") end
+ end
+ end
+end
+
+--[[Draws the colour picker on the right side of the screen, the colour pallette and the footer with any
+ messages currently being displayed
+ Params: none
+ Returns:nil
+]]--
+local function drawInterface()
+ --Picker
+ local coffset,ioffset = 0,0
+ local maxcsize = h-2
+ if h < #column + 2 then
+ maxcsize = h-4
+ coffset = columnoffset
+ ioffset = 1
+ term.setBackgroundColour(colours.lightGrey)
+ term.setTextColour(colours.grey)
+ term.setCursorPos(w-1,1)
+ term.write("^^")
+ term.setCursorPos(w-1,h-2)
+ term.write("VV")
+ end
+ for i=1,math.min(#column+1,maxcsize) do
+ term.setCursorPos(w-1, i + ioffset)
+ local ci = i+coffset
+ if ci == #column+1 then
+ term.setBackgroundColour(colours.black)
+ term.setTextColour(colours.red)
+ term.write("XX")
+ elseif state == "print" then
+ term.setBackgroundColour(column[ci])
+ if column[ci] == colours.black then
+ term.setTextColour(colours.white)
+ else term.setTextColour(colours.black) end
+
+ if requirementsDisplayed then
+ if requiredMaterials[i] < 10 then term.write(" ") end
+ term.setCursorPos(w-#tostring(requiredMaterials[i])+1, i)
+ term.write(requiredMaterials[i])
+ else
+ if i+coffset < 10 then term.write(" ") end
+ term.write(i+coffset)
+ end
+ else
+ term.setBackgroundColour(column[ci])
+ term.write(" ")
+ end
+ end
+ --Filling the whitespace with... 'greyspace' *shudder*
+ if h > #column+3 then
+ term.setTextColour(colours.grey)
+ term.setBackgroundColour(colours.lightGrey)
+ for y=#column+2,h-2 do
+ term.setCursorPos(w-1,y)
+ term.write("| ")
+ end
+ end
+ --Pallette
+ term.setCursorPos(w-1,h-1)
+ if not lSel then
+ term.setBackgroundColour(colours.black)
+ term.setTextColour(colours.red)
+ term.write("X")
+ else
+ term.setBackgroundColour(lSel)
+ term.setTextColour(lSel)
+ term.write(" ")
+ end
+ if not rSel then
+ term.setBackgroundColour(colours.black)
+ term.setTextColour(colours.red)
+ term.write("X")
+ else
+ term.setBackgroundColour(rSel)
+ term.setTextColour(rSel)
+ term.write(" ")
+ end
+ --Footer
+ if inMenu then return end
+
+ term.setCursorPos(1, h)
+ term.setBackgroundColour(colours.lightGrey)
+ term.setTextColour(colours.grey)
+ term.clearLine()
+ if mainAvailable then
+ if inDropDown then
+ term.write(string.rep(" ", #ddModes.name + 2))
+ else
+ term.setBackgroundColour(colours.grey)
+ term.setTextColour(colours.lightGrey)
+ term.write(ddModes.name.." ")
+ end
+ end
+ term.setBackgroundColour(colours.lightGrey)
+ term.setTextColour(colours.grey)
+ term.write(getStateMessage())
+
+ local coords="X:"..sx.." Y:"..sy
+ if animated then coords = coords.." Frame:"..sFrame.."/"..framecount.." " end
+ term.setCursorPos(w-#coords+1,h)
+ if state == "play" then term.setBackgroundColour(colours.lime)
+ elseif record then term.setBackgroundColour(colours.red) end
+ term.write(coords)
+
+ if animated then
+ term.setCursorPos(w-1,h)
+ term.setBackgroundColour(colours.grey)
+ term.setTextColour(colours.lightGrey)
+ term.write("<>")
+ end
+end
+
+--[[Runs an interface where users can select topics of help. Will return once the user quits the help screen.
+ Params: none
+ Returns:nil
+]]--
+local function drawHelpScreen()
+ local selectedHelp = nil
+ while true do
+ term.setBackgroundColour(colours.lightGrey)
+ term.clear()
+ if not selectedHelp then
+ term.setCursorPos(4, 1)
+ term.setTextColour(colours.brown)
+ term.write("Available modes (click for info):")
+ for i=1,#helpTopics do
+ term.setCursorPos(2, 2 + i)
+ term.setTextColour(colours.black)
+ term.write(helpTopics[i].name)
+ if helpTopics[i].key then
+ term.setTextColour(colours.red)
+ term.write(" ("..helpTopics[i].key..")")
+ end
+ end
+ term.setCursorPos(4,h)
+ term.setTextColour(colours.black)
+ term.write("Press any key to exit")
+ else
+ term.setCursorPos(4,1)
+ term.setTextColour(colours.brown)
+ term.write(helpTopics[selectedHelp].name)
+ if helpTopics[selectedHelp].key then
+ term.setTextColour(colours.red)
+ term.write(" ("..helpTopics[selectedHelp].key..")")
+ end
+ term.setCursorPos(1,3)
+ term.setTextColour(colours.black)
+ print(helpTopics[selectedHelp].message.."\n")
+ for i=1,#helpTopics[selectedHelp].controls do
+ term.setTextColour(colours.brown)
+ term.write(helpTopics[selectedHelp].controls[i][1].." ")
+ term.setTextColour(colours.black)
+ print(helpTopics[selectedHelp].controls[i][2])
+ end
+ end
+
+ local id,p1,p2,p3 = os.pullEvent()
+
+ if id == "timer" then updateTimer(p1)
+ elseif id == "key" then
+ if selectedHelp then selectedHelp = nil
+ else break end
+ elseif id == "mouse_click" then
+ if not selectedHelp then
+ if p3 >=3 and p3 <= 2+#helpTopics then
+ selectedHelp = p3-2
+ else break end
+ else
+ selectedHelp = nil
+ end
+ end
+ end
+end
+
+--[[Draws a message in the footer bar. A helper for DrawInterface, but can be called for custom messages, if the
+ inMenu paramter is set to true while this is being done (remember to set it back when done!)
+ Params: message:string = The message to be drawn
+ Returns:nil
+]]--
+local function drawMessage(message)
+ term.setCursorPos(1,h)
+ term.setBackgroundColour(colours.lightGrey)
+ term.setTextColour(colours.grey)
+ term.clearLine()
+ term.write(message)
+end
+
+--[[
+ Section: Generic Interfaces
+]]--
+
+
+--[[One of my generic text printing methods, printing a message at a specified position with width and offset.
+ No colour materials included.
+ Params: msg:string = The message to print off-center
+ height:number = The starting height of the message
+ width:number = The limit as to how many characters long each line may be
+ offset:number = The starting width offset of the message
+ Returns:number the number of lines used in printing the message
+]]--
+local function wprintOffCenter(msg, height, width, offset)
+ local inc = 0
+ local ops = 1
+ while #msg - ops > width do
+ local nextspace = 0
+ while string.find(msg, " ", ops + nextspace) and
+ string.find(msg, " ", ops + nextspace) - ops < width do
+ nextspace = string.find(msg, " ", nextspace + ops) + 1 - ops
+ end
+ local ox,oy = term.getCursorPos()
+ term.setCursorPos(width/2 - (nextspace)/2 + offset, height + inc)
+ inc = inc + 1
+ term.write(string.sub(msg, ops, nextspace + ops - 1))
+ ops = ops + nextspace
+ end
+ term.setCursorPos(width/2 - #string.sub(msg, ops)/2 + offset, height + inc)
+ term.write(string.sub(msg, ops))
+
+ return inc + 1
+end
+
+--[[Draws a message that must be clicked on or a key struck to be cleared. No options, so used for displaying
+ generic information.
+ Params: ctitle:string = The title of the confirm dialogue
+ msg:string = The message displayed in the dialogue
+ Returns:nil
+]]--
+local function displayConfirmDialogue(ctitle, msg)
+ local dialogoffset = 8
+ --We actually print twice- once to get the lines, second time to print proper. Easier this way.
+ local lines = wprintOffCenter(msg, 5, w - (dialogoffset+2) * 2, dialogoffset + 2)
+
+ term.setCursorPos(dialogoffset, 3)
+ term.setBackgroundColour(colours.grey)
+ term.setTextColour(colours.lightGrey)
+ term.write(string.rep(" ", w - dialogoffset * 2))
+ term.setCursorPos(dialogoffset + (w - dialogoffset * 2)/2 - #ctitle/2, 3)
+ term.write(ctitle)
+ term.setTextColour(colours.grey)
+ term.setBackgroundColour(colours.lightGrey)
+ term.setCursorPos(dialogoffset, 4)
+ term.write(string.rep(" ", w - dialogoffset * 2))
+ for i=5,5+lines do
+ term.setCursorPos(dialogoffset, i)
+ term.write(" "..string.rep(" ", w - (dialogoffset) * 2 - 2).." ")
+ end
+ wprintOffCenter(msg, 5, w - (dialogoffset+2) * 2, dialogoffset + 2)
+
+ --In the event of a message, the player hits anything to continue
+ while true do
+ local id,key = os.pullEvent()
+ if id == "timer" then updateTimer(key);
+ elseif id == "key" or id == "mouse_click" or id == "mouse_drag" then break end
+ end
+end
+
+--[[Produces a nice dropdown menu based on a table of strings. Depending on the position, this will auto-adjust the position
+ of the menu drawn, and allows nesting of menus and sub menus. Clicking anywhere outside the menu will cancel and return nothing
+ Params: x:int = the x position the menu should be displayed at
+ y:int = the y position the menu should be displayed at
+ options:table = the list of options available to the user, as strings or submenus (tables of strings, with a name parameter)
+ Returns:string the selected menu option.
+]]--
+local function displayDropDown(x, y, options)
+ inDropDown = true
+ --Figures out the dimensions of our thing
+ local longestX = #options.name
+ for i=1,#options do
+ local currVal = options[i]
+ if type(currVal) == "table" then currVal = currVal.name end
+
+ longestX = math.max(longestX, #currVal)
+ end
+ local xOffset = math.max(0, longestX - ((w-2) - x) + 1)
+ local yOffset = math.max(0, #options - ((h-1) - y))
+
+ local clickTimes = 0
+ local tid = nil
+ local selection = nil
+ while clickTimes < 2 do
+ drawCanvas()
+ drawInterface()
+
+ term.setCursorPos(x-xOffset,y-yOffset)
+ term.setBackgroundColour(colours.grey)
+ term.setTextColour(colours.lightGrey)
+ term.write(options.name..string.rep(" ", longestX-#options.name + 2))
+
+ for i=1,#options do
+ term.setCursorPos(x-xOffset, y-yOffset+i)
+ if i==selection and clickTimes % 2 == 0 then
+ term.setBackgroundColour(colours.grey)
+ term.setTextColour(colours.lightGrey)
+ else
+ term.setBackgroundColour(colours.lightGrey)
+ term.setTextColour(colours.grey)
+ end
+ local currVal = options[i]
+ if type(currVal) == "table" then
+ term.write(currVal.name..string.rep(" ", longestX-#currVal.name + 1))
+ term.setBackgroundColour(colours.grey)
+ term.setTextColour(colours.lightGrey)
+ term.write(">")
+ else
+ term.write(currVal..string.rep(" ", longestX-#currVal + 2))
+ end
+ end
+
+ local id, p1, p2, p3 = os.pullEvent()
+ if id == "timer" then
+ if p1 == tid then
+ clickTimes = clickTimes + 1
+ if clickTimes > 2 then
+ break
+ else
+ tid = os.startTimer(0.1)
+ end
+ else
+ updateTimer(p1)
+ drawCanvas()
+ drawInterface()
+ end
+ elseif id == "mouse_click" then
+ if p2 >=x-xOffset and p2 <= x-xOffset + longestX + 1 and p3 >= y-yOffset+1 and p3 <= y-yOffset+#options then
+ selection = p3-(y-yOffset)
+ tid = os.startTimer(0.1)
+ else
+ selection = ""
+ break
+ end
+ end
+ end
+
+ if type(selection) == "number" then
+ selection = options[selection]
+ end
+
+ if type(selection) == "string" then
+ inDropDown = false
+ return selection
+ elseif type(selection) == "table" then
+ return displayDropDown(x, y, selection)
+ end
+end
+
+--[[A custom io.read() function with a few differences- it limits the number of characters being printed,
+ waits a 1/100th of a second so any keys still in the event library are removed before input is read and
+ the timer for the selectionrectangle is continuously updated during the process.
+ Params: lim:int = the number of characters input is allowed
+ Returns:string the inputted string, trimmed of leading and tailing whitespace
+]]--
+local function readInput(lim)
+ term.setCursorBlink(true)
+
+ local inputString = ""
+ if not lim or type(lim) ~= "number" or lim < 1 then lim = w - ox end
+ local ox,oy = term.getCursorPos()
+ --We only get input from the footer, so this is safe. Change if recycling
+ term.setBackgroundColour(colours.lightGrey)
+ term.setTextColour(colours.grey)
+ term.write(string.rep(" ", lim))
+ term.setCursorPos(ox, oy)
+ --As events queue immediately, we may get an unwanted key... this will solve that problem
+ local inputTimer = os.startTimer(0.01)
+ local keysAllowed = false
+
+ while true do
+ local id,key = os.pullEvent()
+
+ if keysAllowed then
+ if id == "key" and key == 14 and #inputString > 0 then
+ inputString = string.sub(inputString, 1, #inputString-1)
+ term.setCursorPos(ox + #inputString,oy)
+ term.write(" ")
+ elseif id == "key" and key == 28 and inputString ~= string.rep(" ", #inputString) then
+ break
+ elseif id == "key" and key == keys.leftCtrl then
+ return ""
+ elseif id == "char" and #inputString < lim then
+ inputString = inputString..key
+ end
+ end
+
+ if id == "timer" then
+ if key == inputTimer then
+ keysAllowed = true
+ else
+ updateTimer(key)
+ drawCanvas()
+ drawInterface()
+ term.setBackgroundColour(colours.lightGrey)
+ term.setTextColour(colours.grey)
+ end
+ end
+ term.setCursorPos(ox,oy)
+ term.write(inputString)
+ term.setCursorPos(ox + #inputString, oy)
+ end
+
+ while string.sub(inputString, 1, 1) == " " do
+ inputString = string.sub(inputString, 2, #inputString)
+ end
+ while string.sub(inputString, #inputString, #inputString) == " " do
+ inputString = string.sub(inputString, 1, #inputString-1)
+ end
+ term.setCursorBlink(false)
+
+ return inputString
+end
+
+--[[
+ Section: Image tools
+]]--
+
+
+--[[Copies all pixels beneath the selection rectangle into the image buffer. Empty buffers are converted to nil.
+ Params: removeImage:bool = true if the image is to be erased after copying, false otherwise
+ Returns:nil
+]]--
+local function copyToBuffer(removeImage)
+ buffer = { width = selectrect.x2 - selectrect.x1 + 1, height = selectrect.y2 - selectrect.y1 + 1, contents = { } }
+
+ local containsSomething = false
+ for y=1,buffer.height do
+ buffer.contents[y] = { }
+ local f,l = sFrame,sFrame
+ if record then f,l = 1, framecount end
+
+ for fra = f,l do
+ if frames[fra][selectrect.y1 + y - 1] then
+ for x=1,buffer.width do
+ buffer.contents[y][x] = frames[sFrame][selectrect.y1 + y - 1][selectrect.x1 + x - 1]
+ if removeImage then frames[fra][selectrect.y1 + y - 1][selectrect.x1 + x - 1] = nil end
+ if buffer.contents[y][x] then containsSomething = true end
+ end
+ end
+ end
+ end
+ --I don't classify an empty buffer as a real buffer- confusing to the user.
+ if not containsSomething then buffer = nil end
+end
+
+--[[Replaces all pixels under the selection rectangle with the image buffer (or what can be seen of it). Record-dependent.
+ Params: removeBuffer:bool = true if the buffer is to be emptied after copying, false otherwise
+ Returns:nil
+]]--
+local function copyFromBuffer(removeBuffer)
+ if not buffer then return end
+
+ for y = 1, math.min(buffer.height,selectrect.y2-selectrect.y1+1) do
+ local f,l = sFrame, sFrame
+ if record then f,l = 1, framecount end
+
+ for fra = f,l do
+ if not frames[fra][selectrect.y1+y-1] then frames[fra][selectrect.y1+y-1] = { } end
+ for x = 1, math.min(buffer.width,selectrect.x2-selectrect.x1+1) do
+ frames[fra][selectrect.y1+y-1][selectrect.x1+x-1] = buffer.contents[y][x]
+ end
+ end
+ end
+
+ if removeBuffer then buffer = nil end
+end
+
+--[[Moves the entire image (or entire animation) to the specified coordinates. Record-dependent.
+ Params: newx:int = the X coordinate to move the image to
+ newy:int = the Y coordinate to move the image to
+ Returns:nil
+]]--
+local function moveImage(newx,newy)
+ if not leflim or not toplim then return end
+ if newx <=0 or newy <=0 then return end
+ local f,l = sFrame,sFrame
+ if record then f,l = 1,framecount end
+
+ for i=f,l do
+ local newlines = { }
+ for y=toplim,botlim do
+ local line = frames[i][y]
+ if line then
+ newlines[y-toplim+newy] = { }
+ for x,char in pairs(line) do
+ newlines[y-toplim+newy][x-leflim+newx] = char
+ end
+ end
+ end
+ --Exceptions that allow us to move the text as well
+ if textEnabled then
+ newlines.text = { }
+ for y=toplim,botlim do
+ local line = frames[i].text[y]
+ if line then
+ newlines.text[y-toplim+newy] = { }
+ for x,char in pairs(line) do
+ newlines.text[y-toplim+newy][x-leflim+newx] = char
+ end
+ end
+ end
+
+ newlines.textcol = { }
+ for y=toplim,botlim do
+ local line = frames[i].textcol[y]
+ if line then
+ newlines.textcol[y-toplim+newy] = { }
+ for x,char in pairs(line) do
+ newlines.textcol[y-toplim+newy][x-leflim+newx] = char
+ end
+ end
+ end
+ end
+
+ frames[i] = newlines
+ end
+end
+
+--[[Prompts the user to clear the current frame or all frames. Record-dependent.,
+ Params: none
+ Returns:nil
+]]--
+local function clearImage()
+ inMenu = true
+ if not animated then
+ drawMessage("Clear image? Y/N: ")
+ elseif record then
+ drawMessage("Clear ALL frames? Y/N: ")
+ else
+ drawMessage("Clear current frame? Y/N :")
+ end
+ if string.find(string.upper(readInput(1)), "Y") then
+ local f,l = sFrame,sFrame
+ if record then f,l = 1,framecount end
+
+ for i=f,l do
+ frames[i] = { }
+ end
+ end
+ inMenu = false
+end
+
+--[[A recursively called method (watch out for big calls!) in which every pixel of a set colour is
+ changed to another colour. Does not work on the nil colour, for obvious reasons.
+ Params: x:int = The X coordinate of the colour to flood-fill
+ y:int = The Y coordinate of the colour to flood-fill
+ targetColour:colour = the colour that is being flood-filled
+ newColour:colour = the colour with which to replace the target colour
+ Returns:nil
+]]--
+local function floodFill(x, y, targetColour, newColour)
+ if not newColour or not targetColour then return end
+ local nodeList = { }
+
+ table.insert(nodeList, {x = x, y = y})
+
+ while #nodeList > 0 do
+ local node = nodeList[1]
+ if frames[sFrame][node.y] and frames[sFrame][node.y][node.x] == targetColour then
+ frames[sFrame][node.y][node.x] = newColour
+ table.insert(nodeList, { x = node.x + 1, y = node.y})
+ table.insert(nodeList, { x = node.x, y = node.y + 1})
+ if x > 1 then table.insert(nodeList, { x = node.x - 1, y = node.y}) end
+ if y > 1 then table.insert(nodeList, { x = node.x, y = node.y - 1}) end
+ end
+ table.remove(nodeList, 1)
+ end
+end
+
+--[[
+ Section: Animation Tools
+]]--
+
+--[[Enters play mode, allowing the animation to play through. Interface is restricted to allow this,
+ and method only leaves once the player leaves play mode.
+ Params: none
+ Returns:nil
+]]--
+local function playAnimation()
+ state = "play"
+ selectedrect = nil
+
+ local animt = os.startTimer(animtime)
+ repeat
+ drawCanvas()
+ drawInterface()
+
+ local id,key,_,y = os.pullEvent()
+
+ if id=="timer" then
+ if key == animt then
+ animt = os.startTimer(animtime)
+ sFrame = (sFrame % framecount) + 1
+ else
+ updateTimer(key)
+ end
+ elseif id=="key" then
+ if key == keys.comma and animtime > 0.1 then animtime = animtime - 0.05
+ elseif key == keys.period and animtime < 0.5 then animtime = animtime + 0.05
+ elseif key == keys.space then state = "paint" end
+ elseif id=="mouse_click" and y == h then
+ state = "paint"
+ end
+ until state ~= "play"
+ os.startTimer(0.5)
+end
+
+--[[Changes the selected frame (sFrame) to the chosen frame. If this frame is above the framecount,
+ additional frames are created with a copy of the image on the selected frame.
+ Params: newframe:int = the new frame to move to
+ Returns:nil
+]]--
+local function changeFrame(newframe)
+ inMenu = true
+ if not tonumber(newframe) then
+ term.setCursorPos(1,h)
+ term.setBackgroundColour(colours.lightGrey)
+ term.setTextColour(colours.grey)
+ term.clearLine()
+
+ term.write("Go to frame: ")
+ newframe = tonumber(readInput(2))
+ if not newframe or newframe <= 0 then
+ inMenu = false
+ return
+ end
+ elseif newframe <= 0 then return end
+
+ if newframe > framecount then
+ for i=framecount+1,newframe do
+ frames[i] = {}
+ for y,line in pairs(frames[sFrame]) do
+ frames[i][y] = { }
+ for x,v in pairs(line) do
+ frames[i][y][x] = v
+ end
+ end
+ end
+ framecount = newframe
+ end
+ sFrame = newframe
+ inMenu = false
+end
+
+--[[Removes every frame leading after the frame passed in
+ Params: frame:int the non-inclusive lower bounds of the delete
+ Returns:nil
+]]--
+local function removeFramesAfter(frame)
+ inMenu = true
+ if frame==framecount then return end
+ drawMessage("Remove frames "..(frame+1).."/"..framecount.."? Y/N :")
+ local answer = string.upper(readInput(1))
+
+ if string.find(answer, string.upper("Y")) ~= 1 then
+ inMenu = false
+ return
+ end
+
+ for i=frame+1, framecount do
+ frames[i] = nil
+ end
+ framecount = frame
+ inMenu = false
+end
+
+--[[
+ Section: Printing Tools
+]]--
+
+--[[Constructs a new facing to the left of the current facing
+ Params: curx:number = The facing on the X axis
+ curz:number = The facing on the Z axis
+ hand:string = The hand of the axis ("right" or "left")
+ Returns:number,number = the new facing on the X and Z axis after a left turn
+]]--
+local function getLeft(curx, curz)
+ local hand = "left"
+ if layering == "up" then hand = "right" end
+
+ if hand == "right" then
+ if curx == 1 then return 0,-1 end
+ if curx == -1 then return 0,1 end
+ if curz == 1 then return 1,0 end
+ if curz == -1 then return -1,0 end
+ else
+ if curx == 1 then return 0,1 end
+ if curx == -1 then return 0,-1 end
+ if curz == 1 then return -1,0 end
+ if curz == -1 then return 1,0 end
+ end
+end
+
+--[[Constructs a new facing to the right of the current facing
+ Params: curx:number = The facing on the X axis
+ curz:number = The facing on the Z axis
+ hand:string = The hand of the axis ("right" or "left")
+ Returns:number,number = the new facing on the X and Z axis after a right turn
+]]--
+local function getRight(curx, curz)
+ local hand = "left"
+ if layering == "up" then hand = "right" end
+
+ if hand == "right" then
+ if curx == 1 then return 0,1 end
+ if curx == -1 then return 0,-1 end
+ if curz == 1 then return -1,0 end
+ if curz == -1 then return 1,0 end
+ else
+ if curx == 1 then return 0,-1 end
+ if curx == -1 then return 0,1 end
+ if curz == 1 then return 1,0 end
+ if curz == -1 then return -1,0 end
+ end
+end
+
+
+--[[Sends out a rednet signal requesting local printers, and will listen for any responses. Printers found are added to the
+ printerList (for ID's) and printerNames (for names)
+ Params: nil
+ Returns:nil
+]]--
+local function locatePrinters()
+ printerList = { }
+ printerNames = { name = "Printers" }
+ local oldState = state
+ state = "Locating printers, please wait... "
+ drawCanvas()
+ drawInterface()
+ state = oldState
+
+ local modemOpened = false
+ for k,v in pairs(rs.getSides()) do
+ if peripheral.isPresent(v) and peripheral.getType(v) == "modem" then
+ rednet.open(v)
+ modemOpened = true
+ break
+ end
+ end
+
+ if not modemOpened then
+ displayConfirmDialogue("Modem not found!", "No modem peripheral. Must have network modem to locate printers.")
+ return false
+ end
+
+ rednet.broadcast("$3DPRINT IDENTIFY")
+
+ while true do
+ local id, msg = rsTimeReceive(1)
+
+ if not id then break end
+ if string.find(msg, "$3DPRINT IDACK") == 1 then
+ msg = string.gsub(msg, "$3DPRINT IDACK ", "")
+ table.insert(printerList, id)
+ table.insert(printerNames, msg)
+ end
+ end
+
+ if #printerList == 0 then
+ displayConfirmDialogue("Printers not found!", "No active printers found in proximity of this computer.")
+ return false
+ else
+ return true
+ end
+end
+
+--[[Sends a request to the printer. Waits on a response and updates the state of the application accordingly.
+ Params: command:string the command to send
+ param:string a parameter to send, if any
+ Returns:nil
+]]--
+local function sendPC(command,param)
+ local msg = "$PC "..command
+ if param then msg = msg.." "..param end
+ rednet.send(printerList[selectedPrinter], msg)
+
+ while true do
+ local id,key = rsTimeReceive()
+ if id == printerList[selectedPrinter] then
+ if key == "$3DPRINT ACK" then
+ break
+ elseif key == "$3DPRINT DEP" then
+ displayConfirmDialogue("Printer Empty", "The printer has exhasted a material. Please refill slot "..param..
+ ", and click this message when ready to continue.")
+ rednet.send(printerList[selectedPrinter], msg)
+ elseif key == "$3DPRINT OOF" then
+ displayConfirmDialogue("Printer Out of Fuel", "The printer has no fuel. Please replace the material "..
+ "in slot 1 with a fuel source, then click this message.")
+ rednet.send(printerList[selectedPrinter], "$PC SS 1")
+ id,key = rsTimeReceive()
+ rednet.send(printerList[selectedPrinter], "$PC RF")
+ id,key = rsTimeReceive()
+ rednet.send(printerList[selectedPrinter], msg)
+ end
+ end
+ end
+
+ --Changes to position are handled after the event has been successfully completed
+ if command == "FW" then
+ px = px + pfx
+ pz = pz + pfz
+ elseif command == "BK" then
+ px = px - pfx
+ pz = pz - pfz
+ elseif command == "UP" then
+ if layering == "up" then
+ py = py + 1
+ else
+ py = py - 1
+ end
+ elseif command == "DW" then
+ if layering == "up" then
+ py = py - 1
+ else
+ py = py + 1
+ end
+ elseif command == "TL" then
+ pfx,pfz = getLeft(pfx,pfz)
+ elseif command == "TR" then
+ pfx,pfz = getRight(pfx,pfz)
+ elseif command == "TU" then
+ pfx = -pfx
+ pfz = -pfz
+ end
+
+ drawCanvas()
+ drawInterface()
+end
+
+--[[A printing function that commands the printer to turn to face the desired direction, if it is not already doing so
+ Params: desx:number = the normalized x direction to face
+ desz:number = the normalized z direction to face
+ Returns:nil
+]]--
+local function turnToFace(desx,desz)
+ if desx ~= 0 then
+ if pfx ~= desx then
+ local temppfx,_ = getLeft(pfx,pfz)
+ if temppfx == desx then
+ sendPC("TL")
+ elseif temppfx == -desx then
+ sendPC("TR")
+ else
+ sendPC("TU")
+ end
+ end
+ else
+ print("on the z axis")
+ if pfz ~= desz then
+ local _,temppfz = getLeft(pfx,pfz)
+ if temppfz == desz then
+ sendPC("TL")
+ elseif temppfz == -desz then
+ sendPC("TR")
+ else
+ sendPC("TU")
+ end
+ end
+ end
+end
+
+--[[Performs the print
+ Params: nil
+ Returns:nil
+]]--
+local function performPrint()
+ state = "active print"
+ if layering == "up" then
+ --An up layering starts our builder bot on the bottom left corner of our build
+ px,py,pz = leflim, 0, botlim + 1
+ pfx,pfz = 0,-1
+
+ --We move him forward and up a bit from his original position.
+ sendPC("FW")
+ sendPC("UP")
+ --For each layer that needs to be completed, we go up by one each time
+ for layers=1,#frames do
+ --We first decide if we're going forwards or back, depending on what side we're on
+ local rowbot,rowtop,rowinc = nil,nil,nil
+ if pz == botlim then
+ rowbot,rowtop,rowinc = botlim,toplim,-1
+ else
+ rowbot,rowtop,rowinc = toplim,botlim,1
+ end
+
+ for rows = rowbot,rowtop,rowinc do
+ --Then we decide if we're going left or right, depending on what side we're on
+ local linebot,linetop,lineinc = nil,nil,nil
+ if px == leflim then
+ --Facing from the left side has to be easterly- it's changed here
+ turnToFace(1,0)
+ linebot,linetop,lineinc = leflim,riglim,1
+ else
+ --Facing from the right side has to be westerly- it's changed here
+ turnToFace(-1,0)
+ linebot,linetop,lineinc = riglim,leflim,-1
+ end
+
+ for lines = linebot,linetop,lineinc do
+ --We move our turtle forward, placing the right material at each step
+ local material = frames[py][pz][px]
+ if material then
+ material = math.log10(frames[py][pz][px])/math.log10(2) + 1
+ sendPC("SS", material)
+ sendPC("PD")
+ end
+ if lines ~= linetop then
+ sendPC("FW")
+ end
+ end
+
+ --The printer then has to do a U-turn, depending on which way he's facing and
+ --which way he needs to go
+ local temppfx,temppfz = getLeft(pfx,pfz)
+ if temppfz == rowinc and rows ~= rowtop then
+ sendPC("TL")
+ sendPC("FW")
+ sendPC("TL")
+ elseif temppfz == -rowinc and rows ~= rowtop then
+ sendPC("TR")
+ sendPC("FW")
+ sendPC("TR")
+ end
+ end
+ --Now at the end of a run he does a 180 and moves up to begin the next part of the print
+ sendPC("TU")
+ if layers ~= #frames then
+ sendPC("UP")
+ end
+ end
+ --All done- now we head back to where we started.
+ if px ~= leflim then
+ turnToFace(-1,0)
+ while px ~= leflim do
+ sendPC("FW")
+ end
+ end
+ if pz ~= botlim then
+ turnToFace(0,-1)
+ while pz ~= botlim do
+ sendPC("BK")
+ end
+ end
+ turnToFace(0,-1)
+ sendPC("BK")
+ while py > 0 do
+ sendPC("DW")
+ end
+ else
+ --The front facing is at the top-left corner, facing south not north
+ px,py,pz = leflim, botlim, 1
+ pfx,pfz = 0,1
+ --We move the printer to the last layer- he prints from the back forwards
+ while pz < #frames do
+ sendPC("FW")
+ end
+
+ --For each layer in the frame we build our wall, the move back
+ for layers = 1,#frames do
+ --We first decide if we're going left or right based on our position
+ local rowbot,rowtop,rowinc = nil,nil,nil
+ if px == leflim then
+ rowbot,rowtop,rowinc = leflim,riglim,1
+ else
+ rowbot,rowtop,rowinc = riglim,leflim,-1
+ end
+
+ for rows = rowbot,rowtop,rowinc do
+ --Then we decide if we're going up or down, depending on our given altitude
+ local linebot,linetop,lineinc = nil,nil,nil
+ if py == botlim then
+ linebot,linetop,lineinc = botlim,toplim,-1
+ else
+ linebot,linetop,lineinc = toplim,botlim,1
+ end
+
+ for lines = linebot,linetop,lineinc do
+ --We move our turtle up/down, placing the right material at each step
+ local material = frames[pz][py][px]
+ if material then
+ material = math.log10(frames[pz][py][px])/math.log10(2) + 1
+ sendPC("SS", material)
+ sendPC("PF")
+ end
+ if lines ~= linetop then
+ if lineinc == 1 then sendPC("DW")
+ else sendPC("UP") end
+ end
+ end
+
+ if rows ~= rowtop then
+ turnToFace(rowinc,0)
+ sendPC("FW")
+ turnToFace(0,1)
+ end
+ end
+
+ if layers ~= #frames then
+ sendPC("TU")
+ sendPC("FW")
+ sendPC("TU")
+ end
+ end
+ --He's easy to reset
+ while px ~= leflim do
+ turnToFace(-1,0)
+ sendPC("FW")
+ end
+ turnToFace(0,1)
+ end
+
+ sendPC("DE")
+
+ displayConfirmDialogue("Print complete", "The 3D print was successful.")
+end
+
+--[[
+ Section: Interface
+]]--
+
+--[[Runs the printing interface. Allows users to find/select a printer, the style of printing to perform and to begin the operation
+ Params: none
+ Returns:boolean true if printing was started, false otherwse
+]]--
+local function runPrintInterface()
+ calculateMaterials()
+ --There's nothing on canvas yet!
+ if not botlim then
+ displayConfirmDialogue("Cannot Print Empty Canvas", "There is nothing on canvas that "..
+ "can be printed, and the operation cannot be completed.")
+ return false
+ end
+ --No printers nearby
+ if not locatePrinters() then
+ return false
+ end
+
+ layering = "up"
+ requirementsDisplayed = false
+ selectedPrinter = 1
+ while true do
+ drawCanvas()
+ term.setBackgroundColour(colours.lightGrey)
+ for i=1,10 do
+ term.setCursorPos(1,i)
+ term.clearLine()
+ end
+ drawInterface()
+ term.setBackgroundColour(colours.lightGrey)
+ term.setTextColour(colours.black)
+
+ local msg = "3D Printing"
+ term.setCursorPos(w/2-#msg/2 - 2, 1)
+ term.write(msg)
+ term.setBackgroundColour(colours.grey)
+ term.setTextColour(colours.lightGrey)
+ if(requirementsDisplayed) then
+ msg = "Count:"
+ else
+ msg = " Slot:"
+ end
+ term.setCursorPos(w-3-#msg, 1)
+ term.write(msg)
+ term.setBackgroundColour(colours.lightGrey)
+ term.setTextColour(colours.black)
+
+ term.setCursorPos(7, 2)
+ term.write("Layering")
+ drawPictureTable(layerUpIcon, 3, 3, colours.white)
+ drawPictureTable(layerForwardIcon, 12, 3, colours.white)
+ if layering == "up" then
+ term.setBackgroundColour(colours.red)
+ else
+ term.setBackgroundColour(colours.lightGrey)
+ end
+ term.setCursorPos(3, 9)
+ term.write("Upwards")
+ if layering == "forward" then
+ term.setBackgroundColour(colours.red)
+ else
+ term.setBackgroundColour(colours.lightGrey)
+ end
+ term.setCursorPos(12, 9)
+ term.write("Forward")
+
+ term.setBackgroundColour(colours.lightGrey)
+ term.setTextColour(colours.black)
+ term.setCursorPos(31, 2)
+ term.write("Printer ID")
+ term.setCursorPos(33, 3)
+ if #printerList > 1 then
+ term.setBackgroundColour(colours.grey)
+ term.setTextColour(colours.lightGrey)
+ else
+ term.setTextColour(colours.red)
+ end
+ term.write(" "..printerNames[selectedPrinter].." ")
+
+ term.setBackgroundColour(colours.grey)
+ term.setTextColour(colours.lightGrey)
+ term.setCursorPos(25, 10)
+ term.write(" Cancel ")
+ term.setCursorPos(40, 10)
+ term.write(" Print ")
+
+ local id, p1, p2, p3 = os.pullEvent()
+
+ if id == "timer" then
+ updateTimer(p1)
+ elseif id == "mouse_click" then
+ --Layering Buttons
+ if p2 >= 3 and p2 <= 9 and p3 >= 3 and p3 <= 9 then
+ layering = "up"
+ elseif p2 >= 12 and p2 <= 18 and p3 >= 3 and p3 <= 9 then
+ layering = "forward"
+ --Count/Slot
+ elseif p2 >= w - #msg - 3 and p2 <= w - 3 and p3 == 1 then
+ requirementsDisplayed = not requirementsDisplayed
+ --Printer ID
+ elseif p2 >= 33 and p2 <= 33 + #printerNames[selectedPrinter] and p3 == 3 and #printerList > 1 then
+ local chosenName = displayDropDown(33, 3, printerNames)
+ for i=1,#printerNames do
+ if printerNames[i] == chosenName then
+ selectedPrinter = i
+ break;
+ end
+ end
+ --Print and Cancel
+ elseif p2 >= 25 and p2 <= 32 and p3 == 10 then
+ break
+ elseif p2 >= 40 and p2 <= 46 and p3 == 10 then
+ rednet.send(printerList[selectedPrinter], "$3DPRINT ACTIVATE")
+ ready = false
+ while true do
+ local id,msg = rsTimeReceive(10)
+
+ if id == printerList[selectedPrinter] and msg == "$3DPRINT ACTACK" then
+ ready = true
+ break
+ end
+ end
+ if ready then
+ performPrint()
+ break
+ else
+ displayConfirmDialogue("Printer Didn't Respond", "The printer didn't respond to the activation command. Check to see if it's online")
+ end
+ end
+ end
+ end
+ state = "paint"
+end
+
+--[[Performs a legacy save. When the dropdown menu is unavailable, it requests the user to save
+ or exit using keyboard shortcuts rather than selecting a menu option from the dropdown.
+ Pressing the control key again will cancel the save operation.
+ Params: none
+ Returns:string = the selection made
+]]--
+local function performLegacySaveExit()
+ local saveMsg = "(S)ave/(E)xit?"
+ if w < #saveMsg then saveMsg = "S/E?" end
+
+ term.setCursorPos(1,h)
+ term.setBackgroundColour(colours.lightGrey)
+ term.setTextColour(colours.grey)
+ term.clearLine()
+ term.write(saveMsg)
+
+ while true do
+ local id,val = os.pullEvent()
+ if id == "timer" then updateTimer(val)
+ elseif id == "key" then
+ if val == keys.s then return "save"
+ elseif val == keys.e then
+ --Get rid of the extra event
+ os.pullEvent("char")
+ return "exit"
+ elseif val == keys.leftCtrl then return nil
+ end
+ end
+ end
+end
+
+--[[This function changes the current paint program to another tool or mode, depending on user input. Handles
+ any necessary changes in logic involved in that.
+ Params: mode:string = the name of the mode to change to
+ Returns:nil
+]]--
+local function performSelection(mode)
+ if not mode or mode == "" then return
+
+ elseif mode == "help" and helpAvailable then
+ drawHelpScreen()
+
+ elseif mode == "blueprint on" then
+ blueprint = true
+ for i=1,#ddModes[2] do if ddModes[2][i] == "blueprint on" then
+ ddModes[2][i] = "blueprint off"
+ end end
+
+ elseif mode == "blueprint off" then
+ blueprint = false
+ for i=1,#ddModes[2] do if ddModes[2][i] == "blueprint off" then
+ ddModes[2][i] = "blueprint on"
+ end end
+
+ elseif mode == "layers on" then
+ layerDisplay = true
+ for i=1,#ddModes[2] do if ddModes[2][i] == "layers on" then
+ ddModes[2][i] = "layers off"
+ end end
+
+ elseif mode == "layers off" then
+ layerDisplay = false
+ for i=1,#ddModes[2] do if ddModes[2][i] == "layers off" then
+ ddModes[2][i] = "layers on"
+ end end
+
+ elseif mode == "direction on" then
+ printDirection = true
+ for i=1,#ddModes[2] do if ddModes[2][i] == "direction on" then
+ ddModes[2][i] = "direction off"
+ end end
+
+ elseif mode == "direction off" then
+ printDirection = false
+ for i=1,#ddModes[2] do if ddModes[2][i] == "direction off" then
+ ddModes[2][i] = "direction on"
+ end end
+
+ elseif mode == "hide interface" then
+ interfaceHidden = true
+
+ elseif mode == "show interface" then
+ interfaceHidden = false
+
+ elseif mode == "go to" then
+ changeFrame()
+
+ elseif mode == "remove" then
+ removeFramesAfter(sFrame)
+
+ elseif mode == "play" then
+ playAnimation()
+
+ elseif mode == "copy" then
+ if selectrect and selectrect.x1 ~= selectrect.x2 then
+ copyToBuffer(false)
+ end
+
+ elseif mode == "cut" then
+ if selectrect and selectrect.x1 ~= selectrect.x2 then
+ copyToBuffer(true)
+ end
+
+ elseif mode == "paste" then
+ if selectrect and selectrect.x1 ~= selectrect.x2 then
+ copyFromBuffer(false)
+ end
+
+ elseif mode == "hide" then
+ selectrect = nil
+ if state == "select" then state = "corner select" end
+
+ elseif mode == "alpha to left" then
+ if lSel then alphaC = lSel end
+
+ elseif mode == "alpha to right" then
+ if rSel then alphaC = rSel end
+
+ elseif mode == "record" then
+ record = not record
+
+ elseif mode == "clear" then
+ if state=="select" then buffer = nil
+ else clearImage() end
+
+ elseif mode == "select" then
+ if state=="corner select" or state=="select" then
+ state = "paint"
+ elseif selectrect and selectrect.x1 ~= selectrect.x2 then
+ state = "select"
+ else
+ state = "corner select"
+ end
+
+ elseif mode == "print" then
+ state = "print"
+ runPrintInterface()
+ state = "paint"
+
+ elseif mode == "save" then
+ if animated then saveNFA(sPath)
+ elseif textEnabled then saveNFT(sPath)
+ else saveNFP(sPath) end
+
+ elseif mode == "exit" then
+ isRunning = false
+
+ elseif mode ~= state then state = mode
+ else state = "paint"
+ end
+end
+
+--[[The main function of the program, reads and handles all events and updates them accordingly. Mode changes,
+ painting to the canvas and general selections are done here.
+ Params: none
+ Returns:nil
+]]--
+local function handleEvents()
+ recttimer = os.startTimer(0.5)
+ while isRunning do
+ drawCanvas()
+ if not interfaceHidden then drawInterface() end
+
+ if state == "text" then
+ term.setCursorPos(textCurX - sx, textCurY - sy)
+ term.setCursorBlink(true)
+ end
+
+ local id,p1,p2,p3 = os.pullEvent()
+ term.setCursorBlink(false)
+ if id=="timer" then
+ updateTimer(p1)
+ elseif (id=="mouse_click" or id=="mouse_drag") and not interfaceHidden then
+ if p2 >=w-1 and p3 < #column+1 then
+ local off = 0
+ local cansel = true
+ if h < #column + 2 then
+ if p3 == 1 then
+ if columnoffset > 0 then columnoffset = columnoffset-1 end
+ cansel = false
+ elseif p3 == h-2 then
+ if columnoffset < #column-(h-4)+1 then columnoffset = columnoffset+1 end
+ cansel = false
+ else
+ off = columnoffset - 1
+ end
+ end
+ --This rather handily accounts for the nil case (p3+off=#column+1)
+ if p1==1 and cansel then lSel = column[p3+off]
+ elseif p1==2 and cansel then rSel = column[p3+off] end
+ elseif p2 >=w-1 and p3==#column+1 then
+ if p1==1 then lSel = nil
+ else rSel = nil end
+ elseif p2==w-1 and p3==h and animated then
+ changeFrame(sFrame-1)
+ elseif p2==w and p3==h and animated then
+ changeFrame(sFrame+1)
+ elseif p2 <= #ddModes.name + 2 and p3==h and mainAvailable then
+ local sel = displayDropDown(1, h-1, ddModes)
+ performSelection(sel)
+ elseif p2 < w-1 and p3 <= h-1 then
+ if state=="pippette" then
+ if p1==1 then
+ if frames[sFrame][p3+sy] and frames[sFrame][p3+sy][p2+sx] then
+ lSel = frames[sFrame][p3+sy][p2+sx]
+ end
+ elseif p1==2 then
+ if frames[sFrame][p3+sy] and frames[sFrame][p3+sy][p2+sx] then
+ rSel = frames[sFrame][p3+sy][p2+sx]
+ end
+ end
+ elseif state=="move" then
+ updateImageLims(record)
+ moveImage(p2,p3)
+ elseif state=="flood" then
+ if p1 == 1 and lSel and frames[sFrame][p3+sy] then
+ floodFill(p2,p3,frames[sFrame][p3+sy][p2+sx],lSel)
+ elseif p1 == 2 and rSel and frames[sFrame][p3+sy] then
+ floodFill(p2,p3,frames[sFrame][p3+sy][p2+sx],rSel)
+ end
+ elseif state=="corner select" then
+ if not selectrect then
+ selectrect = { x1=p2+sx, x2=p2+sx, y1=p3+sy, y2=p3+sy }
+ elseif selectrect.x1 ~= p2+sx and selectrect.y1 ~= p3+sy then
+ if p2+sx<selectrect.x1 then selectrect.x1 = p2+sx
+ else selectrect.x2 = p2+sx end
+
+ if p3+sy<selectrect.y1 then selectrect.y1 = p3+sy
+ else selectrect.y2 = p3+sy end
+
+ state = "select"
+ end
+ elseif state=="textpaint" then
+ local paintCol = lSel
+ if p1 == 2 then paintCol = rSel end
+ if frames[sFrame].textcol[p3+sy] then
+ frames[sFrame].textcol[p3+sy][p2+sx] = paintCol
+ end
+ elseif state=="text" then
+ textCurX = p2 + sx
+ textCurY = p3 + sy
+ elseif state=="select" then
+ if p1 == 1 then
+ local swidth = selectrect.x2 - selectrect.x1
+ local sheight = selectrect.y2 - selectrect.y1
+
+ selectrect.x1 = p2 + sx
+ selectrect.y1 = p3 + sy
+ selectrect.x2 = p2 + swidth + sx
+ selectrect.y2 = p3 + sheight + sy
+ elseif p1 == 2 and p2 < w-2 and p3 < h-1 and boxdropAvailable then
+ inMenu = true
+ local sel = displayDropDown(p2, p3, srModes)
+ inMenu = false
+ performSelection(sel)
+ end
+ else
+ local f,l = sFrame,sFrame
+ if record then f,l = 1,framecount end
+ local bwidth = 0
+ if state == "brush" then bwidth = brushsize-1 end
+
+ for i=f,l do
+ for x = math.max(1,p2+sx-bwidth),p2+sx+bwidth do
+ for y = math.max(1,p3+sy-bwidth), p3+sy+bwidth do
+ if math.abs(x - (p2+sx)) + math.abs(y - (p3+sy)) <= bwidth then
+ if not frames[i][y] then frames[i][y] = {} end
+ if p1==1 then frames[i][y][x] = lSel
+ else frames[i][y][x] = rSel end
+
+ if textEnabled then
+ if not frames[i].text[y] then frames[i].text[y] = { } end
+ if not frames[i].textcol[y] then frames[i].textcol[y] = { } end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ elseif id=="char" then
+ if state=="text" then
+ if not frames[sFrame][textCurY] then frames[sFrame][textCurY] = { } end
+ if not frames[sFrame].text[textCurY] then frames[sFrame].text[textCurY] = { } end
+ if not frames[sFrame].textcol[textCurY] then frames[sFrame].textcol[textCurY] = { } end
+
+ if rSel then frames[sFrame][textCurY][textCurX] = rSel end
+ if lSel then
+ frames[sFrame].text[textCurY][textCurX] = p1
+ frames[sFrame].textcol[textCurY][textCurX] = lSel
+ else
+ frames[sFrame].text[textCurY][textCurX] = " "
+ frames[sFrame].textcol[textCurY][textCurX] = rSel
+ end
+
+ textCurX = textCurX+1
+ if textCurX > w + sx - 2 then sx = textCurX - w + 2 end
+ elseif tonumber(p1) then
+ if state=="brush" and tonumber(p1) > 1 then
+ brushsize = tonumber(p1)
+ elseif animated and tonumber(p1) > 0 then
+ changeFrame(tonumber(p1))
+ end
+ end
+ elseif id=="key" then
+ --All standard interface methods are locked when the interface is hidden
+ if interfaceHidden then
+ if p1==keys.grave then
+ performSelection("show interface")
+ end
+ --Text needs special handlers (all other keyboard shortcuts are of course reserved for typing)
+ elseif state=="text" then
+ if p1==keys.backspace and textCurX > 1 then
+ textCurX = textCurX-1
+ if frames[sFrame].text[textCurY] then
+ frames[sFrame].text[textCurY][textCurX] = nil
+ frames[sFrame].textcol[textCurY][textCurX] = nil
+ end
+ if textCurX < sx then sx = textCurX end
+ elseif p1==keys.left and textCurX > 1 then
+ textCurX = textCurX-1
+ if textCurX-1 < sx then sx = textCurX-1 end
+ elseif p1==keys.right then
+ textCurX = textCurX+1
+ if textCurX > w + sx - 2 then sx = textCurX - w + 2 end
+ elseif p1==keys.up and textCurY > 1 then
+ textCurY = textCurY-1
+ if textCurY-1 < sy then sy = textCurY-1 end
+ elseif p1==keys.down then
+ textCurY = textCurY+1
+ if textCurY > h + sy - 1 then sy = textCurY - h + 1 end
+ end
+
+ elseif p1==keys.leftCtrl then
+ local sel = nil
+ if mainAvailable then
+ sel = displayDropDown(1, h-1, ddModes[#ddModes])
+ else sel = performLegacySaveExit() end
+ performSelection(sel)
+ elseif p1==keys.leftAlt then
+ local sel = displayDropDown(1, h-1, ddModes[1])
+ performSelection(sel)
+ elseif p1==keys.h then
+ performSelection("help")
+ elseif p1==keys.x then
+ performSelection("cut")
+ elseif p1==keys.c then
+ performSelection("copy")
+ elseif p1==keys.v then
+ performSelection("paste")
+ elseif p1==keys.z then
+ performSelection("clear")
+ elseif p1==keys.s then
+ performSelection("select")
+ elseif p1==keys.tab then
+ performSelection("hide")
+ elseif p1==keys.q then
+ performSelection("alpha to left")
+ elseif p1==keys.w then
+ performSelection("alpha to right")
+ elseif p1==keys.f then
+ performSelection("flood")
+ elseif p1==keys.b then
+ performSelection("brush")
+ elseif p1==keys.m then
+ performSelection("move")
+ elseif p1==keys.backslash and animated then
+ performSelection("record")
+ elseif p1==keys.p then
+ performSelection("pippette")
+ elseif p1==keys.g and animated then
+ performSelection("go to")
+ elseif p1==keys.grave then
+ performSelection("hide interface")
+ elseif p1==keys.period and animated then
+ changeFrame(sFrame+1)
+ elseif p1==keys.comma and animated then
+ changeFrame(sFrame-1)
+ elseif p1==keys.r and animated then
+ performSelection("remove")
+ elseif p1==keys.space and animated then
+ performSelection("play")
+ elseif p1==keys.t and textEnabled then
+ performSelection("text")
+ sleep(0.01)
+ elseif p1==keys.y and textEnabled then
+ performSelection("textpaint")
+ elseif p1==keys.left then
+ if state == "move" and toplim then
+ updateImageLims(record)
+ if toplim and leflim then
+ moveImage(leflim-1,toplim)
+ end
+ elseif state=="select" and selectrect.x1 > 1 then
+ selectrect.x1 = selectrect.x1-1
+ selectrect.x2 = selectrect.x2-1
+ elseif sx > 0 then sx=sx-1 end
+ elseif p1==keys.right then
+ if state == "move" then
+ updateImageLims(record)
+ if toplim and leflim then
+ moveImage(leflim+1,toplim)
+ end
+ elseif state=="select" then
+ selectrect.x1 = selectrect.x1+1
+ selectrect.x2 = selectrect.x2+1
+ else sx=sx+1 end
+ elseif p1==keys.up then
+ if state == "move" then
+ updateImageLims(record)
+ if toplim and leflim then
+ moveImage(leflim,toplim-1)
+ end
+ elseif state=="select" and selectrect.y1 > 1 then
+ selectrect.y1 = selectrect.y1-1
+ selectrect.y2 = selectrect.y2-1
+ elseif sy > 0 then sy=sy-1 end
+ elseif p1==keys.down then
+ if state == "move" then
+ updateImageLims(record)
+ if toplim and leflim then
+ moveImage(leflim,toplim+1)
+ end
+ elseif state=="select" then
+ selectrect.y1 = selectrect.y1+1
+ selectrect.y2 = selectrect.y2+1
+ else sy=sy+1 end
+ end
+ end
+ end
+end
+
+--[[
+ Section: Main
+]]--
+
+--The first thing done is deciding what features we actually have, given the screen size
+if w < 7 or h < 4 then
+ --NPaintPro simply doesn't work at certain configurations
+ shell.run("clear")
+ print("Screen too small")
+ os.pullEvent("key")
+ return
+end
+--And reduces the number of features in others.
+determineAvailableServices()
+
+--There is no b&w support for NPP.
+if not term.isColour() then
+ shell.run("clear")
+ print("NPaintPro\nBy NitrogenFingers\n\nNPaintPro can only be run on advanced "..
+ "computers. Please reinstall on an advanced computer.")
+ return
+end
+
+--Taken almost directly from edit (for consistency)
+local tArgs = {...}
+
+--Command line options can appear before the file path to specify the file format
+local ca = 1
+while ca <= #tArgs do
+ if tArgs[ca] == "-a" then animated = true
+ elseif tArgs[ca] == "-t" then textEnabled = true
+ elseif tArgs[ca] == "-d" then interfaceHidden = true
+ elseif string.sub(tArgs[ca], 1, 1) == "-" then
+ print("Unrecognized option: "..tArgs[ca])
+ return
+ else break end
+ ca = ca + 1
+end
+
+--Presently, animations and text files are not supported
+if animated and textEnabled then
+ print("No support for animated text files- cannot have both -a and -t")
+ return
+end
+
+--Filepaths must be added if the screen is too small
+if #tArgs < ca then
+ if not filemakerAvailable then
+ print("Usage: npaintpro [-a,-t,-d] <path>")
+ return
+ else
+ --Otherwise do the logo draw early, to determine the file.
+ drawLogo()
+ if not runFileMaker() then return end
+ end
+else
+ if not interfaceHidden then drawLogo() end
+ sPath = shell.resolve(tArgs[ca])
+end
+
+if fs.exists(sPath) then
+ if fs.isDir(sPath) then
+ print("Cannot edit a directory.")
+ return
+ elseif string.find(sPath, ".nfp") ~= #sPath-3 and string.find(sPath, ".nfa") ~= #sPath-3 and
+ string.find(sPath, ".nft") ~= #sPath-3 then
+ print("Can only edit .nfp, .nft and .nfa files:",string.find(sPath, ".nfp"),#sPath-3)
+ return
+ end
+
+ if string.find(sPath, ".nfa") == #sPath-3 then
+ animated = true
+ end
+
+ if string.find(sPath, ".nft") == #sPath-3 then
+ textEnabled = true
+ end
+
+ if string.find(sPath, ".nfp") == #sPath-3 and animated then
+ print("Convert to nfa? Y/N")
+ if string.find(string.lower(io.read()), "y") then
+ local nsPath = string.sub(sPath, 1, #sPath-1).."a"
+ fs.move(sPath, nsPath)
+ sPath = nsPath
+ else
+ animated = false
+ end
+ end
+
+ --Again this is possible, I just haven't done it. Maybe I will?
+ if textEnabled and (string.find(sPath, ".nfp") == #sPath-3 or string.find(sPath, ".nfa") == #sPath-3) then
+ print("Cannot convert to nft")
+ end
+else
+ if not animated and not textEnabled and string.find(sPath, ".nfp") ~= #sPath-3 then
+ sPath = sPath..".nfp"
+ elseif animated and string.find(sPath, ".nfa") ~= #sPath-3 then
+ sPath = sPath..".nfa"
+ elseif textEnabled and string.find(sPath, ".nft") ~= #sPath-3 then
+ sPath = sPath..".nft"
+ end
+end
+
+init()
+handleEvents()
+
+term.setBackgroundColour(colours.black)
+shell.run("clear")