View difference between Paste ID: mEbWVaHE and 2c8kGVRc
SHOW: | | - or go back to the newest paste.
1
--[[
2
		NPaintPro
3
		By NitrogenFingers
4
]]--
5
6
--The screen size
7
local w,h = term.getSize()
8
--Whether or not the program is currently waiting on user input
9
local inMenu = false
10
--Whether or not a drop down menu is active
11
local inDropDown = false
12
--Whether or not animation tools are enabled (use -a to turn them on)
13
local animated = false
14
--Whether or not the text tools are enabled (use -t to turn them on)
15
local textual = false
16
--Whether or not "blueprint" display mode is on
17
local blueprint = false
18
--Whether or not the "layer" display is on
19
local layerDisplay = false
20
--Whether or not the "direction" display is on
21
local printDirection = false
22
--The tool/mode npaintpro is currently in. Default is "paint"
23
--For a list of modes, check out the help file
24
local state = "paint"
25
--Whether or not the program is presently running
26
local isRunning = true
27
--The rednet address of the 3D printer, if one has been attached
28
local printer = nil
29
30
--The list of every frame, containing every image in the picture/animation
31
--Note: nfp files always have the picture at frame 1
32
local frames = { }
33
--How many frames are currently in the given animation.
34
local frameCount = 1
35
--The Colour Picker column
36
local column = {}
37
--The currently selected left and right colours
38
local lSel,rSel = colours.white,nil
39
--The amount of scrolling on the X and Y axis
40
local sx,sy = 0,0
41
--The alpha channel colour
42
--Change this to change default canvas colour
43
local alphaC = colours.yellow
44
--The currently selected frame. Default is 1
45
local sFrame = 1
46
--The contents of the image buffer- contains contents, width and height
47
local buffer = nil
48
--The position, width and height of the selection rectangle
49
local selectrect = nil
50
51
--Whether or not text tools are enabled for this document
52
local textEnabled = false
53
--The X and Y positions of the text cursor
54
local textCurX, textCurY = 1,1
55
56
--The currently calculated required materials
57
local requiredMaterials = {}
58
--Whether or not required materials are being displayed in the pallette
59
local requirementsDisplayed = false
60
--A list of the rednet ID's all in-range printers located
61
local printerList = { }
62
--A list of the names of all in-range printers located. Same as the printerList in reference
63
local printerNames = { }
64
--The selected printer
65
local selectedPrinter = 1
66
--The X,Y,Z and facing of the printer
67
local px,py,pz,pfx,pfz = 0,0,0,0,0
68
--The form of layering used
69
local layering = "up"
70
71
--The animation state of the selection rectangle and image buffer 
72
local rectblink = 0
73
--The ID for the timer
74
local recttimer = nil
75
--The radius of the brush tool
76
local brushsize = 3
77
--Whether or not "record" mode is activated (animation mode only)
78
local record = false
79
--The time between each frame when in play mode (animation mode only)
80
local animtime = 0.3
81
82
--The current "cursor position" in text mode
83
local cursorTexX,cursorTexY = 1,1
84
85
--A list of hexidecimal conversions from numbers to hex digits
86
local hexnums = { [10] = "a", [11] = "b", [12] = "c", [13] = "d", [14] = "e" , [15] = "f" }
87
--The NPaintPro logo (divine, isn't it?)
88
local logo = {
89
"fcc              3   339";
90
" fcc          9333    33";
91
"  fcc        933 333  33";
92
"   fcc       933  33  33";
93
"    fcc      933   33 33";
94
"     c88     333   93333";
95
"     888     333    9333";
96
"      333 3  333     939";
97
}
98
--The Layer Up and Layer Forward printing icons
99
local layerUpIcon = {
100
	"0000000";
101
	"0088880";
102
	"0888870";
103
	"07777f0";
104
	"0ffff00";
105
	"0000000";
106
}
107
local layerForwardIcon = {
108
	"0000000";
109
	"000fff0";
110
	"00777f0";
111
	"0888700";
112
	"0888000";
113-
local ddModes = { { "paint", "brush", "pippette", "flood", "move", "clear", "select", name = "painting" }, { "alpha to left", "alpha to right", "blueprint on", name = "display" }, "help", { "print", "save", "exit", name = "file" }, name = "menu" }
113+
114
}
115
--The available menu options in the ctrl menu
116
local mChoices = {"Save","Exit"}
117
--The available modes from the dropdown menu- tables indicate submenus (include a name!)
118
local ddModes = { { "paint", "brush", "pippette", "flood", "move", "clear", "select", name = "painting" }, { "alpha to left", "alpha to right", name = "display" }, "help", { "print", "save", "exit", name = "file" }, name = "menu" }
119
--The available modes from the selection right-click menu
120
local srModes = { "cut", "copy", "paste", "clear", "hide", name = "selection" }
121
--The list of available help topics for each mode 127
122
local helpTopics = {
123
	[1] = {
124
		name = "Paint Mode",
125
		key = nil,
126
		animonly = false,
127
		textonly = false,
128
		message = "The default mode for NPaintPro, for painting pixels."
129
		.." Controls here that are not overridden will apply for all other modes. Leaving a mode by selecting that mode "
130
		.." again will always send the user back to paint mode.",
131
		controls = {
132
			{ "Arrow keys", "Scroll the canvas" },
133
			{ "Left Click", "Paint/select left colour" },
134
			{ "Right Click", "Paint/select right colour" },
135
			{ "Z Key", "Clear image on screen" },
136
			{ "Tab Key", "Hide selection rectangle if visible" },
137
			{ "Q Key", "Set alpha mask to left colour" },
138
			{ "W Key", "Set alpha mask to right colour" },
139
			{ "Number Keys", "Swich between frames 1-9" },
140
			{ "</> keys", "Move to the next/last frame" },
141
			{ "R Key", "Removes every frame after the current frame"}
142
		}
143
	},
144
	[2] = {
145
		name = "Brush Mode",
146
		key = "b",
147
		animonly = false,
148
		textonly = false,
149
		message = "Brush mode allows painting a circular area of variable diameter rather than a single pixel, working in "..
150
		"the exact same way as paint mode in all other regards.",
151
		controls = {
152
			{ "Left Click", "Paints a brush blob with the left colour" },
153
			{ "Right Click", "Paints a brush blob with the right colour" },
154
			{ "Number Keys", "Changes the radius of the brush blob from 2-9" }
155
		}
156
	},
157
	[3] = {
158
		name = "Pippette Mode",
159
		key = "p",
160
		animonly = false,
161
		textonly = false,
162
		message = "Pippette mode allows the user to click the canvas and set the colour clicked to the left or right "..
163
		"selected colour, for later painting.",
164
		controls = {
165
			{ "Left Click", "Sets clicked colour to the left selected colour" },
166
			{ "Right Click", "Sets clicked colour to the right selected colour" }
167
		}
168
	},
169
	[4] = {
170
		name = "Move Mode",
171
		key = "m",
172
		animonly = false,
173-
		name = "Flood Mode (NYI)",
173+
		textonly = false,
174
		message = "Mode mode allows the moving of the entire image on the screen. This is especially useful for justifying"..
175
		" the image to the top-left for animations or game assets.",
176
		controls = {
177
			{ "Left/Right Click", "Moves top-left corner of image to selected square" },
178
			{ "Arrow keys", "Moves image one pixel in any direction" }
179
		}
180
	},
181
	[5] = {
182
		name = "Flood Mode",
183
		key = "f",
184
		animonly = false,
185
		textonly = false,
186
		message = "Flood mode allows the changing of an area of a given colour to that of the selected colour. "..
187
		"The tool uses a flood4 algorithm and will not fill diagonally. Transparency cannot be flood filled.",
188
		controls = {
189
			{ "Left Click", "Flood fills selected area to left colour" },
190
			{ "Right Click", "Flood fills selected area to right colour" }
191
		}
192
	},
193
	[6] = {
194
		name = "Select Mode",
195
		key = "s",
196
		animonly = false,
197
		textonly = false,
198
		message = "Select mode allows the creation and use of the selection rectangle, to highlight specific areas on "..
199
		"the screen and perform operations on the selected area of the image. The selection rectangle can contain an "..
200
		"image on the clipboard- if it does, the image will flash inside the rectangle, and the rectangle edges will "..
201
		"be light grey instead of dark grey.",
202
		controls = {
203
			{ "C Key", "Copy: Moves selection into the clipboard" },
204
			{ "X Key", "Cut: Clears canvas under the rectangle, and moves it into the clipboard" },
205
			{ "V Key", "Paste: Copys clipboard to the canvas" },
206
			{ "Z Key", "Clears clipboard" },
207
			{ "Left Click", "Moves top-left corner of rectangle to selected pixel" },
208
			{ "Right Click", "Opens selection menu" },
209
			{ "Arrow Keys", "Moves rectangle one pixel in any direction" }
210
		}
211
	},
212
	[7] = {
213
		name = "Corner Select Mode",
214
		key = nil,
215
		animonly = false,
216
		textonly = false,
217
		message = "If a selection rectangle isn't visible, this mode is selected automatically. It allows the "..
218
		"defining of the corners of the rectangle- one the top-left and bottom-right corners have been defined, "..
219
		"NPaintPro switches to selection mode. Note rectangle must be at least 2 pixels wide and high.",
220
		controls = {
221
			{ "Left/Right Click", "Defines a corner of the selection rectangle" }
222
		}
223
	},
224
	[8] = {
225
		name = "Play Mode",
226
		key = "space",
227
		animonly = true,
228
		textonly = false,
229
		message = "Play mode will loop through each frame in your animation at a constant rate. Editing tools are "..
230
		"locked in this mode, and the coordinate display will turn green to indicate it is on.",
231
		controls = {
232
			{ "</> Keys", "Increases/Decreases speed of the animation" },
233
			{ "Space Bar", "Returns to paint mode" }
234
		}
235
	},
236
	[9] = {
237
		name = "Record Mode",
238
		key = "\\",
239
		animonly = true,
240
		textonly = false,
241
		message = "Record mode is not a true mode, but influences how other modes work. Changes made that modify the "..
242
		"canvas in record mode will affect ALL frames in the animation. The coordinates will turn red to indicate that "..
243
		"record mode is on.",
244
		controls = {
245
			{ "", "Affects:" },
246
			{ "- Paint Mode", "" },
247
			{ "- Brush Mode", "" },
248
			{ "- Cut and Paste in Select Mode", ""},
249
			{ "- Move Mode", ""}
250
		}
251
	},
252
	[10] = {
253
		name = "Help Mode",
254
		key = "h",
255
		animonly = false,
256
		textonly = false,
257
		message = "Displays this help screen. Clicking on options will display help on that topic. Clicking out of the screen"..
258
		" will leave this mode.",
259
		controls = {
260
			{ "Left/Right Click", "Displays a topic/Leaves the mode" }
261
		}
262
	},
263
	[11] = {
264
		name = "File Mode",
265
		key = nil,
266
		animonly = false,
267
		textonly = false,
268
		message = "Clicking on the mode display at the bottom of the screen will open the options menu. Here you can"..
269
		" activate all of the modes in the program with a simple mouse click. Pressing left control will open up the"..
270
		" file menu automatically.",
271
		controls = { 
272
			{ "leftCtrl", "Opens the file menu" },
273
			{ "leftAlt", "Opens the paint menu" }
274
		}
275
	},
276
	[12] = {
277
		name = "Text Mode",
278
		key = "t",
279
		animonly = false,
280
		textonly = true,
281
		message = "In this mode, the user is able to type letters onto the document for display. The left colour "..
282
		"pallette value determines what colour the text will be, and the right determines what colour the background "..
283
		"will be (set either to nil to keep the same colours as already there).",
284
		controls = {
285
			{ "Backspace", "Deletes the character on the previous line" },
286
			{ "Arrow Keys", "Moves the cursor in any direction" },
287
			{ "Left Click", "Moves the cursor to beneath the mouse cursor" }
288
		}
289
	},
290
	[13] = {
291
		name = "Textpaint Mode",
292
		key = "y",
293
		animonly = false,
294
		textonly = true,
295
		message = "Allows the user to paint any text on screen to the desired colour with the mouse. If affects the text colour"..
296
		" values rather than the background values, but operates identically to paint mode in all other regards.",
297
		controls = {
298
			{ "Left Click", "Paints the text with the left colour" },
299
			{ "Right Click", "Paints the text with the right colour" }
300
		}
301
	},
302
	[14] = {
303
		name = "About NPaintPro",
304
		keys = nil,
305
		animonly = false,
306
		textonly = false,
307
		message = "NPaintPro: The feature-bloated paint program for ComputerCraft by Nitrogen Fingers.",
308
		controls = {
309
			{ "Testers:", " "},
310
			{ " ", "Faubiguy"},
311
			{ " ", "TheOriginalBIT"}
312
		}
313
	}
314
}
315
--The "bounds" of the image- the first/last point on both axes where a pixel appears
316
local toplim,botlim,leflim,riglim = nil,nil,nil,nil
317
--The selected path
318
local sPath = nil
319
320-
			for x,_ in pairs(frames[locf][y]) do
320+
321-
				if frames[locf][y][x] ~= nil then
321+
322-
					if leflim == nil or x < leflim then leflim = x end
322+
323-
					if toplim == nil or y < toplim then toplim = y end
323+
324-
					if riglim == nil or x > riglim then riglim = x end
324+
325-
					if botlim == nil or y > botlim then botlim = y end
325+
326
	Returns:string A string conversion of the colour
327
]]--
328
local function getHexOf(colour)
329
	if not colour or not tonumber(colour) then 
