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