View difference between Paste ID: udDhEHeV 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
43+
local alphaC = colours.black
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] and 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] and 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
			if frames[sFrame].text[y] then
662
				local char = frames[sFrame].text[y][x]
663
				if not char then char = " " end
664
				line = line..char
665
			end
666
		end
667
		file:write(line.."\n")
668
	end
669
	file:close()
670
end
671
672
--[[Saves a animated paint file to the specified path
673
	Params: path:string = The path to save the file to
674
	Returns:nil
675
]]--
676
local function saveNFA(path)
677
	local sDir = string.sub(sPath, 1, #sPath - #fs.getName(sPath))
678
	if not fs.exists(sDir) then
679
		fs.makeDir(sDir)
680
	end
681
	
682
	local file = io.open(path, "w")
683
	updateImageLims(true)
684
	if not toplim then 
685
		file:close()
686
		return
687
	end
688
	for i=1,#frames do
689
		for y=1,botlim do
690
			local line = ""
691
			if frames[i][y] then 
692
				for x=1,riglim do
693
					line = line..getHexOf(frames[i][y][x])
694
				end
695
			end
696
			file:write(line.."\n")
697
		end
698
		if i < #frames then file:write("~\n") end
699
	end
700
	file:close()
701
end
702
703
704
--[[Initializes the program, by loading in the paint file. Called at the start of each program.
705
	Params: none
706
	Returns:nil
707
]]--
708
local function init()
709
	if textEnabled then
710
		loadNFT(sPath)
711
		table.insert(ddModes, 2, { "text", "textpaint", name = "text"})
712
	elseif animated then 
713
		loadNFA(sPath)
714
		table.insert(ddModes, #ddModes, { "record", "play", name = "anim" })
715
		table.insert(ddModes, #ddModes, { "go to", "remove", name = "frames"})
716
		table.insert(ddModes[2], #ddModes[2], "blueprint on")
717
		table.insert(ddModes[2], #ddModes[2], "layers on")
718
	else 
719
		loadNFP(sPath) 
720
		table.insert(ddModes[2], #ddModes[2], "blueprint on")
721
	end
722
723
	for i=0,15 do
724
		table.insert(column, math.pow(2,i))
725
	end
726
end
727
728
--[[  
729
			Section: Drawing  
730
]]--
731
732
733
--[[Draws the rather superflous logo. Takes about 1 second, before user is able to move to the
734
	actual program.
735
	Params: none
736
	Returns:nil
737
]]--
738
local function drawLogo()
739
	term.setBackgroundColour(colours.white)
740
	term.clear()
741
	drawPictureTable(logo, w/2 - #logo[1]/2, h/2 - #logo/2, colours.white)
742
	term.setBackgroundColour(colours.white)
743
	term.setTextColour(colours.black)
744
	local msg = "NPaintPro"
745
	term.setCursorPos(w/2 - #msg/2, h-3)
746
	term.write(msg)
747
	msg = "By NitrogenFingers"
748
	term.setCursorPos(w/2 - #msg/2, h-2)
749
	term.write(msg)
750
	
751
	os.pullEvent()
752
end
753
754
--[[Clears the display to the alpha channel colour, draws the canvas, the image buffer and the selection
755
	rectanlge if any of these things are present.
756
	Params: none
757
	Returns:nil
758
]]--
759
local function drawCanvas()
760
	--We have to readjust the position of the canvas if we're printing
761
	turtlechar = "@"
762
	if state == "active print" then
763
		if layering == "up" then
764
			if py >= 1 and py <= #frames then
765
				sFrame = py
766
			end
767
			if pz < sy then sy = pz
768
			elseif pz > sy + h - 1 then sy = pz + h - 1 end
769
			if px < sx then sx = px
770
			elseif px > sx + w - 2 then sx = px + w - 2 end
771
		else
772
			if pz >= 1 and pz <= #frames then
773
				sFrame = pz
774
			end
775
			
776
			if py < sy then sy = py
777
			elseif py > sy + h - 1 then sy = py + h - 1 end
778
			if px < sx then sx = px
779
			elseif px > sx + w - 2 then sx = px + w - 2 end
780
		end
781
		
782
		if pfx == 1 then turtlechar = ">"
783
		elseif pfx == -1 then turtlechar = "<"
784
		elseif pfz == 1 then turtlechar = "V"
785
		elseif pfz == -1 then turtlechar = "^"
786
		end
787
	end
788
789
	--Picture next
790
	local topLayer, botLayer
791
	if layerDisplay then
792
		topLayer = sFrame
793
		botLayer = 1
794
	else
795
		topLayer,botLayer = sFrame,sFrame
796
	end
797
	
798
	for currframe = botLayer,topLayer,1 do
799
		for y=sy+1,sy+h-1 do
800
			if frames[currframe][y] then 
801
				for x=sx+1,sx+w-2 do
802
					term.setCursorPos(x-sx,y-sy)
803
					if frames[currframe][y][x] then
804
						term.setBackgroundColour(frames[currframe][y][x])
805
						if textEnabled and frames[currframe].textcol[y][x] and frames[currframe].text[y][x] then
806
							term.setTextColour(frames[currframe].textcol[y][x])
807
							term.write(frames[currframe].text[y][x])
808
						else
809
							term.write(" ")
810
						end
811
					else 
812
						tileExists = false
813
						for i=currframe-1,botLayer,-1 do
814
							if frames[i][y][x] then
815
								tileExists = true
816
								break
817
							end
818
						end
819
						
820
						if not tileExists then
821
							if blueprint then
822
								term.setBackgroundColour(colours.blue)
823
								term.setTextColour(colours.white)
824
								if x == sx+1 and y % 4 == 1 then
825
									term.write(""..((y/4) % 10))
826
								elseif y == sy + 1 and x % 4 == 1 then
827
									term.write(""..((x/4) % 10))
828
								elseif x % 2 == 1 and y % 2 == 1 then
829
									term.write("+")
830
								elseif x % 2 == 1 then
831
									term.write("|")
832
								elseif y % 2 == 1 then
833
									term.write("-")
834
								else
835
									term.write(" ")
836
								end
837
							else
838
								term.setBackgroundColour(alphaC) 
839
								if textEnabled and frames[currframe].textcol[y][x] and frames[currframe].text[y][x] then
840
									term.setTextColour(frames[currframe].textcol[y][x])
841
									term.write(frames[currframe].text[y][x])
842
								else
843
									term.write(" ")
844
								end
845
							end
846
						end
847
					end
848
				end
849
			else
850
				for x=sx+1,sx+w-2 do
851
					term.setCursorPos(x-sx,y-sy)
852
					
853
					tileExists = false
854
					for i=currframe-1,botLayer,-1 do
855
						if frames[i][y] and frames[i][y][x] then
856
							tileExists = true
857
							break
858
						end
859
					end
860
					
861
					if not tileExists then
862
						if blueprint then
863
							term.setBackgroundColour(colours.blue)
864
							term.setTextColour(colours.white)
865
							if x == sx+1 and y % 4 == 1 then
866
								term.write(""..((y/4) % 10))
867
							elseif y == sy + 1 and x % 4 == 1 then
868
								term.write(""..((x/4) % 10))
869
							elseif x % 2 == 1 and y % 2 == 1 then
870
								term.write("+")
871
							elseif x % 2 == 1 then
872
								term.write("|")
873
							elseif y % 2 == 1 then
874
								term.write("-")
875
							else
876
								term.write(" ")
877
							end
878
						else
879
							term.setBackgroundColour(alphaC) 
880
							term.write(" ")
881
						end
882
					end
883
				end
884
			end
885
		end
886
	end
887
	
888
	--Then the printer, if he's on
889
	if state == "active print" then
890
		local bgColour = alphaC
891
		if layering == "up" then
892
			term.setCursorPos(px-sx,pz-sy)
893
			if frames[sFrame] and frames[sFrame][pz-sy] and frames[sFrame][pz-sy][px-sx] then
894
				bgColour = frames[sFrame][pz-sy][px-sx]
895
			elseif blueprint then bgColour = colours.blue end
896
		else
897
			term.setCursorPos(px-sx,py-sy)
898
			if frames[sFrame] and frames[sFrame][py-sy] and frames[sFrame][py-sy][px-sx] then
899
				bgColour = frames[sFrame][py-sy][px-sx]
900
			elseif blueprint then bgColour = colours.blue end
901
		end
902
		
903
		term.setBackgroundColour(bgColour)
904
		if bgColour == colours.black then term.setTextColour(colours.white)
905
		else term.setTextColour(colours.black) end
906
		
907
		term.write(turtlechar)
908
	end
909
	
910
	--Then the buffer
911
	if selectrect then
912
		if buffer and rectblink == 1 then
913
		for y=selectrect.y1, math.min(selectrect.y2, selectrect.y1 + buffer.height-1) do
914
			for x=selectrect.x1, math.min(selectrect.x2, selectrect.x1 + buffer.width-1) do
915
				if buffer.contents[y-selectrect.y1+1][x-selectrect.x1+1] then
916
					term.setCursorPos(x+sx,y+sy)
917
					term.setBackgroundColour(buffer.contents[y-selectrect.y1+1][x-selectrect.x1+1])
918
					term.write(" ")
919
				end
920
			end
921
		end
922
		end
923
	
924
		--This draws the "selection" box
925
		local add = nil
926
		if buffer then
927
			term.setBackgroundColour(colours.lightGrey)
928
		else 
929
			term.setBackgroundColour(colours.grey)
930
		end
931
		for i=selectrect.x1, selectrect.x2 do
932
			add = (i + selectrect.y1 + rectblink) % 2 == 0
933
			term.setCursorPos(i-sx,selectrect.y1-sy)
934
			if add then term.write(" ") end
935
			add = (i + selectrect.y2 + rectblink) % 2 == 0
936
			term.setCursorPos(i-sx,selectrect.y2-sy)
937
			if add then term.write(" ") end
938
		end
939
		for i=selectrect.y1 + 1, selectrect.y2 - 1 do
940
			add = (i + selectrect.x1 + rectblink) % 2 == 0
941
			term.setCursorPos(selectrect.x1-sx,i-sy)
942
			if add then term.write(" ") end
943
			add = (i + selectrect.x2 + rectblink) % 2 == 0
944
			term.setCursorPos(selectrect.x2-sx,i-sy)
945
			if add then term.write(" ") end
946
		end
947
	end
948
end
949
950
--[[Draws the colour picker on the right side of the screen, the colour pallette and the footer with any 
951
	messages currently being displayed
952
	Params: none
953
	Returns:nil
954
]]--
955
local function drawInterface()
956
	--Picker
957
	for i=1,#column do
958
		term.setCursorPos(w-1, i)
959
		term.setBackgroundColour(column[i])
960
		if state == "print" then
961
			if i == 16 then
962
				term.setTextColour(colours.white)
963
			else
964
				term.setTextColour(colours.black)
965
			end
966
			if requirementsDisplayed then
967
				if requiredMaterials[i] < 10 then term.write(" ") end
968
				term.setCursorPos(w-#tostring(requiredMaterials[i])+1, i)
969
				term.write(requiredMaterials[i])
970
			else
971
				if i < 10 then term.write(" ") end
972
				term.write(i)
973
			end
974
		else
975
			term.write("  ")
976
		end
977
	end
978
	term.setCursorPos(w-1,#column+1)
979
	term.setBackgroundColour(colours.black)
980
	term.setTextColour(colours.red)
981
	term.write("XX")
982
	--Pallette
983
	term.setCursorPos(w-1,h-1)
984
	if not lSel then
985
		term.setBackgroundColour(colours.black)
986
		term.setTextColour(colours.red)
987
		term.write("X")
988
	else
989
		term.setBackgroundColour(lSel)
990
		term.setTextColour(lSel)
991
		term.write(" ")
992
	end
993
	if not rSel then
994
		term.setBackgroundColour(colours.black)
995
		term.setTextColour(colours.red)
996
		term.write("X")
997
	else
998
		term.setBackgroundColour(rSel)
999
		term.setTextColour(rSel)
1000
		term.write(" ")
1001
	end
1002
	--Footer
1003
	if inMenu then return end
1004
	
1005
	term.setCursorPos(1, h)
1006
	term.setBackgroundColour(colours.lightGrey)
1007
	term.setTextColour(colours.grey)
1008
	term.clearLine()
1009
	if inDropDown then
1010
		term.write(string.rep(" ", 6))
1011
	else
1012
		term.setBackgroundColour(colours.grey)
1013
		term.setTextColour(colours.lightGrey)
1014
		term.write("menu  ")
1015
	end
1016
	term.setBackgroundColour(colours.lightGrey)
1017
	term.setTextColour(colours.grey)
1018
	term.write(getStateMessage())
1019
	
1020
	local coords="X:"..sx.." Y:"..sy
1021
	if animated then coords = coords.." Frame:"..sFrame.."/"..framecount.."   " end
1022
	term.setCursorPos(w-#coords+1,h)
1023
	if state == "play" then term.setBackgroundColour(colours.lime)
1024
	elseif record then term.setBackgroundColour(colours.red) end
1025
	term.write(coords)
1026
	
1027
	if animated then
1028
		term.setCursorPos(w-1,h)
1029
		term.setBackgroundColour(colours.grey)
1030
		term.setTextColour(colours.lightGrey)
1031
		term.write("<>")
1032
	end
1033
end
1034
1035
--[[Runs an interface where users can select topics of help. Will return once the user quits the help screen.
1036
	Params: none
1037
	Returns:nil
1038
]]--
1039
local function drawHelpScreen()
1040
	local selectedHelp = nil
1041
	while true do
1042
		term.setBackgroundColour(colours.lightGrey)
1043
		term.clear()
1044
		if not selectedHelp then
1045
			term.setCursorPos(4, 1)
1046
			term.setTextColour(colours.brown)
1047
			term.write("Available modes (click for info):")
1048
			for i=1,#helpTopics do
1049
				term.setCursorPos(2, 2 + i)
1050
				term.setTextColour(colours.black)
1051
				term.write(helpTopics[i].name)
1052
				if helpTopics[i].key then
1053
					term.setTextColour(colours.red)
1054
					term.write(" ("..helpTopics[i].key..")")
1055
				end
1056
			end
1057
			term.setCursorPos(4,h)
1058
			term.setTextColour(colours.black)
1059
			term.write("Press any key to exit")
1060
		else
1061
			term.setCursorPos(4,1)
1062
			term.setTextColour(colours.brown)
1063
			term.write(helpTopics[selectedHelp].name)
1064
			if helpTopics[selectedHelp].key then
1065
				term.setTextColour(colours.red)
1066
				term.write(" ("..helpTopics[selectedHelp].key..")")
1067
			end
1068
			term.setCursorPos(1,3)
1069
			term.setTextColour(colours.black)
1070
			print(helpTopics[selectedHelp].message.."\n")
1071
			for i=1,#helpTopics[selectedHelp].controls do
1072
				term.setTextColour(colours.brown)
1073
				term.write(helpTopics[selectedHelp].controls[i][1].." ")
1074
				term.setTextColour(colours.black)
1075
				print(helpTopics[selectedHelp].controls[i][2])
1076
			end
1077
		end
1078
		
1079
		local id,p1,p2,p3 = os.pullEvent()
1080
		
1081
		if id == "timer" then updateTimer(p1)
1082
		elseif id == "key" then 
1083
			if selectedHelp then selectedHelp = nil
1084
			else break end
1085
		elseif id == "mouse_click" then
1086
			if not selectedHelp then 
1087
				if p3 >=3 and p3 <= 2+#helpTopics then
1088
					selectedHelp = p3-2 
1089
				else break end
1090
			else
1091
				selectedHelp = nil
1092
			end
1093
		end
1094
	end
1095
end
1096
1097
--[[Draws a message in the footer bar. A helper for DrawInterface, but can be called for custom messages, if the
1098
	inMenu paramter is set to true while this is being done (remember to set it back when done!)
1099
	Params: message:string = The message to be drawn
1100
	Returns:nil
1101
]]--
1102
local function drawMessage(message)
1103
	term.setCursorPos(1,h)
1104
	term.setBackgroundColour(colours.lightGrey)
1105
	term.setTextColour(colours.grey)
1106
	term.clearLine()
1107
	term.write(message)
1108
end
1109
1110
--[[
1111
			Section: Generic Interfaces
1112
]]--
1113
1114
1115
--[[One of my generic text printing methods, printing a message at a specified position with width and offset.
1116
	No colour materials included.
1117
	Params: msg:string = The message to print off-center
1118
			height:number = The starting height of the message
1119
			width:number = The limit as to how many characters long each line may be
1120
			offset:number = The starting width offset of the message
1121
	Returns:number the number of lines used in printing the message
1122
]]--
1123
local function wprintOffCenter(msg, height, width, offset)
1124
	local inc = 0
1125
	local ops = 1
1126
	while #msg - ops > width do
1127
		local nextspace = 0
1128
		while string.find(msg, " ", ops + nextspace) and
1129
				string.find(msg, " ", ops + nextspace) - ops < width do
1130
			nextspace = string.find(msg, " ", nextspace + ops) + 1 - ops
1131
		end
1132
		local ox,oy = term.getCursorPos()
1133
		term.setCursorPos(width/2 - (nextspace)/2 + offset, height + inc)
1134
		inc = inc + 1
1135
		term.write(string.sub(msg, ops, nextspace + ops - 1))
1136
		ops = ops + nextspace
1137
	end
1138
	term.setCursorPos(width/2 - #string.sub(msg, ops)/2 + offset, height + inc)
1139
	term.write(string.sub(msg, ops))
1140
	
1141
	return inc + 1
1142
end
1143
1144
--[[Draws a message that must be clicked on or a key struck to be cleared. No options, so used for displaying
1145
	generic information.
1146
	Params: ctitle:string = The title of the confirm dialogue
1147
			msg:string = The message displayed in the dialogue
1148
	Returns:nil
1149
]]--
1150
local function displayConfirmDialogue(ctitle, msg)
1151
	local dialogoffset = 8
1152
	--We actually print twice- once to get the lines, second time to print proper. Easier this way.
1153
	local lines = wprintOffCenter(msg, 5, w - (dialogoffset+2) * 2, dialogoffset + 2)
1154
	
1155
	term.setCursorPos(dialogoffset, 3)
1156
	term.setBackgroundColour(colours.grey)
1157
	term.setTextColour(colours.lightGrey)
1158
	term.write(string.rep(" ", w - dialogoffset * 2))
1159
	term.setCursorPos(dialogoffset + (w - dialogoffset * 2)/2 - #ctitle/2, 3)
1160
	term.write(ctitle)
1161
	term.setTextColour(colours.grey)
1162
	term.setBackgroundColour(colours.lightGrey)
1163
	term.setCursorPos(dialogoffset, 4)
1164
	term.write(string.rep(" ", w - dialogoffset * 2))
1165
	for i=5,5+lines do
1166
		term.setCursorPos(dialogoffset, i) 
1167
		term.write(" "..string.rep(" ", w - (dialogoffset) * 2 - 2).." ")
1168
	end
1169
	wprintOffCenter(msg, 5, w - (dialogoffset+2) * 2, dialogoffset + 2)
1170
	
1171
	--In the event of a message, the player hits anything to continue
1172
	while true do
1173
		local id,key = os.pullEvent()
1174
		if id == "timer" then updateTimer(key);
1175
		elseif id == "key" or id == "mouse_click" or id == "mouse_drag" then break end
1176
	end
1177
end
1178
1179
--[[Produces a nice dropdown menu based on a table of strings. Depending on the position, this will auto-adjust the position
1180
	of the menu drawn, and allows nesting of menus and sub menus. Clicking anywhere outside the menu will cancel and return nothing
1181
	Params: x:int = the x position the menu should be displayed at
1182
			y:int = the y position the menu should be displayed at
1183
			options:table = the list of options available to the user, as strings or submenus (tables of strings, with a name parameter)
1184
	Returns:string the selected menu option.
1185
]]--
1186
local function displayDropDown(x, y, options)
1187
	inDropDown = true
1188
	--Figures out the dimensions of our thing
1189
	local longestX = #options.name
1190
	for i=1,#options do
1191
		local currVal = options[i]
1192
		if type(currVal) == "table" then currVal = currVal.name end
1193
		
1194
		longestX = math.max(longestX, #currVal)
1195
	end
1196
	local xOffset = math.max(0, longestX - ((w-2) - x) + 1)
1197
	local yOffset = math.max(0, #options - ((h-1) - y))
1198
	
1199
	local clickTimes = 0
1200
	local tid = nil
1201
	local selection = nil
1202
	while clickTimes < 2 do
1203
		drawCanvas()
1204
		drawInterface()
1205
		
1206
		term.setCursorPos(x-xOffset,y-yOffset)
1207
		term.setBackgroundColour(colours.grey)
1208
		term.setTextColour(colours.lightGrey)
1209
		term.write(options.name..string.rep(" ", longestX-#options.name + 2))
1210
	
1211
		for i=1,#options do
1212
			term.setCursorPos(x-xOffset, y-yOffset+i)
1213-
		for y,line in pairs(frames[i]) do
1213+
1214-
			newlines[y-toplim+newy] = { }
1214+
1215-
			for x,char in pairs(line) do
1215+
1216-
				newlines[y-toplim+newy][x-leflim+newx] = char
1216+
1217
				term.setBackgroundColour(colours.lightGrey)
1218
				term.setTextColour(colours.grey)
1219
			end
1220
			local currVal = options[i]
1221
			if type(currVal) == "table" then 
1222
				term.write(currVal.name..string.rep(" ", longestX-#currVal.name + 1))
1223
				term.setBackgroundColour(colours.grey)
1224
				term.setTextColour(colours.lightGrey)
1225
				term.write(">")
1226
			else
1227
				term.write(currVal..string.rep(" ", longestX-#currVal + 2))
1228
			end
1229
		end
1230
		
1231
		local id, p1, p2, p3 = os.pullEvent()
1232
		if id == "timer" then
1233
			if p1 == tid then 
1234
				clickTimes = clickTimes + 1
1235
				if clickTimes > 2 then 
1236
					break
1237
				else 
1238
					tid = os.startTimer(0.1) 
1239
				end
1240
			else 
1241
				updateTimer(p1) 
1242
				drawCanvas()
1243
				drawInterface()
1244
			end
1245
		elseif id == "mouse_click" then
1246
			if p2 >=x-xOffset and p2 <= x-xOffset + longestX + 1 and p3 >= y-yOffset+1 and p3 <= y-yOffset+#options then
1247
				selection = p3-(y-yOffset)
1248
				tid = os.startTimer(0.1)
1249
			else
1250
				selection = ""
1251
				break
1252
			end
1253
		end
1254
	end
1255
	
1256
	if type(selection) == "number" then
1257
		selection = options[selection]
1258
	end
1259
	
1260
	if type(selection) == "string" then 
1261
		inDropDown = false
1262
		return selection
1263
	elseif type(selection) == "table" then 
1264
		return displayDropDown(x, y, selection)
1265
	end
1266
end
1267
1268
--[[A custom io.read() function with a few differences- it limits the number of characters being printed,
1269
	waits a 1/100th of a second so any keys still in the event library are removed before input is read and
1270
	the timer for the selectionrectangle is continuously updated during the process.
1271
	Params: lim:int = the number of characters input is allowed
1272
	Returns:string the inputted string, trimmed of leading and tailing whitespace
1273
]]--
1274
local function readInput(lim)
1275
	term.setCursorBlink(true)
1276
1277
	local inputString = ""
1278
	if not lim or type(lim) ~= "number" or lim < 1 then lim = w - ox end
1279
	local ox,oy = term.getCursorPos()
1280
	--We only get input from the footer, so this is safe. Change if recycling
1281
	term.setBackgroundColour(colours.lightGrey)
1282
	term.setTextColour(colours.grey)
1283
	term.write(string.rep(" ", lim))
1284
	term.setCursorPos(ox, oy)
1285
	--As events queue immediately, we may get an unwanted key... this will solve that problem
1286
	local inputTimer = os.startTimer(0.01)
1287
	local keysAllowed = false
1288
	
1289
	while true do
1290
		local id,key = os.pullEvent()
1291
		
1292
		if keysAllowed then
1293
			if id == "key" and key == 14 and #inputString > 0 then
1294
				inputString = string.sub(inputString, 1, #inputString-1)
1295
				term.setCursorPos(ox + #inputString,oy)
1296
				term.write(" ")
1297
			elseif id == "key" and key == 28 and inputString ~= string.rep(" ", #inputString) then 
1298
				break
1299
			elseif id == "key" and key == keys.leftCtrl then
1300
				return ""
1301
			elseif id == "char" and #inputString < lim then
1302
				inputString = inputString..key
1303
			end
1304
		end
1305
		
1306
		if id == "timer" then
1307
			if key == inputTimer then 
1308
				keysAllowed = true
1309
			else
1310
				updateTimer(key)
1311
				drawCanvas()
1312
				drawInterface()
1313
				term.setBackgroundColour(colours.lightGrey)
1314
				term.setTextColour(colours.grey)
1315
			end
1316
		end
1317
		term.setCursorPos(ox,oy)
1318
		term.write(inputString)
1319
		term.setCursorPos(ox + #inputString, oy)
1320
	end
1321
	
1322
	while string.sub(inputString, 1, 1) == " " do
1323
		inputString = string.sub(inputString, 2, #inputString)
1324
	end
1325
	while string.sub(inputString, #inputString, #inputString) == " " do
1326
		inputString = string.sub(inputString, 1, #inputString-1)
1327
	end
1328
	term.setCursorBlink(false)
1329
	
1330
	return inputString
1331
end
1332
1333
--[[  
1334
			Section: Image tools 
1335
]]--
1336
1337
1338
--[[Copies all pixels beneath the selection rectangle into the image buffer. Empty buffers are converted to nil.
1339
	Params: removeImage:bool = true if the image is to be erased after copying, false otherwise
1340
	Returns:nil
1341
]]--
1342
local function copyToBuffer(removeImage)
1343
	buffer = { width = selectrect.x2 - selectrect.x1 + 1, height = selectrect.y2 - selectrect.y1 + 1, contents = { } }
1344
	
1345
	local containsSomething = false
1346
	for y=1,buffer.height do
1347
		buffer.contents[y] = { }
1348
		local f,l = sFrame,sFrame
1349
		if record then f,l = 1, framecount end
1350
		
1351
		for fra = f,l do
1352
			if frames[fra][selectrect.y1 + y - 1] then
1353
				for x=1,buffer.width do
1354
					buffer.contents[y][x] = frames[sFrame][selectrect.y1 + y - 1][selectrect.x1 + x - 1]
1355
					if removeImage then frames[fra][selectrect.y1 + y - 1][selectrect.x1 + x - 1] = nil end
1356
					if buffer.contents[y][x] then containsSomething = true end
1357
				end
1358
			end
1359
		end
1360
	end
1361
	--I don't classify an empty buffer as a real buffer- confusing to the user.
1362
	if not containsSomething then buffer = nil end
1363
end
1364
1365
--[[Replaces all pixels under the selection rectangle with the image buffer (or what can be seen of it). Record-dependent.
1366
	Params: removeBuffer:bool = true if the buffer is to be emptied after copying, false otherwise
1367
	Returns:nil
1368
]]--
1369
local function copyFromBuffer(removeBuffer)
1370
	if not buffer then return end
1371
1372
	for y = 1, math.min(buffer.height,selectrect.y2-selectrect.y1+1) do
1373
		local f,l = sFrame, sFrame
1374
		if record then f,l = 1, framecount end
1375
		
1376
		for fra = f,l do
1377
			if not frames[fra][selectrect.y1+y-1] then frames[fra][selectrect.y1+y-1] = { } end
1378
			for x = 1, math.min(buffer.width,selectrect.x2-selectrect.x1+1) do
1379
				frames[fra][selectrect.y1+y-1][selectrect.x1+x-1] = buffer.contents[y][x]
1380
			end
1381
		end
1382
	end
1383
	
1384
	if removeBuffer then buffer = nil end
1385
end
1386
1387
--[[Moves the entire image (or entire animation) to the specified coordinates. Record-dependent.
1388
	Params: newx:int = the X coordinate to move the image to
1389
			newy:int = the Y coordinate to move the image to
1390
	Returns:nil
1391
]]--
1392
local function moveImage(newx,newy)
1393
	if not leflim or not toplim then return end
1394
	if newx <=0 or newy <=0 then return end
1395
	local f,l = sFrame,sFrame
1396
	if record then f,l = 1,framecount end
1397
	
1398
	for i=f,l do
1399
		local newlines = { }
1400
		for y=toplim,botlim do
1401
			local line = frames[i][y]
1402
			if line then
1403
				newlines[y-toplim+newy] = { }
1404
				for x,char in pairs(line) do
1405
					newlines[y-toplim+newy][x-leflim+newx] = char
1406
				end
1407
			end
1408
		end
1409
		--Exceptions that allow us to move the text as well
1410
		if textEnabled then
1411
			newlines.text = { }
1412
			for y=toplim,botlim do
1413
				local line = frames[i].text[y]
1414
				if line then
1415
					newlines.text[y-toplim+newy] = { }
1416
					for x,char in pairs(line) do
1417
						newlines.text[y-toplim+newy][x-leflim+newx] = char
1418
					end
1419
				end
1420
			end
1421
			
1422
			newlines.textcol = { }
1423
			for y=toplim,botlim do
1424
				local line = frames[i].textcol[y]
1425
				if line then
1426
					newlines.textcol[y-toplim+newy] = { }
1427
					for x,char in pairs(line) do
1428
						newlines.textcol[y-toplim+newy][x-leflim+newx] = char
1429
					end
1430
				end
1431
			end
1432
		end
1433
		
1434
		frames[i] = newlines
1435
	end
1436
end
1437
1438
--[[Prompts the user to clear the current frame or all frames. Record-dependent.,
1439
	Params: none
1440
	Returns:nil
1441
]]--
1442
local function clearImage()
1443
	inMenu = true
1444
	if not animated then
1445
		drawMessage("Clear image? Y/N: ")
1446
	elseif record then
1447
		drawMessage("Clear ALL frames? Y/N: ")
1448
	else
1449
		drawMessage("Clear current frame? Y/N :")
1450
	end
1451
	if string.find(string.upper(readInput(1)), "Y") then
1452
		local f,l = sFrame,sFrame
1453
		if record then f,l = 1,framecount end
1454
		
1455
		for i=f,l do
1456
			frames[i] = { }
1457
		end
1458
	end
1459
	inMenu = false
1460
end
1461
1462
--[[A recursively called method (watch out for big calls!) in which every pixel of a set colour is
1463
	changed to another colour. Does not work on the nil colour, for obvious reasons.
1464
	Params: x:int = The X coordinate of the colour to flood-fill
1465
			y:int = The Y coordinate of the colour to flood-fill
1466
			targetColour:colour = the colour that is being flood-filled
1467
			newColour:colour = the colour with which to replace the target colour
1468
	Returns:nil
1469
]]--
1470
local function floodFill(x, y, targetColour, newColour)
1471
	if not newColour or not targetColour then return end
1472
	local nodeList = { }
1473
	
1474
	table.insert(nodeList, {x = x, y = y})
1475
	
1476
	while #nodeList > 0 do
1477
		local node = nodeList[1]
1478
		if frames[sFrame][node.y] and frames[sFrame][node.y][node.x] == targetColour then
1479
			frames[sFrame][node.y][node.x] = newColour
1480
			table.insert(nodeList, { x = node.x + 1, y = node.y})
1481
			table.insert(nodeList, { x = node.x, y = node.y + 1})
1482
			if x > 1 then table.insert(nodeList, { x = node.x - 1, y = node.y}) end
1483
			if y > 1 then table.insert(nodeList, { x = node.x, y = node.y - 1}) end
1484
		end
1485
		table.remove(nodeList, 1)
1486
	end
1487
end
1488
1489
--[[  
1490
			Section: Animation Tools  
1491
]]--
1492
1493
--[[Enters play mode, allowing the animation to play through. Interface is restricted to allow this,
1494
	and method only leaves once the player leaves play mode.
1495
	Params: none
1496
	Returns:nil
1497
]]--
1498
local function playAnimation()
1499
	state = "play"
1500
	selectedrect = nil
1501
	
1502
	local animt = os.startTimer(animtime)
1503
	repeat
1504
		drawCanvas()
1505
		drawInterface()
1506
		
1507
		local id,key,_,y = os.pullEvent()
1508
		
1509
		if id=="timer" then
1510
			if key == animt then
1511
				animt = os.startTimer(animtime)
1512
				sFrame = (sFrame % framecount) + 1
1513
			else
1514
				updateTimer(key)
1515
			end
1516
		elseif id=="key" then
1517
			if key == keys.comma and animtime > 0.1 then animtime = animtime - 0.05
1518
			elseif key == keys.period and animtime < 0.5 then animtime = animtime + 0.05
1519
			elseif key == keys.space then state = "paint" end
1520
		elseif id=="mouse_click" and y == h then
1521
			state = "paint"
1522
		end
1523
	until state ~= "play"
1524
	os.startTimer(0.5)
1525
end
1526
1527
--[[Changes the selected frame (sFrame) to the chosen frame. If this frame is above the framecount,
1528
	additional frames are created with a copy of the image on the selected frame.
1529
	Params: newframe:int = the new frame to move to
1530
	Returns:nil
1531
]]--
1532
local function changeFrame(newframe)
1533
	inMenu = true
1534
	if not tonumber(newframe) then
1535
		term.setCursorPos(1,h)
1536
		term.setBackgroundColour(colours.lightGrey)
1537
		term.setTextColour(colours.grey)
1538
		term.clearLine()
1539
	
1540
		term.write("Go to frame: ")
1541
		newframe = tonumber(readInput(2))
1542
		if not newframe or newframe <= 0 then
1543
			inMenu = false
1544
			return 
1545
		end
1546
	elseif newframe <= 0 then return end
1547
	
1548
	if newframe > framecount then
1549
		for i=framecount+1,newframe do
1550
			frames[i] = {}
1551
			for y,line in pairs(frames[sFrame]) do
1552
				frames[i][y] = { }
1553
				for x,v in pairs(line) do
1554
					frames[i][y][x] = v
1555
				end
1556
			end
1557
		end
1558
		framecount = newframe
1559
	end
1560
	sFrame = newframe
1561
	inMenu = false
1562
end
1563
1564
--[[Removes every frame leading after the frame passed in
1565
	Params: frame:int the non-inclusive lower bounds of the delete
1566
	Returns:nil
1567
]]--
1568
local function removeFramesAfter(frame)
1569
	inMenu = true
1570
	if frame==framecount then return end
1571
	drawMessage("Remove frames "..(frame+1).."/"..framecount.."? Y/N :")
1572
	local answer = string.upper(readInput(1))
1573
	
1574
	if string.find(answer, string.upper("Y")) ~= 1 then 
1575
		inMenu = false
1576
		return 
1577
	end
1578
	
1579
	for i=frame+1, framecount do
1580
		frames[i] = nil
1581
	end
1582
	framecount = frame
1583
	inMenu = false
1584
end
1585
1586
--[[
1587
			Section: Printing Tools
1588
]]--
1589
1590
--[[Constructs a new facing to the left of the current facing
1591
	Params: curx:number = The facing on the X axis
1592
			curz:number = The facing on the Z axis
1593
			hand:string = The hand of the axis ("right" or "left")
1594
	Returns:number,number = the new facing on the X and Z axis after a left turn
1595
]]--
1596
local function getLeft(curx, curz)
1597
	local hand = "left"
1598
	if layering == "up" then hand = "right" end
1599
	
1600
	if hand == "right" then
1601
		if curx == 1 then return 0,-1 end
1602
		if curx == -1 then return 0,1 end
1603
		if curz == 1 then return 1,0 end
1604
		if curz == -1 then return -1,0 end
1605
	else
1606
		if curx == 1 then return 0,1 end
1607
		if curx == -1 then return 0,-1 end
1608
		if curz == 1 then return -1,0 end
1609
		if curz == -1 then return 1,0 end
1610
	end
1611
end
1612
1613
--[[Constructs a new facing to the right of the current facing
1614
	Params: curx:number = The facing on the X axis
1615
			curz:number = The facing on the Z axis
1616
			hand:string = The hand of the axis ("right" or "left")
1617
	Returns:number,number = the new facing on the X and Z axis after a right turn
1618
]]--
1619
local function getRight(curx, curz)
1620
	local hand = "left"
1621
	if layering == "up" then hand = "right" end
1622
	
1623
	if hand == "right" then
1624
		if curx == 1 then return 0,1 end
1625
		if curx == -1 then return 0,-1 end
1626
		if curz == 1 then return -1,0 end
1627
		if curz == -1 then return 1,0 end
1628
	else
1629
		if curx == 1 then return 0,-1 end
1630
		if curx == -1 then return 0,1 end
1631
		if curz == 1 then return 1,0 end
1632
		if curz == -1 then return -1,0 end
1633
	end
1634
end
1635
1636
1637
--[[Sends out a rednet signal requesting local printers, and will listen for any responses. Printers found are added to the
1638
	printerList (for ID's) and printerNames (for names)
1639
	Params: nil
1640
	Returns:nil
1641
]]--
1642
local function locatePrinters()
1643
	printerList = { }
1644
	printerNames = { name = "Printers" }
1645
	local oldState = state
1646
	state = "Locating printers, please wait...   "
1647
	drawCanvas()
1648
	drawInterface()
1649
	state = oldState
1650
	
1651
	local modemOpened = false
1652
	for k,v in pairs(rs.getSides()) do
1653
		if peripheral.isPresent(v) and peripheral.getType(v) == "modem" then
1654
			rednet.open(v)
1655
			modemOpened = true
1656
			break
1657
		end
1658
	end
1659
	
1660
	if not modemOpened then
1661
		displayConfirmDialogue("Modem not found!", "No modem peripheral. Must have network modem to locate printers.")
1662
		return false
1663
	end
1664
	
1665
	rednet.broadcast("$3DPRINT IDENTIFY")
1666
	
1667
	while true do
1668
		local id, msg = rsTimeReceive(1)
1669
		
1670
		if not id then break end
1671
		if string.find(msg, "$3DPRINT IDACK") == 1 then
1672
			msg = string.gsub(msg, "$3DPRINT IDACK ", "")
1673
			table.insert(printerList, id)
1674
			table.insert(printerNames, msg)
1675
		end
1676
	end
1677
	
1678
	if #printerList == 0 then
1679
		displayConfirmDialogue("Printers not found!", "No active printers found in proximity of this computer.")
1680
		return false
1681
	else
1682
		return true
1683
	end
1684
end
1685
1686
--[[Sends a request to the printer. Waits on a response and updates the state of the application accordingly.
1687
	Params: command:string the command to send
1688
			param:string a parameter to send, if any
1689
	Returns:nil
1690
]]--
1691
local function sendPC(command,param)
1692
	local msg = "$PC "..command
1693
	if param then msg = msg.." "..param end
1694
	rednet.send(printerList[selectedPrinter], msg)
1695
	
1696
	while true do
1697
		local id,key = rsTimeReceive()
1698
		if id == printerList[selectedPrinter] then
1699
			if key == "$3DPRINT ACK" then
1700
				break
1701
			elseif key == "$3DPRINT DEP" then
1702
				displayConfirmDialogue("Printer Empty", "The printer has exhasted a material. Please refill slot "..param..
1703
					", and click this message when ready to continue.")
1704
				rednet.send(printerList[selectedPrinter], msg)
1705
			elseif key == "$3DPRINT OOF" then
1706
				displayConfirmDialogue("Printer Out of Fuel", "The printer has no fuel. Please replace the material "..
1707
					"in slot 1 with a fuel source, then click this message.")
1708
				rednet.send(printerList[selectedPrinter], "$PC SS 1")
1709
				id,key = rsTimeReceive()
1710
				rednet.send(printerList[selectedPrinter], "$PC RF")
1711
				id,key = rsTimeReceive()
1712
				rednet.send(printerList[selectedPrinter], msg)
1713
			end
1714
		end
1715
	end
1716
	
1717
	--Changes to position are handled after the event has been successfully completed
1718
	if command == "FW" then
1719
		px = px + pfx
1720
		pz = pz + pfz
1721
	elseif command == "BK" then
1722
		px = px - pfx
1723
		pz = pz - pfz
1724
	elseif command == "UP" then
1725
		if layering == "up" then
1726
			py = py + 1
1727
		else 
1728
			py = py - 1
1729
		end
1730
	elseif command == "DW" then
1731
		if layering == "up" then
1732
			py = py - 1
1733
		else 	
1734
			py = py + 1
1735
		end
1736
	elseif command == "TL" then
1737
		pfx,pfz = getLeft(pfx,pfz)
1738
	elseif command == "TR" then
1739
		pfx,pfz = getRight(pfx,pfz)
1740
	elseif command == "TU" then
1741
		pfx = -pfx
1742
		pfz = -pfz
1743
	end
1744
	
1745
	drawCanvas()
1746
	drawInterface()
1747
end
1748
1749
--[[A printing function that commands the printer to turn to face the desired direction, if it is not already doing so
1750
	Params: desx:number = the normalized x direction to face
1751
			desz:number = the normalized z direction to face
1752
	Returns:nil
1753
]]--
1754
local function turnToFace(desx,desz)
1755
	if desx ~= 0 then
1756
		if pfx ~= desx then
1757
			local temppfx,_ = getLeft(pfx,pfz)
1758
			if temppfx == desx then
1759
				sendPC("TL")
1760
			elseif temppfx == -desx then
1761
				sendPC("TR")
1762
			else
1763
				sendPC("TU")
1764
			end
1765
		end
1766
	else
1767
		print("on the z axis")
1768
		if pfz ~= desz then
1769
			local _,temppfz = getLeft(pfx,pfz)
1770
			if temppfz == desz then
1771
				sendPC("TL")
1772
			elseif temppfz == -desz then
1773
				sendPC("TR")
1774
			else
1775
				sendPC("TU")
1776
			end
1777
		end
1778
	end
1779
end
1780
1781
--[[Performs the print
1782
	Params: nil
1783
	Returns:nil
1784
]]--
1785
local function performPrint()
1786
	state = "active print"
1787
	if layering == "up" then
1788
		--An up layering starts our builder bot on the bottom left corner of our build
1789
		px,py,pz = leflim, 0, botlim + 1
1790
		pfx,pfz = 0,-1
1791
		
1792
		--We move him forward and up a bit from his original position.
1793
		sendPC("FW")
1794
		sendPC("UP")
1795
		--For each layer that needs to be completed, we go up by one each time
1796
		for layers=1,#frames do
1797
			--We first decide if we're going forwards or back, depending on what side we're on
1798
			local rowbot,rowtop,rowinc = nil,nil,nil
1799
			if pz == botlim then
1800
				rowbot,rowtop,rowinc = botlim,toplim,-1
1801
			else
1802
				rowbot,rowtop,rowinc = toplim,botlim,1
1803
			end
1804
			
1805
			for rows = rowbot,rowtop,rowinc do
1806
				--Then we decide if we're going left or right, depending on what side we're on
1807
				local linebot,linetop,lineinc = nil,nil,nil
1808
				if px == leflim then
1809
					--Facing from the left side has to be easterly- it's changed here
1810
					turnToFace(1,0)
1811
					linebot,linetop,lineinc = leflim,riglim,1
1812
				else
1813
					--Facing from the right side has to be westerly- it's changed here
1814
					turnToFace(-1,0)
1815
					linebot,linetop,lineinc = riglim,leflim,-1
1816
				end
1817
				
1818
				for lines = linebot,linetop,lineinc do
1819
					--We move our turtle forward, placing the right material at each step
1820
					local material = frames[py][pz][px]
1821
					if material then
1822
						material = math.log10(frames[py][pz][px])/math.log10(2) + 1
1823
						sendPC("SS", material)
1824
						sendPC("PD")
1825
					end
1826
					if lines ~= linetop then
1827
						sendPC("FW")
1828
					end
1829
				end
1830
				
1831
				--The printer then has to do a U-turn, depending on which way he's facing and
1832
				--which way he needs to go
1833
				local temppfx,temppfz = getLeft(pfx,pfz)
1834
				if temppfz == rowinc and rows ~= rowtop then
1835
					sendPC("TL")
1836
					sendPC("FW")
1837
					sendPC("TL")
1838
				elseif temppfz == -rowinc and rows ~= rowtop then
1839
					sendPC("TR")
1840
					sendPC("FW")
1841
					sendPC("TR")
1842
				end
1843
			end
1844
			--Now at the end of a run he does a 180 and moves up to begin the next part of the print
1845
			sendPC("TU")
1846
			if layers ~= #frames then
1847
				sendPC("UP")
1848
			end
1849
		end
1850
		--All done- now we head back to where we started.
1851
		if px ~= leflim then
1852
			turnToFace(-1,0)
1853
			while px ~= leflim do
1854
				sendPC("FW")
1855
			end
1856
		end
1857
		if pz ~= botlim then
1858
			turnToFace(0,-1)
1859
			while pz ~= botlim do
1860
				sendPC("BK")
1861
			end
1862
		end
1863
		turnToFace(0,-1)
1864
		sendPC("BK")
1865
		while py > 0 do
1866
			sendPC("DW")
1867
		end
1868
	else
1869
		--The front facing is at the top-left corner, facing south not north
1870
		px,py,pz = leflim, botlim, 1
1871
		pfx,pfz = 0,1
1872
		--We move the printer to the last layer- he prints from the back forwards
1873
		while pz < #frames do
1874
			sendPC("FW")
1875
		end
1876
		
1877
		--For each layer in the frame we build our wall, the move back
1878
		for layers = 1,#frames do
1879
			--We first decide if we're going left or right based on our position
1880
			local rowbot,rowtop,rowinc = nil,nil,nil
1881
			if px == leflim then
1882
				rowbot,rowtop,rowinc = leflim,riglim,1
1883
			else
1884
				rowbot,rowtop,rowinc = riglim,leflim,-1
1885
			end
1886
			
1887
			for rows = rowbot,rowtop,rowinc do
1888
				--Then we decide if we're going up or down, depending on our given altitude
1889
				local linebot,linetop,lineinc = nil,nil,nil
1890
				if py == botlim then
1891
					linebot,linetop,lineinc = botlim,toplim,-1
1892
				else
1893
					linebot,linetop,lineinc = toplim,botlim,1
1894
				end
1895
				
1896
				for lines = linebot,linetop,lineinc do
1897
				--We move our turtle up/down, placing the right material at each step
1898
					local material = frames[pz][py][px]
1899
					if material then
1900
						material = math.log10(frames[pz][py][px])/math.log10(2) + 1
1901
						sendPC("SS", material)
1902
						sendPC("PF")
1903
					end
1904
					if lines ~= linetop then
1905
						if lineinc == 1 then sendPC("DW")
1906
						else sendPC("UP") end
1907
					end
1908
				end
1909
					
1910
				if rows ~= rowtop then
1911
					turnToFace(rowinc,0)
1912
					sendPC("FW")
1913
					turnToFace(0,1)
1914
				end
1915
			end
1916
			
1917
			if layers ~= #frames then
1918
				sendPC("TU")
1919
				sendPC("FW")
1920
				sendPC("TU")
1921
			end
1922
		end
1923
		--He's easy to reset
1924
		while px ~= leflim do
1925
			turnToFace(-1,0)
1926
			sendPC("FW")
1927
		end
1928
		turnToFace(0,1)
1929
	end
1930
	
1931
	sendPC("DE")
1932
	
1933
	displayConfirmDialogue("Print complete", "The 3D print was successful.")
1934
end
1935
1936
--[[  
1937
			Section: Interface  
1938
]]--
1939
1940
--[[Runs the printing interface. Allows users to find/select a printer, the style of printing to perform and to begin the operation
1941
	Params: none
1942
	Returns:boolean true if printing was started, false otherwse
1943
]]--
1944
local function runPrintInterface()
1945
	calculateMaterials()
1946
	--There's nothing on canvas yet!
1947
	if not botlim then
1948
		displayConfirmDialogue("Cannot Print Empty Canvas", "There is nothing on canvas that "..
1949
				"can be printed, and the operation cannot be completed.")
1950
		return false
1951
	end
1952
	--No printers nearby
1953
	if not locatePrinters() then
1954
		return false
1955
	end
1956
	
1957
	layering = "up"
1958
	requirementsDisplayed = false
1959
	selectedPrinter = 1
1960
	while true do
1961
		drawCanvas()
1962
		term.setBackgroundColour(colours.lightGrey)
1963
		for i=1,10 do
1964
			term.setCursorPos(1,i)
1965
			term.clearLine()
1966
		end
1967
		drawInterface()
1968
		term.setBackgroundColour(colours.lightGrey)
1969
		term.setTextColour(colours.black)
1970
		
1971
		local msg = "3D Printing"
1972
		term.setCursorPos(w/2-#msg/2 - 2, 1)
1973
		term.write(msg)
1974
		term.setBackgroundColour(colours.grey)
1975
		term.setTextColour(colours.lightGrey)
1976
		if(requirementsDisplayed) then
1977
			msg = "Count:"
1978
		else
1979
			msg = " Slot:"
1980
		end
1981
		term.setCursorPos(w-3-#msg, 1)
1982
		term.write(msg)
1983
		term.setBackgroundColour(colours.lightGrey)
1984
		term.setTextColour(colours.black)
1985
		
1986
		term.setCursorPos(7, 2)
1987
		term.write("Layering")
1988
		drawPictureTable(layerUpIcon, 3, 3, colours.white)
1989
		drawPictureTable(layerForwardIcon, 12, 3, colours.white)
1990
		if layering == "up" then
1991
			term.setBackgroundColour(colours.red)
1992
		else
1993
			term.setBackgroundColour(colours.lightGrey)
1994
		end
1995
		term.setCursorPos(3, 9)
1996
		term.write("Upwards")
1997
		if layering == "forward" then
1998
			term.setBackgroundColour(colours.red)
1999
		else
2000
			term.setBackgroundColour(colours.lightGrey)
2001
		end
2002
		term.setCursorPos(12, 9)
2003
		term.write("Forward")
2004
		
2005
		term.setBackgroundColour(colours.lightGrey)
2006
		term.setTextColour(colours.black)
2007
		term.setCursorPos(31, 2)
2008
		term.write("Printer ID")
2009
		term.setCursorPos(33, 3)
2010
		if #printerList > 1 then
2011
			term.setBackgroundColour(colours.grey)
2012
			term.setTextColour(colours.lightGrey)
2013
		else
2014
			term.setTextColour(colours.red)
2015
		end
2016
		term.write(" "..printerNames[selectedPrinter].." ")
2017
		
2018
		term.setBackgroundColour(colours.grey)
2019
		term.setTextColour(colours.lightGrey)
2020
		term.setCursorPos(25, 10)
2021
		term.write(" Cancel ")
2022
		term.setCursorPos(40, 10)
2023
		term.write(" Print ")
2024
		
2025
		local id, p1, p2, p3 = os.pullEvent()
2026
		
2027
		if id == "timer" then
2028
			updateTimer(p1)
2029
		elseif id == "mouse_click" then
2030
			--Layering Buttons
2031
			if p2 >= 3 and p2 <= 9 and p3 >= 3 and p3 <= 9 then
2032
				layering = "up"
2033
			elseif p2 >= 12 and p2 <= 18 and p3 >= 3 and p3 <= 9 then
2034
				layering = "forward"
2035
			--Count/Slot
2036
			elseif p2 >= w - #msg - 3 and p2 <= w - 3 and p3 == 1 then
2037
				requirementsDisplayed = not requirementsDisplayed
2038
			--Printer ID
2039
			elseif p2 >= 33 and p2 <= 33 + #printerNames[selectedPrinter] and p3 == 3 and #printerList > 1 then
2040
				local chosenName = displayDropDown(33, 3, printerNames)
2041
				for i=1,#printerNames do
2042
					if printerNames[i] == chosenName then
2043
						selectedPrinter = i
2044
						break;
2045
					end
2046
				end
2047
			--Print and Cancel
2048
			elseif p2 >= 25 and p2 <= 32 and p3 == 10 then
2049
				break
2050
			elseif p2 >= 40 and p2 <= 46 and p3 == 10 then
2051
				rednet.send(printerList[selectedPrinter], "$3DPRINT ACTIVATE")
2052
				ready = false
2053
				while true do
2054-
			if p1==keys.leftCtrl then
2054+
2055
					
2056
					if id == printerList[selectedPrinter] and msg == "$3DPRINT ACTACK" then
2057
						ready = true
2058
						break
2059
					end
2060
				end
2061
				if ready then
2062
					performPrint()
2063
					break
2064
				else
2065
					displayConfirmDialogue("Printer Didn't Respond", "The printer didn't respond to the activation command. Check to see if it's online")
2066
				end
2067
			end
2068
		end
2069
	end
2070
	state = "paint"
2071
end
2072
2073
--[[This function changes the current paint program to another tool or mode, depending on user input. Handles
2074
	any necessary changes in logic involved in that.
2075
	Params: mode:string = the name of the mode to change to
2076
	Returns:nil
2077
]]--
2078
local function performSelection(mode)
2079
	if not mode or mode == "" then return
2080
	
2081
	elseif mode == "help" then
2082
		drawHelpScreen()
2083
		
2084
	elseif mode == "blueprint on" then
2085
		blueprint = true
2086
		ddModes[2][3] = "blueprint off"
2087
		
2088
	elseif mode == "blueprint off" then
2089
		blueprint = false
2090
		ddModes[2][3] = "blueprint on"
2091
		
2092
	elseif mode == "layers on" then
2093
		layerDisplay = true
2094
		ddModes[2][4] = "layers off"
2095
	
2096
	elseif mode == "layers off" then
2097
		layerDisplay = false
2098
		ddModes[2][4] = "layers on"
2099
	
2100
	elseif mode == "direction on" then
2101-
					moveImage(leflim-1,toplim)
2101+
2102
		ddModes[2][5] = "direction off"
2103
		
2104
	elseif mode == "direction off" then
2105
		printDirection = false
2106
		ddModes[2][5] = "direction on"
2107
	
2108
	elseif mode == "go to" then
2109-
					moveImage(leflim+1,toplim)
2109+
2110
	
2111
	elseif mode == "remove" then
2112
		removeFramesAfter(sFrame)
2113
	
2114
	elseif mode == "play" then
2115
		playAnimation()
2116
		
2117-
					moveImage(leflim,toplim-1)
2117+
2118
		if selectrect and selectrect.x1 ~= selectrect.x2 then
2119
			copyToBuffer(false)
2120
		end
2121
	
2122
	elseif mode == "cut" then
2123
		if selectrect and selectrect.x1 ~= selectrect.x2 then 
2124
			copyToBuffer(true)
2125-
					moveImage(leflim,toplim+1)
2125+
2126
		
2127
	elseif mode == "paste" then
2128
		if selectrect and selectrect.x1 ~= selectrect.x2 then 
2129
			copyFromBuffer(false)
2130
		end
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+
2136
	elseif mode == "alpha to left" then
2137
		if lSel then alphaC = lSel end
2138
		
2139
	elseif mode == "alpha to right" then
2140
		if rSel then alphaC = rSel end
2141
		
2142
	elseif mode == "record" then
2143
		record = not record
2144
		
2145
	elseif mode == "clear" then
2146
		if state=="select" then buffer = nil
2147
		else clearImage() end
2148
	
2149
	elseif mode == "select" then
2150
		if state=="corner select" or state=="select" then
2151
			state = "paint"
2152
		elseif selectrect and selectrect.x1 ~= selectrect.x2 then
2153
			state = "select"
2154
		else
2155
			state = "corner select" 
2156-
	print("Usage: npaintpro [-a] <path>")
2156+
2157
		
2158
	elseif mode == "print" then
2159
		state = "print"
2160
		runPrintInterface()
2161
		state = "paint"
2162
		
2163
	elseif mode == "save" then
2164
		if animated then saveNFA(sPath)
2165
		elseif textEnabled then saveNFT(sPath)
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
	elseif mode == "exit" then
2169
		isRunning = false
2170
	
2171
	elseif mode ~= state then state = mode
2172
	else state = "paint"
2173
	
2174
	end
2175
end
2176
2177
--[[The main function of the program, reads and handles all events and updates them accordingly. Mode changes,
2178
	painting to the canvas and general selections are done here.
2179
	Params: none
2180
	Returns:nil
2181
]]--
2182
local function handleEvents()
2183
	recttimer = os.startTimer(0.5)
2184
	while isRunning do
2185
		drawCanvas()
2186-
	if not animated and string.find(sPath, ".nfp") ~= #sPath-3 then 
2186+
2187
		
2188
		if state == "text" then
2189
			term.setCursorPos(textCurX - sx, textCurY - sy)
2190
			term.setCursorBlink(true)
2191
		end
2192
		
2193
		local id,p1,p2,p3 = os.pullEvent()
2194
			term.setCursorBlink(false)
2195
		if id=="timer" then
2196
			updateTimer(p1)
2197
		elseif id=="mouse_click" or id=="mouse_drag" then
2198
			if p2 >=w-1 and p3 < #column+1 then
2199
				if p1==1 then lSel = column[p3]
2200
				else rSel = column[p3] end
2201
			elseif p2 >=w-1 and p3==#column+1 then
2202
				if p1==1 then lSel = nil
2203
				else rSel = nil end
2204
			elseif p2==w-1 and p3==h and animated then
2205
				changeFrame(sFrame-1)
2206
			elseif p2==w and p3==h and animated then
2207
				changeFrame(sFrame+1)
2208
			elseif p2 < w-10 and p3==h then
2209
				local sel = displayDropDown(1, h-1, ddModes)
2210
				performSelection(sel)
2211
			elseif p2 < w-1 and p3 <= h-1 then
2212
				if state=="pippette" then
2213
					if p1==1 then
2214
						if frames[sFrame][p3+sy] and frames[sFrame][p3+sy][p2+sx] then
2215
							lSel = frames[sFrame][p3+sy][p2+sx] 
2216
						end
2217
					elseif p1==2 then
2218
						if frames[sFrame][p3+sy] and frames[sFrame][p3+sy][p2+sx] then
2219
							rSel = frames[sFrame][p3+sy][p2+sx] 
2220
						end
2221
					end
2222
				elseif state=="move" then
2223
					updateImageLims(record)
2224
					moveImage(p2,p3)
2225
				elseif state=="flood" then
2226
					if p1 == 1 and lSel and frames[sFrame][p3+sy]  then 
2227
						floodFill(p2,p3,frames[sFrame][p3+sy][p2+sx],lSel)
2228
					elseif p1 == 2 and rSel and frames[sFrame][p3+sy] then 
2229
						floodFill(p2,p3,frames[sFrame][p3+sy][p2+sx],rSel)
2230
					end
2231
				elseif state=="corner select" then
2232
					if not selectrect then
2233
						selectrect = { x1=p2+sx, x2=p2+sx, y1=p3+sy, y2=p3+sy }
2234
					elseif selectrect.x1 ~= p2+sx and selectrect.y1 ~= p3+sy then
2235
						if p2+sx<selectrect.x1 then selectrect.x1 = p2+sx
2236
						else selectrect.x2 = p2+sx end
2237
						
2238
						if p3+sy<selectrect.y1 then selectrect.y1 = p3+sy
2239
						else selectrect.y2 = p3+sy end
2240
						
2241
						state = "select"
2242
					end
2243
				elseif state=="textpaint" then
2244
					local paintCol = lSel
2245
					if p1 == 2 then paintCol = rSel end
2246
					if frames[sFrame].textcol[p3+sy] then
2247
						frames[sFrame].textcol[p3+sy][p2+sx] = paintCol
2248
					end
2249
				elseif state=="text" then
2250
					textCurX = p2 + sx
2251
					textCurY = p3 + sy
2252
				elseif state=="select" then
2253
					if p1 == 1 then
2254
						local swidth = selectrect.x2 - selectrect.x1
2255
						local sheight = selectrect.y2 - selectrect.y1
2256
					
2257
						selectrect.x1 = p2 + sx
2258
						selectrect.y1 = p3 + sy
2259
						selectrect.x2 = p2 + swidth + sx
2260
						selectrect.y2 = p3 + sheight + sy
2261
					elseif p1 == 2 and p2 < w-2 and p3 < h-1 then
2262
						inMenu = true
2263
						local sel = displayDropDown(p2, p3, srModes) 
2264
						inMenu = false
2265
						performSelection(sel)
2266
					end
2267
				else
2268
					local f,l = sFrame,sFrame
2269
					if record then f,l = 1,framecount end
2270
					local bwidth = 0
2271
					if state == "brush" then bwidth = brushsize-1 end
2272
				
2273
					for i=f,l do
2274
						for x = math.max(1,p2+sx-bwidth),p2+sx+bwidth do
2275
							for y = math.max(1,p3+sy-bwidth), p3+sy+bwidth do
2276
								if math.abs(x - (p2+sx)) + math.abs(y - (p3+sy)) <= bwidth then
2277
									if not frames[i][y] then frames[i][y] = {} end
2278
									if p1==1 then frames[i][y][x] = lSel
2279
									else frames[i][y][x] = rSel end
2280
									
2281
									if textEnabled then
2282
										if not frames[i].text[y] then frames[i].text[y] = { } end
2283
										if not frames[i].textcol[y] then frames[i].textcol[y] = { } end
2284
									end
2285
								end
2286
							end
2287
						end
2288
					end
2289
				end
2290
			end
2291
		elseif id=="char" then
2292
			if state=="text" then
2293
				if not frames[sFrame][textCurY] then frames[sFrame][textCurY] = { } end
2294
				if not frames[sFrame].text[textCurY] then frames[sFrame].text[textCurY] = { } end
2295
				if not frames[sFrame].textcol[textCurY] then frames[sFrame].textcol[textCurY] = { } end
2296
				
2297
				if rSel then frames[sFrame][textCurY][textCurX] = rSel end
2298
				if lSel then 
2299
					frames[sFrame].text[textCurY][textCurX] = p1
2300
					frames[sFrame].textcol[textCurY][textCurX] = lSel
2301
				else
2302
					frames[sFrame].text[textCurY][textCurX] = " "
2303
					frames[sFrame].textcol[textCurY][textCurX] = rSel
2304
				end
2305
				
2306
				textCurX = textCurX+1
2307
				if textCurX > w + sx - 2 then sx = textCurX - w + 2 end
2308
			elseif tonumber(p1) then
2309
				if state=="brush" and tonumber(p1) > 1 then
2310
					brushsize = tonumber(p1)
2311
				elseif animated and tonumber(p1) > 0 then
2312
					changeFrame(tonumber(p1))
2313
				end
2314
			end
2315
		elseif id=="key" then
2316
			--Text needs special handlers (all other keyboard shortcuts are of course reserved for typing)
2317
			if state=="text" then
2318
				if p1==keys.backspace and textCurX > 1 then
2319
					textCurX = textCurX-1
2320
					if frames[sFrame].text[textCurY] then
2321
						frames[sFrame].text[textCurY][textCurX] = nil
2322
						frames[sFrame].textcol[textCurY][textCurX] = nil
2323
					end
2324
					if textCurX < sx then sx = textCurX end
2325
				elseif p1==keys.left and textCurX > 1 then
2326
					textCurX = textCurX-1
2327
					if textCurX-1 < sx then sx = textCurX-1 end
2328
				elseif p1==keys.right then
2329
					textCurX = textCurX+1
2330
					if textCurX > w + sx - 2 then sx = textCurX - w + 2 end
2331
				elseif p1==keys.up and textCurY > 1 then
2332
					textCurY = textCurY-1
2333
					if textCurY-1 < sy then sy = textCurY-1 end
2334
				elseif p1==keys.down then
2335
					textCurY = textCurY+1
2336
					if textCurY > h + sy - 1 then sy = textCurY - h + 1 end
2337
				end
2338
			
2339
			elseif p1==keys.leftCtrl then
2340
				local sel = displayDropDown(1, h-1, ddModes[#ddModes]) 
2341
				performSelection(sel)
2342
			elseif p1==keys.leftAlt then
2343
				local sel = displayDropDown(1, h-1, ddModes[1]) 
2344
				performSelection(sel)
2345
			elseif p1==keys.h then 
2346
				performSelection("help")
2347
			elseif p1==keys.x then 
2348
				performSelection("cut")
2349
			elseif p1==keys.c then
2350
				performSelection("copy")
2351
			elseif p1==keys.v then
2352
				performSelection("paste")
2353
			elseif p1==keys.z then
2354
				performSelection("clear")
2355
			elseif p1==keys.s then
2356
				performSelection("select")
2357
			elseif p1==keys.tab then
2358
				performSelection("hide")
2359
			elseif p1==keys.q then
2360
				performSelection("alpha to left")
2361
			elseif p1==keys.w then
2362
				performSelection("alpha to right")
2363
			elseif p1==keys.f then
2364
				performSelection("flood")
2365
			elseif p1==keys.b then
2366
				performSelection("brush")
2367
			elseif p1==keys.m then
2368
				performSelection("move")
2369
			elseif p1==keys.backslash and animated then
2370
				performSelection("record")
2371
			elseif p1==keys.p then
2372
				performSelection("pippette")
2373
			elseif p1==keys.g and animated then
2374
				performSelection("go to")
2375
			elseif p1==keys.period and animated then
2376
				changeFrame(sFrame+1)
2377
			elseif p1==keys.comma and animated then
2378
				changeFrame(sFrame-1)
2379
			elseif p1==keys.r and animated then
2380
				performSelection("remove")
2381
			elseif p1==keys.space and animated then
2382
				performSelection("play")
2383
			elseif p1==keys.t and textEnabled then
2384
				performSelection("text")
2385
				sleep(0.01)
2386
			elseif p1==keys.y and textEnabled then
2387
				performSelection("textpaint")
2388
			elseif p1==keys.left then
2389
				if state == "move" and toplim then
2390
					updateImageLims(record)
2391
					if toplim and leflim then
2392
						moveImage(leflim-1,toplim)
2393
					end
2394
				elseif state=="select" and selectrect.x1 > 1 then
2395
					selectrect.x1 = selectrect.x1-1
2396
					selectrect.x2 = selectrect.x2-1
2397
				elseif sx > 0 then sx=sx-1 end
2398
			elseif p1==keys.right then
2399
				if state == "move" then
2400
					updateImageLims(record)
2401
					if toplim and leflim then
2402
						moveImage(leflim+1,toplim)
2403
					end
2404
				elseif state=="select" then
2405
					selectrect.x1 = selectrect.x1+1
2406
					selectrect.x2 = selectrect.x2+1
2407
				else sx=sx+1 end
2408
			elseif p1==keys.up then
2409
				if state == "move" then
2410
					updateImageLims(record)
2411
					if toplim and leflim then
2412
						moveImage(leflim,toplim-1)
2413
					end
2414
				elseif state=="select" and selectrect.y1 > 1 then
2415
					selectrect.y1 = selectrect.y1-1
2416
					selectrect.y2 = selectrect.y2-1
2417
				elseif sy > 0 then sy=sy-1 end
2418
			elseif p1==keys.down then 
2419
				if state == "move" then
2420
					updateImageLims(record)
2421
					if toplim and leflim then
2422
						moveImage(leflim,toplim+1)
2423
					end
2424
				elseif state=="select" then
2425
					selectrect.y1 = selectrect.y1+1
2426
					selectrect.y2 = selectrect.y2+1
2427
				else sy=sy+1 end
2428
			end
2429
		end
2430
	end
2431
end
2432
2433
--[[
2434
			Section: Main  
2435
]]--
2436
2437
if not term.isColour() then
2438
	print("For colour computers only")
2439
	return
2440
end
2441
2442
--Taken almost directly from edit (for consistency)
2443
local tArgs = {...}
2444
2445
local ca = 1
2446
2447
if tArgs[ca] == "-a" then
2448
	animated = true
2449
	ca = ca + 1
2450
end
2451
2452
if tArgs[ca] == "-t" then
2453
	textEnabled = true
2454
	ca = ca + 1
2455
end
2456
2457
if #tArgs < ca then
2458
	print("Usage: npaintpro [-a,-t] <path>")
2459
	return
2460
end
2461
2462
--Yeah you can't have animated text files YET... I haven't supported that, maybe later?
2463
if animated and textEnabled then
2464
	print("No support for animated text files- cannot have both -a and -t")
2465
end
2466
2467
sPath = shell.resolve(tArgs[ca])
2468
local bReadOnly = fs.isReadOnly(sPath)
2469
if fs.exists(sPath) then
2470
	if fs.isDir(sPath) then
2471
		print("Cannot edit a directory.")
2472
		return
2473
	elseif string.find(sPath, ".nfp") ~= #sPath-3 and string.find(sPath, ".nfa") ~= #sPath-3 and
2474
			string.find(sPath, ".nft") ~= #sPath-3 then
2475
		print("Can only edit .nfp, .nft and .nfa files:",string.find(sPath, ".nfp"),#sPath-3)
2476
		return
2477
	end
2478
	
2479
	if string.find(sPath, ".nfa") == #sPath-3 then
2480
		animated = true
2481
	end
2482
	
2483
	if string.find(sPath, ".nft") == #sPath-3 then
2484
		textEnabled = true
2485
	end	
2486
	
2487
	if string.find(sPath, ".nfp") == #sPath-3 and animated then
2488
		print("Convert to nfa? Y/N")
2489
		if string.find(string.lower(io.read()), "y") then
2490
			local nsPath = string.sub(sPath, 1, #sPath-1).."a"
2491
			fs.move(sPath, nsPath)
2492
			sPath = nsPath
2493
		else
2494
			animated = false
2495
		end
2496
	end
2497
	
2498
	--Again this is possible, I just haven't done it. Maybe I will?
2499
	if textEnabled and (string.find(sPath, ".nfp") == #sPath-3 or string.find(sPath, ".nfa") == #sPath-3) then
2500
		print("Cannot convert to nft")
2501
	end
2502
else
2503
	if not animated and not textEnabled and string.find(sPath, ".nfp") ~= #sPath-3 then 
2504
		sPath = sPath..".nfp"
2505
	elseif animated and string.find(sPath, ".nfa") ~= #sPath-3 then 
2506
		sPath = sPath..".nfa"
2507
	elseif textEnabled and string.find(sPath, ".nft") ~= #sPath-3 then
2508
		sPath = sPath..".nft"
2509
	end
2510
end 
2511
2512
drawLogo()
2513
init()
2514
handleEvents()
2515
2516
term.setBackgroundColour(colours.black)
2517
shell.run("clear")