330
		return " " 
331
	end
332
	local value = math.log(colour)/math.log(2)
333
	if value > 9 then 
334
		value = hexnums[value] 
335
	end
336
	return value
337
end
338
339
--[[Converts a hex digit into a colour value
340
	Params: hex:?string = the hex digit to be converted
341
	Returns:string A colour value corresponding to the hex, or nil if the character is invalid
342
]]--
343
local function getColourOf(hex)
344
	local value = tonumber(hex, 16)
345
	if not value then return nil end
346
	value = math.pow(2,value)
347
	return value
348
end
349
350
--[[Finds the biggest and smallest bounds of the image- the outside points beyond which pixels do not appear
351
	These values are assigned to the "lim" parameters for access by other methods
352
	Params: forAllFrames:bool = True if all frames should be used to find bounds, otherwise false or nil
353
	Returns:nil
354
]]--
355
local function updateImageLims(forAllFrames)
356
	local f,l = sFrame,sFrame
357
	if forAllFrames == true then f,l = 1,framecount end
358
	
359
	toplim,botlim,leflim,riglim = nil,nil,nil,nil
360
	for locf = f,l do
361
		for y,_ in pairs(frames[locf]) do
362
			if type(y) == "number" then
363
				for x,_ in pairs(frames[locf][y]) do
364
					if frames[locf][y][x] ~= nil then
365
						if leflim == nil or x < leflim then leflim = x end
366
						if toplim == nil or y < toplim then toplim = y end
367
						if riglim == nil or x > riglim then riglim = x end
368
						if botlim == nil or y > botlim then botlim = y end
369
					end
370
				end
371
			end
372
		end
373
	end
374
	
375
	--There is just... no easier way to do this. It's horrible, but necessary
376
	if textEnabled then
377
		for locf = f,l do
378
			for y,_ in pairs(frames[locf].text) do
379
				for x,_ in pairs(frames[locf].text[y]) do
380
					if frames[locf].text[y][x] ~= nil then
381
						if leflim == nil or x < leflim then leflim = x end
382
						if toplim == nil or y < toplim then toplim = y end
383
						if riglim == nil or x > riglim then riglim = x end
384
						if botlim == nil or y > botlim then botlim = y end
385
					end
386
				end
387
			end
388
			for y,_ in pairs(frames[locf].textcol) do
389
				for x,_ in pairs(frames[locf].textcol[y]) do
390
					if frames[locf].textcol[y][x] ~= nil then
391
						if leflim == nil or x < leflim then leflim = x end
392
						if toplim == nil or y < toplim then toplim = y end
393
						if riglim == nil or x > riglim then riglim = x end
394
						if botlim == nil or y > botlim then botlim = y end
395
					end
396
				end
397
			end
398
		end
399
	end
400
end
401
402
--[[Determines how much of each material is required for a print. Done each time printing is called.
403
	Params: none
404
	Returns:table A complete list of how much of each material is required.
405
]]--
406
function calculateMaterials()
407
	updateImageLims(animated)
408
	requiredMaterials = {}
409
	for i=1,16 do 
410
		requiredMaterials[i] = 0 
411
	end
412
	
413
	if not toplim then return end
414
	
415
	for i=1,#frames do
416
		for y = toplim, botlim do
417
			for x = leflim, riglim do
418
				if type(frames[i][y][x]) == "number" then
419
					requiredMaterials[math.log10(frames[i][y][x])/math.log10(2) + 1] = 
420
						requiredMaterials[math.log10(frames[i][y][x])/math.log10(2) + 1] + 1
421
				end	
422
			end
423
		end
424
	end
425
end
426
427
428
--[[Updates the rectangle blink timer. Should be called anywhere events are captured, along with a timer capture.
429
	Params: nil
430
	Returns:nil
431
]]--
432
local function updateTimer(id)
433
	if id == recttimer then
434
		recttimer = os.startTimer(0.5)
435
		rectblink = (rectblink % 2) + 1
436
	end
