--[[ 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 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] ") 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")