437
end
438
439
--[[Constructs a message based on the state currently selected
440
	Params: nil
441
	Returns:string A message regarding the state of the application
442
]]--
443
local function getStateMessage()
444
	local msg = " "..string.upper(string.sub(state, 1, 1))..string.sub(state, 2, #state).." mode"
445
	if state == "brush" then msg = msg..", size="..brushsize end
446
	return msg
447
end
448
449
--[[Calls the rednet_message event, but also looks for timer events to keep then
450
	system timer ticking.
451
	Params: timeout:number how long before the event times out
452
	Returns:number the id of the sender
453
		   :number the message send
454
]]--
455
local function rsTimeReceive(timeout)
456
	local timerID
457
	if timeout then timerID = os.startTimer(timeout) end
458
	
459
	local id,key,msg = nil,nil
460
	while true do
461
		id,key,msg = os.pullEvent()
462
		
463
		if id == "timer" then
464
			if key == timerID then return
465
			else updateTimer(key) end
466
		end
467
		if id == "rednet_message" then 
468
			return key,msg
469
		end
470
	end
471
end
472
473
--[[Draws a picture, in paint table format on the screen
474
	Params: image:table = the image to display
475
			xinit:number = the x position of the top-left corner of the image
476
			yinit:number = the y position of the top-left corner of the image
477
			alpha:number = the color to display for the alpha channel. Default is white.
478
	Returns:nil
479
]]--
480
local function drawPictureTable(image, xinit, yinit, alpha)
481
	if not alpha then alpha = 1 end
482
	for y=1,#image do
483
		for x=1,#image[y] do
484
			term.setCursorPos(xinit + x-1, yinit + y-1)
485
			local col = getColourOf(string.sub(image[y], x, x))
486
			if not col then col = alpha end
487
			term.setBackgroundColour(col)
488
			term.write(" ")
489
		end
490
	end
491
end
492
493
--[[  
494
			Section: Loading  
495
]]-- 
496
497
--[[Loads a non-animted paint file into the program
498
	Params: path:string = The path in which the file is located
499
	Returns:nil
500
]]--
501
local function loadNFP(path)
502
	sFrame = 1
503
	frames[sFrame] = { }
504
	if fs.exists(path) then
505
		local file = io.open(path, "r" )
506
		local sLine = file:read()
507
		local num = 1
508
		while sLine do
509
			table.insert(frames[sFrame], num, {})
510
			for i=1,#sLine do
511
				frames[sFrame][num][i] = getColourOf(string.sub(sLine,i,i))
512
			end
513
			num = num+1
514
			sLine = file:read()
515
		end
516
		file:close()
517
	end
518
end
519
520
--[[Loads a text-paint file into the program
521
	Params: path:string = The path in which the file is located
522
	Returns:nil
523
]]--
524
local function loadNFT(path)
525
	sFrame = 1
526
	frames[sFrame] = { }
527
	frames[sFrame].text = { }
528
	frames[sFrame].textcol = { }
529
	
530
	if fs.exists(path) then
531
		local file = io.open(path, "r")
532
		local sLine = file:read()
533
		local num = 1
534
		while sLine do
535
			table.insert(frames[sFrame], num, {})
536
			table.insert(frames[sFrame].text, num, {})
537
			table.insert(frames[sFrame].textcol, num, {})
538
			
539-
	if animated then 
539+
			--As we're no longer 1-1, we keep track of what index to write to
540
			local writeIndex = 1
541
			--Tells us if we've hit a 30 or 31 (BG and FG respectively)- next char specifies the curr colour
542
			local bgNext, fgNext = false, false
543-
		table.insert(ddModes[2], #ddModes, "layers on")
543+
			--The current background and foreground colours
544-
	else loadNFP(sPath) end
544+
			local currBG, currFG = nil,nil
545
			term.setCursorPos(1,1)
546
			for i=1,#sLine do
547
				local nextChar = string.sub(sLine, i, i)
548
				if nextChar:byte() == 30 then
549
					bgNext = true
550
				elseif nextChar:byte() == 31 then
551
					fgNext = true
552
				elseif bgNext then
553
					currBG = getColourOf(nextChar)
554
					bgNext = false
555
				elseif fgNext then
556
					currFG = getColourOf(nextChar)
557
					fgNext = false
558
				else
559
					if nextChar ~= " " and currFG == nil then
560
						currFG = colours.white
561
					end
562
					frames[sFrame][num][writeIndex] = currBG
563
					frames[sFrame].textcol[num][writeIndex] = currFG
564
					frames[sFrame].text[num][writeIndex] = nextChar
565
					writeIndex = writeIndex + 1
566
				end
567
			end
568
			num = num+1
569
			sLine = file:read()
570
		end
571
		file:close()
572
	end
573
end
574
575
--[[Loads an animated paint file into the program
576
	Params: path:string = The path in which the file is located
577
	Returns:nil
578
]]--
579
local function loadNFA(path)
580
	frames[sFrame] = { }
581
	if fs.exists(path) then
582
		local file = io.open(path, "r" )
583
		local sLine = file:read()
584
		local num = 1
585
		while sLine do
586
			table.insert(frames[sFrame], num, {})
587
			if sLine == "~" then
588
				sFrame = sFrame + 1
589
				frames[sFrame] = { }
590
				num = 1
591
			else
592
				for i=1,#sLine do
593
					frames[sFrame][num][i] = getColourOf(string.sub(sLine,i,i))
594
				end
595
				num = num+1
596
			end
597
			sLine = file:read()
598
		end
599
		file:close()
600
	end
601
	framecount = sFrame
602
	sFrame = 1
603
end
604
605
--[[Saves a non-animated paint file to the specified path
606
	Params: path:string = The path to save the file to
607
	Returns:nil
608
]]--
609
local function saveNFP(path)
610
	local sDir = string.sub(sPath, 1, #sPath - #fs.getName(sPath))
611
	if not fs.exists(sDir) then
612
		fs.makeDir(sDir)
613
	end
614
615
	local file = io.open(path, "w")
616
	updateImageLims(false)
617
	if not toplim then 
618
		file:close()
619
		return
620
	end
621
	for y=1,botlim do
622
		local line = ""
623
		if frames[sFrame][y] then 
624
			for x=1,riglim do
625
				line = line..getHexOf(frames[sFrame][y][x])
626
			end
627
		end
628-
						term.write(" ")
628+
629
	end
630
	file:close()
631
end
632
633
--[[Saves a text-paint file to the specified path
634
	Params: path:string = The path to save the file to
635
	Returns:nil
636
]]--
637
local function saveNFT(path)
638
	local sDir = string.sub(sPath, 1, #sPath - #fs.getName(sPath))
639
	if not fs.exists(sDir) then
640
		fs.makeDir(sDir)
641
	end
642
	
643
	local file = io.open(path, "w")
644
	updateImageLims(false)
645
	if not toplim then
646
		file:close()
647
		return
648
	end
649
	for y=1,botlim do
650
		local line = ""
651
		local currBG, currFG = nil,nil
652
		for x=1,riglim do
653
			if frames[sFrame][y][x] ~= currBG then
654
				line = line..string.char(30)..getHexOf(frames[sFrame][y][x])
655
				currBG = frames[sFrame][y][x]
656
			end
657
			if frames[sFrame].textcol[y][x] ~= currFG then
658
				line = line..string.char(31)..getHexOf(frames[sFrame].textcol[y][x])
659
				currFG = frames[sFrame].textcol[y][x]
660
			end
661
			local char = frames[sFrame].text[y][x]
662
			if not char then char = " " end
663
			line = line..frames[sFrame].text[y][x]
664
		end
665
		file:write(line.."\n")
666
	end
667
	file:close()
668
end
669
670
--[[Saves a animated paint file to the specified path
671
	Params: path:string = The path to save the file to
672
	Returns:nil
673
]]--
674
local function saveNFA(path)
675
	local sDir = string.sub(sPath, 1, #sPath - #fs.getName(sPath))
676
	if not fs.exists(sDir) then
677
		fs.makeDir(sDir)
678
	end
679
	
680
	local file = io.open(path, "w")
681
	updateImageLims(true)
682
	if not toplim then 
683
		file:close()
684
		return
685
	end
686
	for i=1,#frames do
687
		for y=1,botlim do
688
			local line = ""
689
			if frames[i][y] then 
690
				for x=1,riglim do
691
					line = line..getHexOf(frames[i][y][x])
692
				end
693
			end
694
			file:write(line.."\n")
695
		end
696
		if i < #frames then file:write("~\n") end
697
	end
698
	file:close()
699
end
700
701
702
--[[Initializes the program, by loading in the paint file. Called at the start of each program.
703
	Params: none
704
	Returns:nil
705
]]--
706
local function init()
707
	if textEnabled then
708
		loadNFT(sPath)
709
		table.insert(ddModes, 2, { "text", "textpaint", name = "text"})
710
	elseif animated then 
711
		loadNFA(sPath)
712
		table.insert(ddModes, #ddModes, { "record", "play", name = "anim" })
713
		table.insert(ddModes, #ddModes, { "go to", "remove", name = "frames"})
714
		table.insert(ddModes[2], #ddModes[2], "blueprint on")
715
		table.insert(ddModes[2], #ddModes[2], "layers on")
716
	else 
717
		loadNFP(sPath) 
718
		table.insert(ddModes[2], #ddModes[2], "blueprint on")
719
	end
720
721
	for i=0,15 do
722
		table.insert(column, math.pow(2,i))
723
	end
724
end
725
726
--[[  
727
			Section: Drawing  
728
]]--
729
730
731
--[[Draws the rather superflous logo. Takes about 1 second, before user is able to move to the
732
	actual program.
733
	Params: none
734
	Returns:nil
735
]]--
736
local function drawLogo()
737
	term.setBackgroundColour(colours.white)
738
	term.clear()
739
	drawPictureTable(logo, w/2 - #logo[1]/2, h/2 - #logo/2, colours.white)
740
	term.setBackgroundColour(colours.white)
741
	term.setTextColour(colours.black)
742
	local msg = "NPaintPro"
743
	term.setCursorPos(w/2 - #msg/2, h-3)
744
	term.write(msg)
745
	msg = "By NitrogenFingers"
746
	term.setCursorPos(w/2 - #msg/2, h-2)
747
	term.write(msg)
748
	
749
	os.pullEvent()
750
end
751
752
--[[Clears the display to the alpha channel colour, draws the canvas, the image buffer and the selection
753
	rectanlge if any of these things are present.
754
	Params: none
755
	Returns:nil
756
]]--
757
local function drawCanvas()
758
	--We have to readjust the position of the canvas if we're printing
759
	turtlechar = "@"
760
	if state == "active print" then
761
		if layering == "up" then
762
			if py >= 1 and py <= #frames then
763
				sFrame = py
764
			end
765
			if pz < sy then sy = pz
766
			elseif pz > sy + h - 1 then sy = pz + h - 1 end
767
			if px < sx then sx = px
768
			elseif px > sx + w - 2 then sx = px + w - 2 end
769
		else
770
			if pz >= 1 and pz <= #frames then
771
				sFrame = pz
772
			end
773
			
774
			if py < sy then sy = py
775
			elseif py > sy + h - 1 then sy = py + h - 1 end
776
			if px < sx then sx = px
777
			elseif px > sx + w - 2 then sx = px + w - 2 end
778
		end
779
		
780
		if pfx == 1 then turtlechar = ">"
781
		elseif pfx == -1 then turtlechar = "<"
782
		elseif pfz == 1 then turtlechar = "V"
783
		elseif pfz == -1 then turtlechar = "^"
784
		end
785
	end
786
787
	--Picture next
788
	local topLayer, botLayer
789
	if layerDisplay then
790
		topLayer = sFrame
791
		botLayer = 1
792
	else
793
		topLayer,botLayer = sFrame,sFrame
794
	end
795
	
796
	for currframe = botLayer,topLayer,1 do
797
		for y=sy+1,sy+h-1 do
798
			if frames[currframe][y] then 
799
				for x=sx+1,sx+w-2 do
800
					term.setCursorPos(x-sx,y-sy)
801
					if frames[currframe][y][x] then
802
						term.setBackgroundColour(frames[currframe][y][x])
803
						if textEnabled and frames[currframe].textcol[y][x] and frames[currframe].text[y][x] then
804
							term.setTextColour(frames[currframe].textcol[y][x])
805
							term.write(frames[currframe].text[y][x])
806
						else
807
							term.write(" ")
808
						end
809
					else 
810
						tileExists = false
811
						for i=currframe-1,botLayer,-1 do
812
							if frames[i][y][x] then
813
								tileExists = true
814
								break
815
							end
816
						end
817
						
818
						if not tileExists then
819
							if blueprint then
820
								term.setBackgroundColour(colours.blue)
821
								term.setTextColour(colours.white)
822
								if x == sx+1 and y % 4 == 1 then
823
									term.write(""..((y/4) % 10))
824
								elseif y == sy + 1 and x % 4 == 1 then
825
									term.write(""..((x/4) % 10))
826
								elseif x % 2 == 1 and y % 2 == 1 then
827
									term.write("+")
828
								elseif x % 2 == 1 then
829
									term.write("|")
830
								elseif y % 2 == 1 then
831
									term.write("-")
832
								else
833
									term.write(" ")
834
								end
835
							else
836
								term.setBackgroundColour(alphaC) 
837
								if textEnabled and frames[currframe].textcol[y][x] and frames[currframe].text[y][x] then
838
									term.setTextColour(frames[currframe].textcol[y][x])
839
									term.write(frames[currframe].text[y][x])
840
								else
841
									term.write(" ")
842
								end
843
							end
844
						end
845
					end
846
				end
847
			else
848
				for x=sx+1,sx+w-2 do
849
					term.setCursorPos(x-sx,y-sy)
850
					
851
					tileExists = false
852
					for i=currframe-1,botLayer,-1 do
853
						if frames[i][y] and frames[i][y][x] then
854
							tileExists = true
855
							break
856
						end
857
					end
858
					
859
					if not tileExists then
860
						if blueprint then
861
							term.setBackgroundColour(colours.blue)
862
							term.setTextColour(colours.white)
863
							if x == sx+1 and y % 4 == 1 then
864
								term.write(""..((y/4) % 10))
865
							elseif y == sy + 1 and x % 4 == 1 then
866
								term.write(""..((x/4) % 10))
867
							elseif x % 2 == 1 and y % 2 == 1 then
868
								term.write("+")
869
							elseif x % 2 == 1 then
870
								term.write("|")
871
							elseif y % 2 == 1 then
872
								term.write("-")
873
							else
874
								term.write(" ")
875
							end
876
						else
877
							term.setBackgroundColour(alphaC) 
878
							term.write(" ")
879
						end
880
					end
881
				end
882
			end
883
		end
884
	end
885
	
886
	--Then the printer, if he's on
887
	if state == "active print" then
888
		local bgColour = alphaC
889
		if layering == "up" then
890
			term.setCursorPos(px-sx,pz-sy)
891
			if frames[sFrame] and frames[sFrame][pz-sy] and frames[sFrame][pz-sy][px-sx] then
892
				bgColour = frames[sFrame][pz-sy][px-sx]
893
			elseif blueprint then bgColour = colours.blue end
894
		else
895
			term.setCursorPos(px-sx,py-sy)
896
			if frames[sFrame] and frames[sFrame][py-sy] and frames[sFrame][py-sy][px-sx] then
897
				bgColour = frames[sFrame][py-sy][px-sx]
898
			elseif blueprint then bgColour = colours.blue end
899
		end
900
		
901
		term.setBackgroundColour(bgColour)
902
		if bgColour == colours.black then term.setTextColour(colours.white)
903
		else term.setTextColour(colours.black) end
904
		
905
		term.write(turtlechar)
906
	end
907
	
908
	--Then the buffer
909
	if selectrect then
910
		if buffer and rectblink == 1 then
911
		for y=selectrect.y1, math.min(selectrect.y2, selectrect.y1 + buffer.height-1) do
912
			for x=selectrect.x1, math.min(selectrect.x2, selectrect.x1 + buffer.width-1) do
913
				if buffer.contents[y-selectrect.y1+1][x-selectrect.x1+1] then
914
					term.setCursorPos(x+sx,y+sy)
915
					term.setBackgroundColour(buffer.contents[y-selectrect.y1+1][x-selectrect.x1+1])
916
					term.write(" ")
917
				end
918
			end
919
		end
920
		end
921
	
922
		--This draws the "selection" box
923
		local add = nil
924
		if buffer then
925
			term.setBackgroundColour(colours.lightGrey)
926
		else 
927
			term.setBackgroundColour(colours.grey)
928
		end
929
		for i=selectrect.x1, selectrect.x2 do
930
			add = (i + selectrect.y1 + rectblink) % 2 == 0
931
			term.setCursorPos(i-sx,selectrect.y1-sy)
932
			if add then term.write(" ") end
933
			add = (i + selectrect.y2 + rectblink) % 2 == 0
934
			term.setCursorPos(i-sx,selectrect.y2-sy)
935
			if add then term.write(" ") end
936
		end
937
		for i=selectrect.y1 + 1, selectrect.y2 - 1 do
938
			add = (i + selectrect.x1 + rectblink) % 2 == 0
939
			term.setCursorPos(selectrect.x1-sx,i-sy)
940
			if add then term.write(" ") end
941
			add = (i + selectrect.x2 + rectblink) % 2 == 0
942
			term.setCursorPos(selectrect.x2-sx,i-sy)
943
			if add then term.write(" ") end
944
		end
945
	end
946
end
947
948
--[[Draws the colour picker on the right side of the screen, the colour pallette and the footer with any 
949
	messages currently being displayed
950
	Params: none
951
	Returns:nil
952
]]--
953
local function drawInterface()
954
	--Picker
955
	for i=1,#column do
956
		term.setCursorPos(w-1, i)
957
		term.setBackgroundColour(column[i])
958
		if state == "print" then
959
			if i == 16 then
960
				term.setTextColour(colours.white)
961
			else
962
				term.setTextColour(colours.black)
963
			end
964
			if requirementsDisplayed then
965
				if requiredMaterials[i] < 10 then term.write(" ") end
966
				term.setCursorPos(w-#tostring(requiredMaterials[i])+1, i)
967
				term.write(requiredMaterials[i])
968
			else
969
				if i < 10 then term.write(" ") end
970
				term.write(i)
971
			end
972
		else
973
			term.write("  ")
974
		end
975
	end
976
	term.setCursorPos(w-1,#column+1)
977
	term.setBackgroundColour(colours.black)
978
	term.setTextColour(colours.red)
979
	term.write("XX")
980
	--Pallette
981
	term.setCursorPos(w-1,h-1)
982
	if not lSel then
983
		term.setBackgroundColour(colours.black)
984
		term.setTextColour(colours.red)
985
		term.write("X")
986
	else
987
		term.setBackgroundColour(lSel)
988
		term.setTextColour(lSel)
989
		term.write(" ")
990
	end
991
	if not rSel then
992
		term.setBackgroundColour(colours.black)
993
		term.setTextColour(colours.red)
994
		term.write("X")
995
	else
996
		term.setBackgroundColour(rSel)
997
		term.setTextColour(rSel)
998
		term.write(" ")
999
	end
1000
	--Footer
1001
	if inMenu then return end
1002
	
1003
	term.setCursorPos(1, h)
1004
	term.setBackgroundColour(colours.lightGrey)
1005
	term.setTextColour(colours.grey)
1006
	term.clearLine()
1007
	if inDropDown then
1008
		term.write(string.rep(" ", 6))
1009
	else
1010
		term.setBackgroundColour(colours.grey)
1011
		term.setTextColour(colours.lightGrey)
1012
		term.write("menu  ")
1013
	end
1014
	term.setBackgroundColour(colours.lightGrey)
1015
	term.setTextColour(colours.grey)
1016
	term.write(getStateMessage())
1017
	
1018
	local coords="X:"..sx.." Y:"..sy
1019
	if animated then coords = coords.." Frame:"..sFrame.."/"..framecount.."   " end
1020
	term.setCursorPos(w-#coords+1,h)
1021
	if state == "play" then term.setBackgroundColour(colours.lime)
1022
	elseif record then term.setBackgroundColour(colours.red) end
1023
	term.write(coords)
1024
	
1025
	if animated then
1026
		term.setCursorPos(w-1,h)
1027
		term.setBackgroundColour(colours.grey)
1028
		term.setTextColour(colours.lightGrey)
1029
		term.write("<>")
1030
	end
1031
end
1032
1033
--[[Runs an interface where users can select topics of help. Will return once the user quits the help screen.
1034
	Params: none
1035
	Returns:nil
1036
]]--
1037
local function drawHelpScreen()
1038
	local selectedHelp = nil
1039
	while true do
1040
		term.setBackgroundColour(colours.lightGrey)
1041
		term.clear()
1042
		if not selectedHelp then
1043
			term.setCursorPos(4, 1)
1044
			term.setTextColour(colours.brown)
1045
			term.write("Available modes (click for info):")
1046
			for i=1,#helpTopics do
1047
				term.setCursorPos(2, 2 + i)
1048
				term.setTextColour(colours.black)
1049
				term.write(helpTopics[i].name)
1050
				if helpTopics[i].key then
1051
					term.setTextColour(colours.red)
1052
					term.write(" ("..helpTopics[i].key..")")
1053
				end
1054
			end
1055
			term.setCursorPos(4,h)
1056
			term.setTextColour(colours.black)
1057
			term.write("Press any key to exit")
1058
		else
1059
			term.setCursorPos(4,1)
1060
			term.setTextColour(colours.brown)
1061
			term.write(helpTopics[selectedHelp].name)
1062
			if helpTopics[selectedHelp].key then
1063
				term.setTextColour(colours.red)
1064
				term.write(" ("..helpTopics[selectedHelp].key..")")
1065
			end
1066
			term.setCursorPos(1,3)
1067
			term.setTextColour(colours.black)
1068
			print(helpTopics[selectedHelp].message.."\n")
1069
			for i=1,#helpTopics[selectedHelp].controls do
1070
				term.setTextColour(colours.brown)
1071
				term.write(helpTopics[selectedHelp].controls[i][1].." ")
1072
				term.setTextColour(colours.black)
1073
				print(helpTopics[selectedHelp].controls[i][2])
1074
			end
1075
		end
1076
		
1077
		local id,p1,p2,p3 = os.pullEvent()
1078
		
1079
		if id == "timer" then updateTimer(p1)
1080
		elseif id == "key" then 
1081
			if selectedHelp then selectedHelp = nil
1082
			else break end
1083
		elseif id == "mouse_click" then
1084
			if not selectedHelp then 
1085
				if p3 >=3 and p3 <= 2+#helpTopics then
1086
					selectedHelp = p3-2 
1087
				else break end
1088
			else
1089
				selectedHelp = nil
1090
			end
1091
		end
1092
	end
1093
end
1094
1095
--[[Draws a message in the footer bar. A helper for DrawInterface, but can be called for custom messages, if the
1096
	inMenu paramter is set to true while this is being done (remember to set it back when done!)
1097
	Params: message:string = The message to be drawn
1098
	Returns:nil
1099
]]--
1100
local function drawMessage(message)
1101
	term.setCursorPos(1,h)
1102
	term.setBackgroundColour(colours.lightGrey)
1103
	term.setTextColour(colours.grey)
1104
	term.clearLine()
1105
	term.write(message)
1106
end
1107
1108
--[[
1109
			Section: Generic Interfaces
1110
]]--
1111
1112
1113
--[[One of my generic text printing methods, printing a message at a specified position with width and offset.
1114
	No colour materials included.
1115
	Params: msg:string = The message to print off-center
1116
			height:number = The starting height of the message
1117
			width:number = The limit as to how many characters long each line may be
1118
			offset:number = The starting width offset of the message
1119
	Returns:number the number of lines used in printing the message
1120
]]--
1121
local function wprintOffCenter(msg, height, width, offset)
1122
	local inc = 0
1123
	local ops = 1
1124
	while #msg - ops > width do
1125
		local nextspace = 0
1126
		while string.find(msg, " ", ops + nextspace) and
1127
				string.find(msg, " ", ops + nextspace) - ops < width do
1128
			nextspace = string.find(msg, " ", nextspace + ops) + 1 - ops
1129
		end
1130
		local ox,oy = term.getCursorPos()
1131
		term.setCursorPos(width/2 - (nextspace)/2 + offset, height + inc)
1132
		inc = inc + 1
1133
		term.write(string.sub(msg, ops, nextspace + ops - 1))
1134
		ops = ops + nextspace
1135
	end
1136
	term.setCursorPos(width/2 - #string.sub(msg, ops)/2 + offset, height + inc)
1137
	term.write(string.sub(msg, ops))
1138
	
1139
	return inc + 1
1140
end
1141
1142
--[[Draws a message that must be clicked on or a key struck to be cleared. No options, so used for displaying
1143
	generic information.
1144
	Params: ctitle:string = The title of the confirm dialogue
1145
			msg:string = The message displayed in the dialogue
1146
	Returns:nil
1147
]]--
1148
local function displayConfirmDialogue(ctitle, msg)
1149
	local dialogoffset = 8
1150
	--We actually print twice- once to get the lines, second time to print proper. Easier this way.
1151
	local lines = wprintOffCenter(msg, 5, w - (dialogoffset+2) * 2, dialogoffset + 2)
1152
	
1153
	term.setCursorPos(dialogoffset, 3)
1154
	term.setBackgroundColour(colours.grey)
1155
	term.setTextColour(colours.lightGrey)
1156
	term.write(string.rep(" ", w - dialogoffset * 2))
1157
	term.setCursorPos(dialogoffset + (w - dialogoffset * 2)/2 - #ctitle/2, 3)
1158
	term.write(ctitle)
1159
	term.setTextColour(colours.grey)
1160
	term.setBackgroundColour(colours.lightGrey)
1161
	term.setCursorPos(dialogoffset, 4)
1162
	term.write(string.rep(" ", w - dialogoffset * 2))
1163
	for i=5,5+lines do
1164
		term.setCursorPos(dialogoffset, i) 
1165
		term.write(" "..string.rep(" ", w - (dialogoffset) * 2 - 2).." ")
1166
	end
1167
	wprintOffCenter(msg, 5, w - (dialogoffset+2) * 2, dialogoffset + 2)
1168
	
1169
	--In the event of a message, the player hits anything to continue
1170
	while true do
1171
		local id,key = os.pullEvent()
1172
		if id == "timer" then updateTimer(key);
1173
		elseif id == "key" or id == "mouse_click" or id == "mouse_drag" then break end
1174
	end
1175
end
1176
1177
--[[Produces a nice dropdown menu based on a table of strings. Depending on the position, this will auto-adjust the position
1178
	of the menu drawn, and allows nesting of menus and sub menus. Clicking anywhere outside the menu will cancel and return nothing
1179
	Params: x:int = the x position the menu should be displayed at
1180
			y:int = the y position the menu should be displayed at
1181
			options:table = the list of options available to the user, as strings or submenus (tables of strings, with a name parameter)
1182
	Returns:string the selected menu option.
1183
]]--
1184
local function displayDropDown(x, y, options)
1185
	inDropDown = true
1186
	--Figures out the dimensions of our thing
1187
	local longestX = #options.name
1188
	for i=1,#options do
1189
		local currVal = options[i]
1190
		if type(currVal) == "table" then currVal = currVal.name end
1191
		
1192
		longestX = math.max(longestX, #currVal)
1193
	end
1194
	local xOffset = math.max(0, longestX - ((w-2) - x) + 1)
1195
	local yOffset = math.max(0, #options - ((h-1) - y))
1196
	
1197
	local clickTimes = 0
1198
	local tid = nil
1199
	local selection = nil
1200
	while clickTimes < 2 do
1201
		drawCanvas()
1202
		drawInterface()
1203
		
1204
		term.setCursorPos(x-xOffset,y-yOffset)
1205
		term.setBackgroundColour(colours.grey)
1206
		term.setTextColour(colours.lightGrey)
1207
		term.write(options.name..string.rep(" ", longestX-#options.name + 2))
1208
	
1209
		for i=1,#options do
1210
			term.setCursorPos(x-xOffset, y-yOffset+i)
1211
			if i==selection and clickTimes % 2 == 0 then
1212
				term.setBackgroundColour(colours.grey)
1213
				term.setTextColour(colours.lightGrey)
1214
			else
1215
				term.setBackgroundColour(colours.lightGrey)
1216
				term.setTextColour(colours.grey)
1217
			end
1218
			local currVal = options[i]
1219
			if type(currVal) == "table" then 
1220
				term.write(currVal.name..string.rep(" ", longestX-#currVal.name + 1))
1221
				term.setBackgroundColour(colours.grey)
1222
				term.setTextColour(colours.lightGrey)
1223
				term.write(">")
1224
			else
1225
				term.write(currVal..string.rep(" ", longestX-#currVal + 2))
1226
			end
1227
		end
1228
		
1229
		local id, p1, p2, p3 = os.pullEvent()
1230
		if id == "timer" then
1231
			if p1 == tid then 
1232
				clickTimes = clickTimes + 1
1233
				if clickTimes > 2 then 
1234
					break
1235
				else 
1236
					tid = os.startTimer(0.1) 
1237
				end
1238
			else 
1239
				updateTimer(p1) 
1240
				drawCanvas()
1241
				drawInterface()
1242
			end
1243
		elseif id == "mouse_click" then
1244
			if p2 >=x-xOffset and p2 <= x-xOffset + longestX + 1 and p3 >= y-yOffset+1 and p3 <= y-yOffset+#options then
1245
				selection = p3-(y-yOffset)
1246
				tid = os.startTimer(0.1)
1247
			else
1248
				selection = ""
1249
				break
1250
			end
1251
		end
1252
	end
1253
	
1254
	if type(selection) == "number" then
1255
		selection = options[selection]
1256
	end
1257
	
1258
	if type(selection) == "string" then 
1259
		inDropDown = false
1260
		return selection
1261
	elseif type(selection) == "table" then 
1262
		return displayDropDown(x, y, selection)
1263
	end
1264
end
1265
1266
--[[A custom io.read() function with a few differences- it limits the number of characters being printed,
1267
	waits a 1/100th of a second so any keys still in the event library are removed before input is read and
1268
	the timer for the selectionrectangle is continuously updated during the process.
1269
	Params: lim:int = the number of characters input is allowed
1270
	Returns:string the inputted string, trimmed of leading and tailing whitespace
1271
]]--
1272
local function readInput(lim)
1273
	term.setCursorBlink(true)
1274
1275
	local inputString = ""
1276
	if not lim or type(lim) ~= "number" or lim < 1 then lim = w - ox end
1277
	local ox,oy = term.getCursorPos()
1278
	--We only get input from the footer, so this is safe. Change if recycling
1279
	term.setBackgroundColour(colours.lightGrey)
1280
	term.setTextColour(colours.grey)
1281
	term.write(string.rep(" ", lim))
1282
	term.setCursorPos(ox, oy)
1283
	--As events queue immediately, we may get an unwanted key... this will solve that problem
1284
	local inputTimer = os.startTimer(0.01)
1285
	local keysAllowed = false
1286
	
1287
	while true do
1288
		local id,key = os.pullEvent()
1289
		
1290
		if keysAllowed then
1291
			if id == "key" and key == 14 and #inputString > 0 then
1292
				inputString = string.sub(inputString, 1, #inputString-1)
1293
				term.setCursorPos(ox + #inputString,oy)
1294
				term.write(" ")
1295
			elseif id == "key" and key == 28 and inputString ~= string.rep(" ", #inputString) then 
1296
				break
1297
			elseif id == "key" and key == keys.leftCtrl then
1298
				return ""
1299
			elseif id == "char" and #inputString < lim then
1300
				inputString = inputString..key
1301
			end
1302
		end
1303
		
1304
		if id == "timer" then
1305
			if key == inputTimer then 
1306
				keysAllowed = true
1307
			else
1308
				updateTimer(key)
1309
				drawCanvas()
1310
				drawInterface()
1311
				term.setBackgroundColour(colours.lightGrey)
1312
				term.setTextColour(colours.grey)
1313
			end
1314
		end
1315
		term.setCursorPos(ox,oy)
1316
		term.write(inputString)
1317
		term.setCursorPos(ox + #inputString, oy)
1318
	end
1319
	
1320
	while string.sub(inputString, 1, 1) == " " do
1321
		inputString = string.sub(inputString, 2, #inputString)
1322
	end
1323
	while string.sub(inputString, #inputString, #inputString) == " " do
1324
		inputString = string.sub(inputString, 1, #inputString-1)
1325
	end
1326
	term.setCursorBlink(false)
1327
	
1328
	return inputString
1329
end
1330
1331
--[[  
1332
			Section: Image tools 
1333
]]--
1334
1335
1336
--[[Copies all pixels beneath the selection rectangle into the image buffer. Empty buffers are converted to nil.
1337
	Params: removeImage:bool = true if the image is to be erased after copying, false otherwise
1338
	Returns:nil
1339
]]--
1340
local function copyToBuffer(removeImage)
1341
	buffer = { width = selectrect.x2 - selectrect.x1 + 1, height = selectrect.y2 - selectrect.y1 + 1, contents = { } }
1342
	
1343
	local containsSomething = false
1344
	for y=1,buffer.height do
1345
		buffer.contents[y] = { }
1346
		local f,l = sFrame,sFrame
1347
		if record then f,l = 1, framecount end
1348
		
1349
		for fra = f,l do
1350
			if frames[fra][selectrect.y1 + y - 1] then
1351
				for x=1,buffer.width do
1352
					buffer.contents[y][x] = frames[sFrame][selectrect.y1 + y - 1][selectrect.x1 + x - 1]
1353
					if removeImage then frames[fra][selectrect.y1 + y - 1][selectrect.x1 + x - 1] = nil end
1354
					if buffer.contents[y][x] then containsSomething = true end
1355
				end
1356
			end
1357
		end
1358
	end
1359
	--I don't classify an empty buffer as a real buffer- confusing to the user.
1360
	if not containsSomething then buffer = nil end
1361
end
1362
1363
--[[Replaces all pixels under the selection rectangle with the image buffer (or what can be seen of it). Record-dependent.
1364
	Params: removeBuffer:bool = true if the buffer is to be emptied after copying, false otherwise
1365
	Returns:nil
1366
]]--
1367
local function copyFromBuffer(removeBuffer)
1368
	if not buffer then return end
1369
1370
	for y = 1, math.min(buffer.height,selectrect.y2-selectrect.y1+1) do
1371
		local f,l = sFrame, sFrame
1372
		if record then f,l = 1, framecount end
1373
		
1374
		for fra = f,l do
1375
			if not frames[fra][selectrect.y1+y-1] then frames[fra][selectrect.y1+y-1] = { } end
1376
			for x = 1, math.min(buffer.width,selectrect.x2-selectrect.x1+1) do
1377
				frames[fra][selectrect.y1+y-1][selectrect.x1+x-1] = buffer.contents[y][x]
1378
			end
1379
		end
1380
	end
1381
	
1382
	if removeBuffer then buffer = nil end
1383
end
1384
1385
--[[Moves the entire image (or entire animation) to the specified coordinates. Record-dependent.
1386
	Params: newx:int = the X coordinate to move the image to
1387
			newy:int = the Y coordinate to move the image to
1388
	Returns:nil
1389
]]--
1390
local function moveImage(newx,newy)
1391
	if not leflim or not toplim then return end
1392
	if newx <=0 or newy <=0 then return end
1393
	local f,l = sFrame,sFrame
1394
	if record then f,l = 1,framecount end
1395
	
1396
	for i=f,l do
1397
		local newlines = { }
1398
		for y,line in pairs(frames[i]) do
1399
			newlines[y-toplim+newy] = { }
1400
			for x,char in pairs(line) do
1401
				newlines[y-toplim+newy][x-leflim+newx] = char
1402
			end
1403
		end
1404
		frames[i] = newlines
1405
	end
1406
end
1407
1408
--[[Prompts the user to clear the current frame or all frames. Record-dependent.,
1409
	Params: none
1410
	Returns:nil
1411
]]--
1412
local function clearImage()
1413
	inMenu = true
1414
	if not animated then
1415
		drawMessage("Clear image? Y/N: ")
1416
	elseif record then
1417
		drawMessage("Clear ALL frames? Y/N: ")
1418
	else
1419
		drawMessage("Clear current frame? Y/N :")
1420
	end
1421
	if string.find(string.upper(readInput(1)), "Y") then
1422
		local f,l = sFrame,sFrame
1423
		if record then f,l = 1,framecount end
1424
		
1425
		for i=f,l do
1426
			frames[i] = { }
1427
		end
1428
	end
1429
	inMenu = false
1430
end
1431
1432
--[[A recursively called method (watch out for big calls!) in which every pixel of a set colour is
1433
	changed to another colour. Does not work on the nil colour, for obvious reasons.
1434
	Params: x:int = The X coordinate of the colour to flood-fill
1435
			y:int = The Y coordinate of the colour to flood-fill
1436
			targetColour:colour = the colour that is being flood-filled
1437
			newColour:colour = the colour with which to replace the target colour
1438
	Returns:nil
1439
]]--
1440
local function floodFill(x, y, targetColour, newColour)
1441
	if not newColour or not targetColour then return end
1442
	local nodeList = { }
1443
	
1444
	table.insert(nodeList, {x = x, y = y})
1445
	
1446
	while #nodeList > 0 do
1447
		local node = nodeList[1]
1448
		if frames[sFrame][node.y] and frames[sFrame][node.y][node.x] == targetColour then
1449
			frames[sFrame][node.y][node.x] = newColour
1450
			table.insert(nodeList, { x = node.x + 1, y = node.y})
1451
			table.insert(nodeList, { x = node.x, y = node.y + 1})
1452
			if x > 1 then table.insert(nodeList, { x = node.x - 1, y = node.y}) end
1453
			if y > 1 then table.insert(nodeList, { x = node.x, y = node.y - 1}) end
1454
		end
1455
		table.remove(nodeList, 1)
1456
	end
1457
end
1458
1459
--[[  
1460
			Section: Animation Tools  
1461
]]--
1462
1463
--[[Enters play mode, allowing the animation to play through. Interface is restricted to allow this,
1464
	and method only leaves once the player leaves play mode.
1465
	Params: none
1466
	Returns:nil
1467
]]--
1468
local function playAnimation()
1469
	state = "play"
1470
	selectedrect = nil
1471
	
1472
	local animt = os.startTimer(animtime)
1473
	repeat
1474
		drawCanvas()
1475
		drawInterface()
1476
		
1477
		local id,key,_,y = os.pullEvent()
1478
		
1479
		if id=="timer" then
1480
			if key == animt then
1481
				animt = os.startTimer(animtime)
1482
				sFrame = (sFrame % framecount) + 1
1483
			else
1484
				updateTimer(key)
1485
			end
1486
		elseif id=="key" then
1487
			if key == keys.comma and animtime > 0.1 then animtime = animtime - 0.05
1488
			elseif key == keys.period and animtime < 0.5 then animtime = animtime + 0.05
1489
			elseif key == keys.space then state = "paint" end
1490
		elseif id=="mouse_click" and y == h then
1491
			state = "paint"
1492
		end
1493
	until state ~= "play"
1494
	os.startTimer(0.5)
1495
end
1496
1497
--[[Changes the selected frame (sFrame) to the chosen frame. If this frame is above the framecount,
1498
	additional frames are created with a copy of the image on the selected frame.
1499
	Params: newframe:int = the new frame to move to
1500
	Returns:nil
1501
]]--
1502
local function changeFrame(newframe)
1503
	inMenu = true
1504
	if not tonumber(newframe) then
1505
		term.setCursorPos(1,h)
1506
		term.setBackgroundColour(colours.lightGrey)
1507
		term.setTextColour(colours.grey)
1508
		term.clearLine()
1509
	
1510
		term.write("Go to frame: ")
1511
		newframe = tonumber(readInput(2))
1512
		if not newframe or newframe <= 0 then
1513
			inMenu = false
1514
			return 
1515
		end
1516
	elseif newframe <= 0 then return end
1517
	
1518
	if newframe > framecount then
1519
		for i=framecount+1,newframe do
1520
			frames[i] = {}
1521
			for y,line in pairs(frames[sFrame]) do
1522
				frames[i][y] = { }
1523
				for x,v in pairs(line) do
1524
					frames[i][y][x] = v
1525
				end
1526
			end
1527
		end
1528
		framecount = newframe
1529
	end
1530
	sFrame = newframe
1531
	inMenu = false
1532
end
1533
1534
--[[Removes every frame leading after the frame passed in
1535
	Params: frame:int the non-inclusive lower bounds of the delete
1536
	Returns:nil
1537
]]--
1538
local function removeFramesAfter(frame)
1539
	inMenu = true
1540
	if frame==framecount then return end
1541
	drawMessage("Remove frames "..(frame+1).."/"..framecount.."? Y/N :")
1542
	local answer = string.upper(readInput(1))
1543
	
1544
	if string.find(answer, string.upper("Y")) ~= 1 then 
1545
		inMenu = false
1546
		return 
1547
	end
1548
	
1549
	for i=frame+1, framecount do
1550
		frames[i] = nil
1551
	end
1552
	framecount = frame
1553
	inMenu = false
1554
end
1555
1556
--[[
1557
			Section: Printing Tools
1558
]]--
1559
1560
--[[Constructs a new facing to the left of the current facing
1561
	Params: curx:number = The facing on the X axis
1562
			curz:number = The facing on the Z axis
1563
			hand:string = The hand of the axis ("right" or "left")
1564
	Returns:number,number = the new facing on the X and Z axis after a left turn
1565
]]--
1566
local function getLeft(curx, curz)
1567
	local hand = "left"
1568
	if layering == "up" then hand = "right" end
1569
	
1570
	if hand == "right" then
1571
		if curx == 1 then return 0,-1 end
1572
		if curx == -1 then return 0,1 end
1573
		if curz == 1 then return 1,0 end
1574
		if curz == -1 then return -1,0 end
1575
	else
1576
		if curx == 1 then return 0,1 end
1577
		if curx == -1 then return 0,-1 end
1578
		if curz == 1 then return -1,0 end
1579
		if curz == -1 then return 1,0 end
1580
	end
1581
end
1582
1583
--[[Constructs a new facing to the right of the current facing
1584
	Params: curx:number = The facing on the X axis
1585
			curz:number = The facing on the Z axis
1586
			hand:string = The hand of the axis ("right" or "left")
1587
	Returns:number,number = the new facing on the X and Z axis after a right turn
1588
]]--
1589
local function getRight(curx, curz)
1590
	local hand = "left"
1591
	if layering == "up" then hand = "right" end
1592
	
1593
	if hand == "right" then
1594
		if curx == 1 then return 0,1 end
1595
		if curx == -1 then return 0,-1 end
1596
		if curz == 1 then return -1,0 end
1597
		if curz == -1 then return 1,0 end
1598
	else
1599
		if curx == 1 then return 0,-1 end
1600
		if curx == -1 then return 0,1 end
1601
		if curz == 1 then return 1,0 end
1602
		if curz == -1 then return -1,0 end
1603
	end
1604
end
1605
1606
1607
--[[Sends out a rednet signal requesting local printers, and will listen for any responses. Printers found are added to the
1608
	printerList (for ID's) and printerNames (for names)
1609
	Params: nil
1610
	Returns:nil
1611
]]--
1612
local function locatePrinters()
1613
	printerList = { }
1614
	printerNames = { name = "Printers" }
1615
	local oldState = state
1616
	state = "Locating printers, please wait...   "
1617
	drawCanvas()
1618
	drawInterface()
1619
	state = oldState
1620
	
1621
	local modemOpened = false
1622
	for k,v in pairs(rs.getSides()) do
1623
		if peripheral.isPresent(v) and peripheral.getType(v) == "modem" then
1624
			rednet.open(v)
1625
			modemOpened = true
1626
			break
1627
		end
1628
	end
1629
	
1630
	if not modemOpened then
1631
		displayConfirmDialogue("Modem not found!", "No modem peripheral. Must have network modem to locate printers.")
1632
		return false
1633
	end
1634
	
1635
	rednet.broadcast("$3DPRINT IDENTIFY")
1636
	
1637
	while true do
1638
		local id, msg = rsTimeReceive(1)
1639
		
1640
		if not id then break end
1641
		if string.find(msg, "$3DPRINT IDACK") == 1 then
1642
			msg = string.gsub(msg, "$3DPRINT IDACK ", "")
1643
			table.insert(printerList, id)
1644
			table.insert(printerNames, msg)
1645
		end
1646
	end
1647
	
1648
	if #printerList == 0 then
1649
		displayConfirmDialogue("Printers not found!", "No active printers found in proximity of this computer.")
1650
		return false
1651
	else
1652
		return true
1653
	end
1654
end
1655
1656
--[[Sends a request to the printer. Waits on a response and updates the state of the application accordingly.
1657
	Params: command:string the command to send
1658
			param:string a parameter to send, if any
1659
	Returns:nil
1660
]]--
1661
local function sendPC(command,param)
1662
	local msg = "$PC "..command
1663
	if param then msg = msg.." "..param end
1664
	rednet.send(printerList[selectedPrinter], msg)
1665
	
1666
	while true do
1667
		local id,key = rsTimeReceive()
1668
		if id == printerList[selectedPrinter] then
1669
			if key == "$3DPRINT ACK" then
1670
				break
1671
			elseif key == "$3DPRINT DEP" then
1672
				displayConfirmDialogue("Printer Empty", "The printer has exhasted a material. Please refill slot "..param..
1673
					", and click this message when ready to continue.")
1674
				rednet.send(printerList[selectedPrinter], msg)
1675
			elseif key == "$3DPRINT OOF" then
1676
				displayConfirmDialogue("Printer Out of Fuel", "The printer has no fuel. Please replace the material "..
1677
					"in slot 1 with a fuel source, then click this message.")
1678
				rednet.send(printerList[selectedPrinter], "$PC SS 1")
1679
				id,key = rsTimeReceive()
1680
				rednet.send(printerList[selectedPrinter], "$PC RF")
1681
				id,key = rsTimeReceive()
1682
				rednet.send(printerList[selectedPrinter], msg)
1683
			end
1684
		end
1685
	end
1686
	
1687
	--Changes to position are handled after the event has been successfully completed
1688
	if command == "FW" then
1689
		px = px + pfx
1690
		pz = pz + pfz
1691
	elseif command == "BK" then
1692
		px = px - pfx
1693
		pz = pz - pfz
1694
	elseif command == "UP" then
1695
		if layering == "up" then
1696
			py = py + 1
1697
		else 
1698
			py = py - 1
1699
		end
1700
	elseif command == "DW" then
1701
		if layering == "up" then
1702
			py = py - 1
1703
		else 	
1704
			py = py + 1
1705
		end
1706
	elseif command == "TL" then
1707
		pfx,pfz = getLeft(pfx,pfz)
1708
	elseif command == "TR" then
1709
		pfx,pfz = getRight(pfx,pfz)
1710
	elseif command == "TU" then
1711
		pfx = -pfx
1712
		pfz = -pfz
1713
	end
1714
	
1715
	drawCanvas()
1716
	drawInterface()
1717
end
1718
1719
--[[A printing function that commands the printer to turn to face the desired direction, if it is not already doing so
1720
	Params: desx:number = the normalized x direction to face
1721
			desz:number = the normalized z direction to face
1722
	Returns:nil
1723
]]--
1724
local function turnToFace(desx,desz)
1725
	if desx ~= 0 then
1726
		if pfx ~= desx then
1727
			local temppfx,_ = getLeft(pfx,pfz)
1728
			if temppfx == desx then
1729
				sendPC("TL")
1730
			elseif temppfx == -desx then
1731
				sendPC("TR")
1732
			else
1733
				sendPC("TU")
1734
			end
1735
		end
1736
	else
1737
		print("on the z axis")
1738
		if pfz ~= desz then
1739
			local _,temppfz = getLeft(pfx,pfz)
1740
			if temppfz == desz then
1741
				sendPC("TL")
1742
			elseif temppfz == -desz then
1743
				sendPC("TR")
1744
			else
1745
				sendPC("TU")
1746
			end
1747
		end
1748
	end
1749
end
1750
1751
--[[Performs the print
1752
	Params: nil
1753
	Returns:nil
1754
]]--
1755
local function performPrint()
1756
	state = "active print"
1757
	if layering == "up" then
1758
		--An up layering starts our builder bot on the bottom left corner of our build
1759
		px,py,pz = leflim, 0, botlim + 1
1760
		pfx,pfz = 0,-1
1761
		
1762
		--We move him forward and up a bit from his original position.
1763
		sendPC("FW")
1764
		sendPC("UP")
1765
		--For each layer that needs to be completed, we go up by one each time
1766
		for layers=1,#frames do
1767
			--We first decide if we're going forwards or back, depending on what side we're on
1768
			local rowbot,rowtop,rowinc = nil,nil,nil
1769
			if pz == botlim then
1770
				rowbot,rowtop,rowinc = botlim,toplim,-1
1771
			else
1772
				rowbot,rowtop,rowinc = toplim,botlim,1
1773
			end
1774
			
1775
			for rows = rowbot,rowtop,rowinc do
1776
				--Then we decide if we're going left or right, depending on what side we're on
1777
				local linebot,linetop,lineinc = nil,nil,nil
1778
				if px == leflim then
1779
					--Facing from the left side has to be easterly- it's changed here
1780
					turnToFace(1,0)
1781
					linebot,linetop,lineinc = leflim,riglim,1
1782
				else
1783
					--Facing from the right side has to be westerly- it's changed here
1784
					turnToFace(-1,0)
1785
					linebot,linetop,lineinc = riglim,leflim,-1
1786
				end
1787
				
1788
				for lines = linebot,linetop,lineinc do
1789
					--We move our turtle forward, placing the right material at each step
1790
					local material = frames[py][pz][px]
1791
					if material then
1792
						material = math.log10(frames[py][pz][px])/math.log10(2) + 1
1793
						sendPC("SS", material)
1794
						sendPC("PD")
1795
					end
1796
					if lines ~= linetop then
1797
						sendPC("FW")
1798
					end
1799
				end
1800
				
1801
				--The printer then has to do a U-turn, depending on which way he's facing and
1802
				--which way he needs to go
1803
				local temppfx,temppfz = getLeft(pfx,pfz)
1804
				if temppfz == rowinc and rows ~= rowtop then
1805
					sendPC("TL")
1806
					sendPC("FW")
1807
					sendPC("TL")
1808
				elseif temppfz == -rowinc and rows ~= rowtop then
1809
					sendPC("TR")
1810
					sendPC("FW")
1811
					sendPC("TR")
1812
				end
1813
			end
1814
			--Now at the end of a run he does a 180 and moves up to begin the next part of the print
1815
			sendPC("TU")
1816
			if layers ~= #frames then
1817
				sendPC("UP")
1818
			end
1819
		end
1820
		--All done- now we head back to where we started.
1821
		if px ~= leflim then
1822
			turnToFace(-1,0)
1823
			while px ~= leflim do
1824
				sendPC("FW")
1825
			end
1826
		end
1827
		if pz ~= botlim then
1828
			turnToFace(0,-1)
1829
			while pz ~= botlim do
1830
				sendPC("BK")
1831
			end
1832
		end
1833
		turnToFace(0,-1)
1834
		sendPC("BK")
1835
		while py > 0 do
1836
			sendPC("DW")
1837
		end
1838
	else
1839
		--The front facing is at the top-left corner, facing south not north
1840
		px,py,pz = leflim, botlim, 1
1841
		pfx,pfz = 0,1
1842
		--We move the printer to the last layer- he prints from the back forwards
1843
		while pz < #frames do
1844
			sendPC("FW")
1845
		end
1846
		
1847
		--For each layer in the frame we build our wall, the move back
1848
		for layers = 1,#frames do
1849
			--We first decide if we're going left or right based on our position
1850
			local rowbot,rowtop,rowinc = nil,nil,nil
1851
			if px == leflim then
1852
				rowbot,rowtop,rowinc = leflim,riglim,1
1853
			else
1854
				rowbot,rowtop,rowinc = riglim,leflim,-1
1855
			end
1856
			
1857
			for rows = rowbot,rowtop,rowinc do
1858
				--Then we decide if we're going up or down, depending on our given altitude
1859
				local linebot,linetop,lineinc = nil,nil,nil
1860
				if py == botlim then
1861
					linebot,linetop,lineinc = botlim,toplim,-1
1862
				else
1863
					linebot,linetop,lineinc = toplim,botlim,1
1864
				end
1865
				
1866
				for lines = linebot,linetop,lineinc do
1867
				--We move our turtle up/down, placing the right material at each step
1868
					local material = frames[pz][py][px]
1869
					if material then
1870
						material = math.log10(frames[pz][py][px])/math.log10(2) + 1
1871
						sendPC("SS", material)
1872
						sendPC("PF")
1873
					end
1874
					if lines ~= linetop then
1875
						if lineinc == 1 then sendPC("DW")
1876
						else sendPC("UP") end
1877
					end
1878
				end
1879
					
1880
				if rows ~= rowtop then
1881
					turnToFace(rowinc,0)
1882
					sendPC("FW")
1883
					turnToFace(0,1)
1884
				end
1885
			end
1886
			
1887
			if layers ~= #frames then
1888
				sendPC("TU")
1889
				sendPC("FW")
1890
				sendPC("TU")
1891
			end
1892
		end
1893
		--He's easy to reset
1894
		while px ~= leflim do
1895
			turnToFace(-1,0)
1896
			sendPC("FW")
1897
		end
1898
		turnToFace(0,1)
1899
	end
1900
	
1901
	sendPC("DE")
1902
	
1903
	displayConfirmDialogue("Print complete", "The 3D print was successful.")
1904
end
1905
1906
--[[  
1907
			Section: Interface  
1908
]]--
1909
1910
--[[Runs the printing interface. Allows users to find/select a printer, the style of printing to perform and to begin the operation
1911
	Params: none
1912
	Returns:boolean true if printing was started, false otherwse
1913
]]--
1914
local function runPrintInterface()
1915
	calculateMaterials()
1916
	--There's nothing on canvas yet!
1917
	if not botlim then
1918
		displayConfirmDialogue("Cannot Print Empty Canvas", "There is nothing on canvas that "..
1919
				"can be printed, and the operation cannot be completed.")
1920
		return false
1921
	end
1922
	--No printers nearby
1923
	if not locatePrinters() then
1924
		return false
1925
	end
1926
	
1927
	layering = "up"
1928
	requirementsDisplayed = false
1929
	selectedPrinter = 1
1930
	while true do
1931
		drawCanvas()
1932
		term.setBackgroundColour(colours.lightGrey)
1933
		for i=1,10 do
1934
			term.setCursorPos(1,i)
1935
			term.clearLine()
1936
		end
1937
		drawInterface()
1938
		term.setBackgroundColour(colours.lightGrey)
1939
		term.setTextColour(colours.black)
1940
		
1941
		local msg = "3D Printing"
1942
		term.setCursorPos(w/2-#msg/2 - 2, 1)
1943
		term.write(msg)
1944
		term.setBackgroundColour(colours.grey)
1945
		term.setTextColour(colours.lightGrey)
1946
		if(requirementsDisplayed) then
1947
			msg = "Count:"
1948
		else
1949
			msg = " Slot:"
1950
		end
1951
		term.setCursorPos(w-3-#msg, 1)
1952
		term.write(msg)
1953
		term.setBackgroundColour(colours.lightGrey)
1954
		term.setTextColour(colours.black)
1955
		
1956
		term.setCursorPos(7, 2)
1957
		term.write("Layering")
1958
		drawPictureTable(layerUpIcon, 3, 3, colours.white)
1959
		drawPictureTable(layerForwardIcon, 12, 3, colours.white)
1960
		if layering == "up" then
1961
			term.setBackgroundColour(colours.red)
1962
		else
1963
			term.setBackgroundColour(colours.lightGrey)
1964
		end
1965
		term.setCursorPos(3, 9)
1966
		term.write("Upwards")
1967
		if layering == "forward" then
1968
			term.setBackgroundColour(colours.red)
1969
		else
1970
			term.setBackgroundColour(colours.lightGrey)
1971
		end
1972
		term.setCursorPos(12, 9)
1973
		term.write("Forward")
1974
		
1975
		term.setBackgroundColour(colours.lightGrey)
1976
		term.setTextColour(colours.black)
1977
		term.setCursorPos(31, 2)
1978
		term.write("Printer ID")
1979
		term.setCursorPos(33, 3)
1980
		if #printerList > 1 then
1981
			term.setBackgroundColour(colours.grey)
1982
			term.setTextColour(colours.lightGrey)
1983
		else
1984
			term.setTextColour(colours.red)
1985
		end
1986
		term.write(" "..printerNames[selectedPrinter].." ")
1987
		
1988
		term.setBackgroundColour(colours.grey)
1989
		term.setTextColour(colours.lightGrey)
1990
		term.setCursorPos(25, 10)
1991
		term.write(" Cancel ")
1992
		term.setCursorPos(40, 10)
1993
		term.write(" Print ")
1994
		
1995
		local id, p1, p2, p3 = os.pullEvent()
1996
		
1997
		if id == "timer" then
1998
			updateTimer(p1)
1999
		elseif id == "mouse_click" then
2000
			--Layering Buttons
2001
			if p2 >= 3 and p2 <= 9 and p3 >= 3 and p3 <= 9 then
2002
				layering = "up"
2003
			elseif p2 >= 12 and p2 <= 18 and p3 >= 3 and p3 <= 9 then
2004
				layering = "forward"
2005
			--Count/Slot
2006
			elseif p2 >= w - #msg - 3 and p2 <= w - 3 and p3 == 1 then
2007
				requirementsDisplayed = not requirementsDisplayed
2008
			--Printer ID
2009
			elseif p2 >= 33 and p2 <= 33 + #printerNames[selectedPrinter] and p3 == 3 and #printerList > 1 then
2010
				local chosenName = displayDropDown(33, 3, printerNames)
2011
				for i=1,#printerNames do
2012
					if printerNames[i] == chosenName then
2013
						selectedPrinter = i
2014
						break;
2015
					end
2016
				end
2017
			--Print and Cancel
2018
			elseif p2 >= 25 and p2 <= 32 and p3 == 10 then
2019
				break
2020
			elseif p2 >= 40 and p2 <= 46 and p3 == 10 then
2021
				rednet.send(printerList[selectedPrinter], "$3DPRINT ACTIVATE")
2022
				ready = false
2023
				while true do
2024
					local id,msg = rsTimeReceive(10)
2025
					
2026
					if id == printerList[selectedPrinter] and msg == "$3DPRINT ACTACK" then
2027
						ready = true
2028
						break
2029
					end
2030
				end
2031
				if ready then
2032
					performPrint()
2033
					break
2034
				else
2035
					displayConfirmDialogue("Printer Didn't Respond", "The printer didn't respond to the activation command. Check to see if it's online")
2036
				end
2037
			end
2038
		end
2039
	end
2040
	state = "paint"
2041
end
2042
2043
--[[This function changes the current paint program to another tool or mode, depending on user input. Handles
2044
	any necessary changes in logic involved in that.
2045
	Params: mode:string = the name of the mode to change to
2046
	Returns:nil
2047
]]--
2048
local function performSelection(mode)
2049
	if not mode or mode == "" then return
2050
	
2051
	elseif mode == "help" then
2052
		drawHelpScreen()
2053
		
2054-
			if p1==keys.leftCtrl then
2054+
2055
		blueprint = true
2056
		ddModes[2][3] = "blueprint off"
2057
		
2058
	elseif mode == "blueprint off" then
2059
		blueprint = false
2060
		ddModes[2][3] = "blueprint on"
2061
		
2062
	elseif mode == "layers on" then
2063
		layerDisplay = true
2064
		ddModes[2][4] = "layers off"
2065
	
2066
	elseif mode == "layers off" then
2067
		layerDisplay = false
2068
		ddModes[2][4] = "layers on"
2069
	
2070
	elseif mode == "direction on" then
2071
		printDirection = true
2072
		ddModes[2][5] = "direction off"
2073
		
2074
	elseif mode == "direction off" then
2075
		printDirection = false
2076
		ddModes[2][5] = "direction on"
2077
	
2078
	elseif mode == "go to" then
2079
		changeFrame()
2080
	
2081
	elseif mode == "remove" then
2082
		removeFramesAfter(sFrame)
2083
	
2084
	elseif mode == "play" then
2085
		playAnimation()
2086
		
2087
	elseif mode == "copy" then
2088
		if selectrect and selectrect.x1 ~= selectrect.x2 then
2089
			copyToBuffer(false)
2090
		end
2091
	
2092
	elseif mode == "cut" then
2093
		if selectrect and selectrect.x1 ~= selectrect.x2 then 
2094
			copyToBuffer(true)
2095
		end
2096
		
2097
	elseif mode == "paste" then
2098
		if selectrect and selectrect.x1 ~= selectrect.x2 then 
2099
			copyFromBuffer(false)
2100
		end
2101
		
2102
	elseif mode == "hide" then
2103
		selectrect = nil
2104
		if state == "select" then state = "corner select" end
2105
		
2106
	elseif mode == "alpha to left" then
2107
		if lSel then alphaC = lSel end
2108
		
2109
	elseif mode == "alpha to right" then
2110
		if rSel then alphaC = rSel end
2111
		
2112
	elseif mode == "record" then
2113
		record = not record
2114
		
2115
	elseif mode == "clear" then
2116
		if state=="select" then buffer = nil
2117
		else clearImage() end
2118
	
2119
	elseif mode == "select" then
2120
		if state=="corner select" or state=="select" then
2121
			state = "paint"
2122
		elseif selectrect and selectrect.x1 ~= selectrect.x2 then
2123
			state = "select"
2124
		else
2125
			state = "corner select" 
2126
		end
2127
		
2128
	elseif mode == "print" then
2129
		state = "print"
2130
		runPrintInterface()
2131-
		elseif id=="char" and tonumber(p1) then
2131+
2132-
			if state=="brush" and tonumber(p1) > 1 then
2132+
2133-
				brushsize = tonumber(p1)
2133+
2134-
			elseif animated and tonumber(p1) > 0 then
2134+
2135-
				changeFrame(tonumber(p1))
2135+
		elseif textEnabled then saveNFT(sPath)
2136
		else saveNFP(sPath) end
2137
		
2138
	elseif mode == "exit" then
2139
		isRunning = false
2140
	
2141
	elseif mode ~= state then state = mode
2142
	else state = "paint"
2143
	
2144
	end
2145
end
2146
2147
--[[The main function of the program, reads and handles all events and updates them accordingly. Mode changes,
2148
	painting to the canvas and general selections are done here.
2149
	Params: none
2150
	Returns:nil
2151
]]--
2152
local function handleEvents()
2153
	recttimer = os.startTimer(0.5)
2154
	while isRunning do
2155
		drawCanvas()
2156-
	print("Usage: npaintpro [-a] <path>")
2156+
2157
		
2158
		if state == "text" then
2159
			term.setCursorPos(textCurX - sx, textCurY - sy)
2160
			term.setCursorBlink(true)
2161
		end
2162
		
2163
		local id,p1,p2,p3 = os.pullEvent()
2164
			term.setCursorBlink(false)
2165
		if id=="timer" then
2166-
	elseif string.find(sPath, ".nfp") ~= #sPath-3 and string.find(sPath, ".nfa") ~= #sPath-3 then
2166+
2167-
		print("Can only edit .nfp and nfa files:",string.find(sPath, ".nfp"),#sPath-3)
2167+
2168
			if p2 >=w-1 and p3 < #column+1 then
2169
				if p1==1 then lSel = column[p3]
2170
				else rSel = column[p3] end
2171
			elseif p2 >=w-1 and p3==#column+1 then
2172
				if p1==1 then lSel = nil
2173
				else rSel = nil end
2174
			elseif p2==w-1 and p3==h and animated then
2175
				changeFrame(sFrame-1)
2176
			elseif p2==w and p3==h and animated then
2177
				changeFrame(sFrame+1)
2178
			elseif p2 < w-10 and p3==h then
2179
				local sel = displayDropDown(1, h-1, ddModes)
2180
				performSelection(sel)
2181
			elseif p2 < w-1 and p3 <= h-1 then
2182
				if state=="pippette" then
2183
					if p1==1 then
2184
						if frames[sFrame][p3+sy] and frames[sFrame][p3+sy][p2+sx] then
2185
							lSel = frames[sFrame][p3+sy][p2+sx] 
2186-
	if not animated and string.find(sPath, ".nfp") ~= #sPath-3 then 
2186+
2187
					elseif p1==2 then
2188
						if frames[sFrame][p3+sy] and frames[sFrame][p3+sy][p2+sx] then
2189
							rSel = frames[sFrame][p3+sy][p2+sx] 
2190
						end
2191
					end
2192
				elseif state=="move" then
2193
					updateImageLims(record)
2194
					moveImage(p2,p3)
2195
				elseif state=="flood" then
2196
					if p1 == 1 and lSel and frames[sFrame][p3+sy]  then 
2197
						floodFill(p2,p3,frames[sFrame][p3+sy][p2+sx],lSel)
2198
					elseif p1 == 2 and rSel and frames[sFrame][p3+sy] then 
2199
						floodFill(p2,p3,frames[sFrame][p3+sy][p2+sx],rSel)
2200
					end
2201
				elseif state=="corner select" then
2202
					if not selectrect then
2203
						selectrect = { x1=p2+sx, x2=p2+sx, y1=p3+sy, y2=p3+sy }
2204
					elseif selectrect.x1 ~= p2+sx and selectrect.y1 ~= p3+sy then
2205
						if p2+sx<selectrect.x1 then selectrect.x1 = p2+sx
2206
						else selectrect.x2 = p2+sx end
2207
						
2208
						if p3+sy<selectrect.y1 then selectrect.y1 = p3+sy
2209
						else selectrect.y2 = p3+sy end
2210
						
2211
						state = "select"
2212
					end
2213
				elseif state=="textpaint" then
2214
					local paintCol = lSel
2215
					if p1 == 2 then paintCol = rSel end
2216
					if frames[sFrame].textcol[p3+sy] and frames[sFrame].textcol[p3+sy][p3+sx] then
2217
						frames[sFrame].textcol[p3+sy][p2+sx] = paintCol
2218
					end
2219
				elseif state=="text" then
2220
					textCurX = p2 + sx
2221
					textCurY = p3 + sy
2222
				elseif state=="select" then
2223
					if p1 == 1 then
2224
						local swidth = selectrect.x2 - selectrect.x1
2225
						local sheight = selectrect.y2 - selectrect.y1
2226
					
2227
						selectrect.x1 = p2 + sx
2228
						selectrect.y1 = p3 + sy
2229
						selectrect.x2 = p2 + swidth + sx
2230
						selectrect.y2 = p3 + sheight + sy
2231
					elseif p1 == 2 and p2 < w-2 and p3 < h-1 then
2232
						inMenu = true
2233
						local sel = displayDropDown(p2, p3, srModes) 
2234
						inMenu = false
2235
						performSelection(sel)
2236
					end
2237
				else
2238
					local f,l = sFrame,sFrame
2239
					if record then f,l = 1,framecount end
2240
					local bwidth = 0
2241
					if state == "brush" then bwidth = brushsize-1 end
2242
				
2243
					for i=f,l do
2244
						for x = math.max(1,p2+sx-bwidth),p2+sx+bwidth do
2245
							for y = math.max(1,p3+sy-bwidth), p3+sy+bwidth do
2246
								if math.abs(x - (p2+sx)) + math.abs(y - (p3+sy)) <= bwidth then
2247
									if not frames[i][y] then frames[i][y] = {} end
2248
									if p1==1 then frames[i][y][x] = lSel
2249
									else frames[i][y][x] = rSel end
2250
									
2251
									if textEnabled then
2252
										if not frames[i].text[y] then frames[i].text[y] = { } end
2253
										if not frames[i].textcol[y] then frames[i].textcol[y] = { } end
2254
									end
2255
								end
2256
							end
2257
						end
2258
					end
2259
				end
2260
			end
2261
		elseif id=="char" then
2262
			if state=="text" then
2263
				if not frames[sFrame][textCurY] then frames[sFrame][textCurY] = { } end
2264
				if not frames[sFrame].text[textCurY] then frames[sFrame].text[textCurY] = { } end
2265
				if not frames[sFrame].textcol[textCurY] then frames[sFrame].textcol[textCurY] = { } end
2266
				
2267
				if lSel then frames[sFrame][textCurY][textCurX] = lSel end
2268
				if rSel then 
2269
					frames[sFrame].text[textCurY][textCurX] = p1
2270
					frames[sFrame].textcol[textCurY][textCurX] = rSel
2271
				else
2272
					frames[sFrame].text[textCurY][textCurX] = " "
2273
					frames[sFrame].textcol[textCurY][textCurX] = lSel
2274
				end
2275
				
2276
				textCurX = textCurX+1
2277
				if textCurX > w + sx - 2 then sx = textCurX - w + 2 end
2278
			elseif tonumber(p1) then
2279
				if state=="brush" and tonumber(p1) > 1 then
2280
					brushsize = tonumber(p1)
2281
				elseif animated and tonumber(p1) > 0 then
2282
					changeFrame(tonumber(p1))
2283
				end
2284
			end
2285
		elseif id=="key" then
2286
			--Text needs special handlers (all other keyboard shortcuts are of course reserved for typing)
2287
			if state=="text" then
2288
				if p1==keys.backspace and textCurX > 1 then
2289
					textCurX = textCurX-1
2290
					frames[sFrame].text[textCurY][textCurX] = nil
2291
					frames[sFrame].textcol[textCurY][textCurX] = nil
2292
					if textCurX < sx then sx = textCurX end
2293
				elseif p1==keys.left and textCurX > 1 then
2294
					textCurX = textCurX-1
2295
					if textCurX < sx then sx = textCurX end
2296
				elseif p1==keys.right then
2297
					textCurX = textCurX+1
2298
					if textCurX > w + sx - 2 then sx = textCurX - w + 2 end
2299
				elseif p1==keys.up and textCurY > 1 then
2300
					textCurY = textCurY-1
2301
					if textCurY < sy then sy = textCurY end
2302
				elseif p1==keys.down then
2303
					textCurY = textCurY+1
2304
					if textCurY > h + sy - 2 then sy = textCurY - h + 2 end
2305
				end
2306
			
2307
			elseif p1==keys.leftCtrl then
2308
				local sel = displayDropDown(1, h-1, ddModes[#ddModes]) 
2309
				performSelection(sel)
2310
			elseif p1==keys.leftAlt then
2311
				local sel = displayDropDown(1, h-1, ddModes[1]) 
2312
				performSelection(sel)
2313
			elseif p1==keys.h then 
2314
				performSelection("help")
2315
			elseif p1==keys.x then 
2316
				performSelection("cut")
2317
			elseif p1==keys.c then
2318
				performSelection("copy")
2319
			elseif p1==keys.v then
2320
				performSelection("paste")
2321
			elseif p1==keys.z then
2322
				performSelection("clear")
2323
			elseif p1==keys.s then
2324
				performSelection("select")
2325
			elseif p1==keys.tab then
2326
				performSelection("hide")
2327
			elseif p1==keys.q then
2328
				performSelection("alpha to left")
2329
			elseif p1==keys.w then
2330
				performSelection("alpha to right")
2331
			elseif p1==keys.f then
2332
				performSelection("flood")
2333
			elseif p1==keys.b then
2334
				performSelection("brush")
2335
			elseif p1==keys.m then
2336
				performSelection("move")
2337
			elseif p1==keys.backslash and animated then
2338
				performSelection("record")
2339
			elseif p1==keys.p then
2340
				performSelection("pippette")
2341
			elseif p1==keys.g and animated then
2342
				performSelection("go to")
2343
			elseif p1==keys.period and animated then
2344
				changeFrame(sFrame+1)
2345
			elseif p1==keys.comma and animated then
2346
				changeFrame(sFrame-1)
2347
			elseif p1==keys.r and animated then
2348
				performSelection("remove")
2349
			elseif p1==keys.space and animated then
2350
				performSelection("play")
2351
			elseif p1==keys.t and textEnabled then
2352
				performSelection("text")
2353
				sleep(0.01)
2354
			elseif p1==keys.y and textEnabled then
2355
				performSelection("textpaint")
2356
			elseif p1==keys.left then
2357
				if state == "move" then
2358
					updateImageLims(record)
2359
					moveImage(leflim-1,toplim)
2360
				elseif state=="select" and selectrect.x1 > 1 then
2361
					selectrect.x1 = selectrect.x1-1
2362
					selectrect.x2 = selectrect.x2-1
2363
				elseif sx > 0 then sx=sx-1 end
2364
			elseif p1==keys.right then
2365
				if state == "move" then
2366
					updateImageLims(record)
2367
					moveImage(leflim+1,toplim)
2368
				elseif state=="select" then
2369
					selectrect.x1 = selectrect.x1+1
2370
					selectrect.x2 = selectrect.x2+1
2371
				else sx=sx+1 end
2372
			elseif p1==keys.up then
2373
				if state == "move" then
2374
					updateImageLims(record)
2375
					moveImage(leflim,toplim-1)
2376
				elseif state=="select" and selectrect.y1 > 1 then
2377
					selectrect.y1 = selectrect.y1-1
2378
					selectrect.y2 = selectrect.y2-1
2379
				elseif sy > 0 then sy=sy-1 end
2380
			elseif p1==keys.down then 
2381
				if state == "move" then
2382
					updateImageLims(record)
2383
					moveImage(leflim,toplim+1)
2384
				elseif state=="select" then
2385
					selectrect.y1 = selectrect.y1+1
2386
					selectrect.y2 = selectrect.y2+1
2387
				else sy=sy+1 end
2388
			end
2389
		end
2390
	end
2391
end
2392
2393
--[[
2394
			Section: Main  
2395
]]--
2396
2397
if not term.isColour() then
2398
	print("For colour computers only")
2399
	return
2400
end
2401
2402
--Taken almost directly from edit (for consistency)
2403
local tArgs = {...}
2404
2405
local ca = 1
2406
2407
if tArgs[ca] == "-a" then
2408
	animated = true
2409
	ca = ca + 1
2410
end
2411
2412
if tArgs[ca] == "-t" then
2413
	textEnabled = true
2414
	ca = ca + 1
2415
end
2416
2417
if #tArgs < ca then
2418
	print("Usage: npaintpro [-a,-t] <path>")
2419
	return
2420
end
2421
2422
--Yeah you can't have animated text files YET... I haven't supported that, maybe later?
2423
if animated and textEnabled then
2424
	print("No support for animated text files- cannot have both -a and -t")
2425
end
2426
2427
sPath = shell.resolve(tArgs[ca])
2428
local bReadOnly = fs.isReadOnly(sPath)
2429
if fs.exists(sPath) then
2430
	if fs.isDir(sPath) then
2431
		print("Cannot edit a directory.")
2432
		return
2433
	elseif string.find(sPath, ".nfp") ~= #sPath-3 and string.find(sPath, ".nfa") ~= #sPath-3 and
2434
			string.find(sPath, ".nft") ~= #sPath-3 then
2435
		print("Can only edit .nfp, .nft and .nfa files:",string.find(sPath, ".nfp"),#sPath-3)
2436
		return
2437
	end
2438
	
2439
	if string.find(sPath, ".nfa") == #sPath-3 then
2440
		animated = true
2441
	end
2442
	
2443
	if string.find(sPath, ".nft") == #sPath-3 then
2444
		textEnabled = true
2445
	end	
2446
	
2447
	if string.find(sPath, ".nfp") == #sPath-3 and animated then
2448
		print("Convert to nfa? Y/N")
2449
		if string.find(string.lower(io.read()), "y") then
2450
			local nsPath = string.sub(sPath, 1, #sPath-1).."a"
2451
			fs.move(sPath, nsPath)
2452
			sPath = nsPath
2453
		else
2454
			animated = false
2455
		end
2456
	end
2457
	
2458
	--Again this is possible, I just haven't done it. Maybe I will?
2459
	if textEnabled and (string.find(sPath, ".nfp") == #sPath-3 or string.find(sPath, ".nfa") == #sPath-3) then
2460
		print("Cannot convert to nft")
2461
	end
2462
else
2463
	if not animated and not textEnabled and string.find(sPath, ".nfp") ~= #sPath-3 then 
2464
		sPath = sPath..".nfp"
2465
	elseif animated and string.find(sPath, ".nfa") ~= #sPath-3 then 
2466
		sPath = sPath..".nfa"
2467
	elseif textEnabled and string.find(sPath, ".nft") ~= #sPath-3 then
2468
		sPath = sPath..".nft"
2469
	end
2470
end 
2471
2472
drawLogo()
2473
init()
2474
handleEvents()
2475
2476
term.setBackgroundColour(colours.black)
2477
shell.run("clear")