· 5 years ago · Oct 25, 2020, 08:00 PM
1--[[
2
3This script lets people explore three-dimensional cellular automata
4using Golly. Inspired by the work of Carter Bays and his colleagues
5(see http://www.cse.sc.edu/~bays/d4d4d4/).
6
7The 3D drawing is based on code in "Macintosh Graphics in Modula-2"
8by Russell L. Schnapp. Note that orthographic projection is used to
9speed up rendering (because all live cells look the same at a given
10scale and rotation).
11
12Author: Andrew Trevorrow (andrew@trevorrow.com), Feb 2018.
13
14Thanks to Tom Rokicki for optimizing the generating code.
15Thanks to Chris Rowett for optimizing the rendering code and many
16other improvements.
17
18This script uses custom purpose ovtable commmands for increased
19performance when computing the next generation and displaying cells:
20
21nextgen3d compute next generation
22setrule3d set the rule for next generation calculation
23setsize3d set the grid size
24setstep3d set the step size modulus
25settrans3d set the transformation matrix
26displaycells3d display current grid
27setcelltype3d set the cell shape for drawing
28setdepthshading3d set whether depth shading should be used
29setpattern3d set the current pattern
30setselpasact3d set the select, paste and active grids
31sethistory3d set the cell history display mode
32
33--]]
34
35local g = golly()
36-- require "gplus.strict"
37local gp = require "gplus"
38local int = gp.int
39local round = gp.round
40local validint = gp.validint
41local split = gp.split
42
43local ov = g.overlay
44local ovt = g.ovtable
45local op = require "oplus"
46local DrawLine = op.draw_line
47
48local sin = math.sin
49local cos = math.cos
50local sqrt = math.sqrt
51local rand = math.random
52local abs = math.abs
53local min = math.min
54local max = math.max
55local floor = math.floor
56
57math.randomseed(os.time()) -- init seed for math.random
58
59local N = 0 -- current grid size (N*N*N cells)
60local DEFAULTN = 30 -- default grid size
61local MINN = 3 -- minimum grid size
62local MAXN = 100 -- maximum grid size (must be even for BusyBoxes)
63local BORDER = 2 -- space around live cubes
64local MINSIZE = 1+BORDER*2 -- minimum size of empty cells
65local MAXSIZE = 100 -- maximum size of empty cells
66local CELLSIZE = 15 -- initial size of empty cells
67local ZOOMSIZE = CELLSIZE -- used for mouse wheel zoom
68local HALFCELL = CELLSIZE/2.0 -- for drawing mid points of cells
69local LEN = CELLSIZE-BORDER*2 -- edge length of live cubes
70local DEGTORAD = math.pi/180.0 -- converts degrees to radians
71local HISTORYOFF = 0 -- history off
72local DEFAULTHISTORY = 100 -- default history longevity
73local MINHISTORYALPHA = 12 -- minimum history alpha value when fading
74
75-- MIDGRID is used to ensure that rotation occurs about the
76-- mid point of the middle cube in the grid
77local MIDGRID = (N+1-(N%2))*HALFCELL
78local MIDCELL = HALFCELL-MIDGRID
79
80-- these global strings can be changed by a startup script to customize colors
81BACK_COLOR = "rgba 0 0 80 255" -- for background
82LINE_COLOR = "rgba 70 70 100 255" -- for lattice lines (should be close to BACK_COLOR)
83INFO_COLOR = op.white -- for info text
84MSG_COLOR = op.yellow -- for message text
85POINT_COLOR = op.white -- for drawing points
86SELPT_COLOR = "rgba 0 255 0 128" -- for selected points (should be translucent)
87PASTE_COLOR = "rgba 255 0 0 64" -- for paste pattern (must be translucent)
88SELECT_COLOR = "rgba 0 255 0 64" -- for selected cells (ditto)
89ACTIVE_COLOR = "rgba 0 0 255 48" -- for active plane (ditto)
90HISTORY_COLOR = "rgba 240 240 0 64" -- for history cells (ditto)
91PASTE_MENU = "rgba 176 48 48 255" -- for paste menu background (should match PASTE_COLOR)
92SELECT_MENU = "rgba 0 128 0 255" -- for select menu background (should match SELECT_COLOR)
93X_COLOR = op.red -- for front X axes
94Y_COLOR = op.green -- for front Y axes
95Z_COLOR = op.blue -- for front Z axes
96REARX_COLOR = "rgba 128 0 0 255" -- for rear X axes (should be darker than front axes)
97REARY_COLOR = "rgba 0 128 0 255" -- for rear Y axes (ditto)
98REARZ_COLOR = "rgba 0 0 128 255" -- for rear Z axes (ditto)
99START_COLOR = "rgba 0 150 0 255" -- for Start button's background
100SELSTART_COLOR = "rgba 0 90 0 255" -- for Start button's selected background
101STOP_COLOR = "rgba 210 0 0 255" -- for Stop button's background
102SELSTOP_COLOR = "rgba 150 0 0 255" -- for Stop button's selected background
103
104-- following are for BusyBoxes
105EVEN_COLOR = "rgba 255 180 255 255" -- for even cell points and spheres (pale magenta)
106ODD_COLOR = "rgba 140 255 255 255" -- for odd cell points and spheres (pale cyan)
107EVEN_CUBE = "replace *# *#-50 *# *#" -- for even cell cubes (change white to pale magenta)
108ODD_CUBE = "replace *#-110 *# *# *#" -- for odd cell cubes (change white to pale cyan)
109
110local xylattice = {} -- lattice lines between XY axes
111local xzlattice = {} -- lattice lines between XZ axes
112local yzlattice = {} -- lattice lines between YZ axes
113local xaxes = {} -- four X axes
114local yaxes = {} -- four Y axes
115local zaxes = {} -- four Z axes
116
117local update_grid = false -- need to call setpattern3d?
118local grid1 = {} -- sparse 3D matrix with up to N*N*N live cells
119local popcount = 0 -- number of live cells
120local pattname = "untitled" -- pattern name
121local currtitle = "" -- current window title
122local showaxes = true -- draw axes?
123local showlines = true -- draw lattice lines?
124local generating = false -- generate pattern?
125local gencount = 0 -- current generation count
126local stepsize = 1 -- display each generation
127local perc = 20 -- initial percentage for RandomPattern
128local randstring = "20" -- initial string for RandomPattern
129local message = nil -- text message displayed by Refresh if not nil
130local selcount = 0 -- number of selected cells (live or dead)
131local selected = {} -- grid positions of selected cells
132local pastecount = 0 -- number of cells in paste pattern
133local pastepatt = {} -- grid positions of cells in paste pattern
134local drawstate = 1 -- for drawing/erasing cells
135local selstate = true -- for selecting/deselecting cells
136local celltype = "cube" -- draw live cell as cube/sphere/point
137local depthshading = true -- whether using depth shading
138local depthlayers = 64 -- number of shading layers
139local depthrange = 224 -- rgb levels for depth shading
140local mindepth, maxdepth -- minimum and maximum depth (with corner pointing at screen)
141local xyline = { "lines" } -- coordinates for batch line draw
142local xyln = 2 -- index of next position in xyline
143local showhistory = HISTORYOFF -- cell history longevity: 0 = off, >0 = on
144local fadehistory = true -- whether to fade history cells
145
146local active = {} -- grid positions of cells in active plane
147local activeplane = "XY" -- orientation of active plane (XY/XZ/YZ)
148local activepos = 0 -- active plane's cell position along 3rd axis
149local activecell = "" -- "x,y,z" if mouse is inside an active cell
150local prevactive = "" -- previous activecell
151
152-- boundary grid coords for live cells (not always minimal)
153local minx, miny, minz
154local maxx, maxy, maxz
155local minimal_live_bounds = true -- becomes false if a live cell is deleted
156local liveedge = false -- becomes true if there is at least one cell on the grid edge
157
158-- boundary grid coords for selected cells (not always minimal)
159local minselx, minsely, minselz
160local maxselx, maxsely, maxselz
161local minimal_sel_bounds = true -- becomes false if a selected cell is deselected
162
163-- boundary grid coords for the active plane (always minimal)
164local minactivex, minactivey, minactivez
165local maxactivex, maxactivey, maxactivez
166
167-- boundary grid coords for the paste pattern (always minimal)
168local minpastex, minpastey, minpastez
169local maxpastex, maxpastey, maxpastez
170
171-- for undo/redo
172local undostack = {} -- stack of states that can be undone
173local redostack = {} -- stack of states that can be redone
174local startcount = 0 -- starting gencount (can be > 0)
175local startstate = {} -- starting state (used by Reset)
176local dirty = false -- pattern has been modified?
177
178local refcube = {} -- invisible reference cube
179local rotrefz = {} -- Z coords of refcube's rotated vertices
180local rotx = {} -- rotated X coords for 8 vertices
181local roty = {} -- rotated Y coords for 8 vertices
182local rotz = {} -- rotated Z coords for 8 vertices
183local projectedx = {} -- projected X coords for 8 vertices
184local projectedy = {} -- projected Y coords for 8 vertices
185
186-- current transformation matrix
187local xixo, yixo, zixo
188local xiyo, yiyo, ziyo
189local xizo, yizo, zizo
190
191local viewwd, viewht -- current size of viewport
192local ovwd, ovht -- current size of overlay
193local minwd, minht = 900, 400 -- minimum size of overlay
194local midx, midy -- overlay's middle pixel
195
196local mbar -- the menu bar
197local mbarht = 28 -- height of menu bar
198
199-- tool bar controls
200local ssbutton -- Start/Stop
201local s1button -- +1
202local resetbutton -- Reset
203local fitbutton -- Fit
204local undobutton -- Undo
205local redobutton -- Redo
206local helpbutton -- ?
207local exitbutton -- X
208local drawbox -- radio button for draw mode
209local selectbox -- radio button for select mode
210local movebox -- radio button for move mode
211local stepslider -- slider for adjusting stepsize
212
213local pastemenu -- pop-up menu for choosing a paste action
214local selmenu -- pop-up menu for choosing a selection action
215
216local buttonht = 20
217local gap = 10 -- space around buttons
218
219-- tool bar height includes menu bar
220local toolbarht = mbarht+buttonht+gap*2
221
222local drawcursor = "pencil" -- cursor for drawing cells
223local selectcursor = "cross" -- cursor for selecting cells
224local movecursor = "hand" -- cursor for rotating grid
225local currcursor = movecursor -- current cursor
226local arrow_cursor = false -- true if cursor is in tool bar
227
228DEFAULT_RULE = "3D5..7/6" -- initial rule
229local rulestring = "" -- so very first Initialize calls ParseRule(DEFAULT_RULE)
230local survivals, births -- set by ParseRule
231local NextGeneration -- ditto (set to NextGenStandard or NextGenBusyBoxes)
232
233pattdir = g.getdir("data") -- initial directory for OpenPattern/SavePattern
234scriptdir = g.getdir("app") -- initial directory for RunScript
235local scriptlevel = 0 -- greater than 0 if a user script is running
236
237-- the default path for the script to run when 3D.lua starts up
238pathsep = g.getdir("app"):sub(-1)
239startup = g.getdir("app").."My-scripts"..pathsep.."3D-start.lua"
240
241-- user settings are stored in this file
242settingsfile = g.getdir("data").."3D.ini"
243
244--------------------------------------------------------------------------------
245
246function AddCount(item, counts, maxcount, bcount)
247 local mincount = 0
248 if bcount then mincount = 1 end
249 local i = tonumber(item)
250 if (i >= mincount) and (i <= maxcount) then
251 counts[#counts+1] = i
252 else
253 if bcount then
254 g.warn("Birth count ("..i..") must be from 1 to "..maxcount)
255 else
256 g.warn("Survival count ("..i..") must be from 0 to "..maxcount)
257 end
258 return false
259 end
260 return true
261end
262
263--------------------------------------------------------------------------------
264
265function AddCountRange(low, high, counts, maxcount, bcount)
266 local mincount = 0
267 local what = "Survival"
268 if bcount then
269 mincount = 1
270 what = "Birth"
271 end
272 local l = tonumber(low)
273 local h = tonumber(high)
274
275 -- check range is in ascending order
276 if l > h then
277 g.warn("End of "..what.." range ("..h..") must be higher than start ("..l..")")
278 return false
279 end
280
281 -- check start of range is valid
282 if (l < mincount or l > maxcount) then
283 g.warn("Start of "..what.." range ("..l..") must be from "..mincount.." to "..maxcount)
284 return false
285 end
286
287 -- check end of range is valid
288 if (h < mincount or h > maxcount) then
289 g.warn("End of "..what.." range ("..h..") must be from "..mincount.." to "..maxcount)
290 return false
291 end
292
293 -- add range to counts
294 for i = l, h do
295 counts[#counts+1] = i
296 end
297 return true
298end
299
300--------------------------------------------------------------------------------
301
302function AppendCounts(array, counts, maxcount, bcount)
303 for _, item in ipairs(array) do
304 if validint(item) then
305 -- check for single number
306 if not AddCount(item, counts, maxcount, bcount) then return false end
307 else
308 -- check for range e.g. 5..7
309 local l, h = split(item, "..")
310 if h == nil then h = "" end
311 if #l > 0 and #h > 0 and validint(l) and validint(h) then
312 if not AddCountRange(l, h, counts, maxcount, bcount) then return false end
313 else
314 g.warn("Illegal number in rule: "..item)
315 return false
316 end
317 end
318 end
319 return true
320end
321
322--------------------------------------------------------------------------------
323
324function AddCanonicalPart(canonstr, startrun, i)
325 if #canonstr > 0 then canonstr = canonstr.."," end
326 -- add the start value
327 canonstr = canonstr..startrun
328 -- check for a run
329 if i > startrun + 1 then
330 if i == startrun + 2 then
331 -- run of two becomes a,b
332 canonstr = canonstr..","..i-1
333 else
334 -- run of more than two becomes a..b
335 canonstr = canonstr..".."..i-1
336 end
337 end
338 return canonstr
339end
340
341--------------------------------------------------------------------------------
342
343function CanonicalForm(counts, maxcount)
344 local result = ""
345 local startrun = -1
346 for i = 0, maxcount do
347 if counts[i] then
348 if startrun == -1 then startrun = i end
349 else
350 if startrun ~= -1 then
351 result = AddCanonicalPart(result, startrun, i)
352 startrun = -1
353 end
354 end
355 end
356 -- check for unfinished run
357 if startrun ~= -1 then
358 result = AddCanonicalPart(result, startrun, maxcount+1)
359 end
360 return result
361end
362
363--------------------------------------------------------------------------------
364
365function ParseRule(newrule)
366 -- parse newrule and if ok set rulestring, survivals, births and NextGeneration
367 if #newrule == 0 then
368 newrule = DEFAULT_RULE
369 else
370 newrule = newrule:upper()
371 end
372
373 -- first check for BusyBoxes or BusyBoxesM
374 if newrule == "BUSYBOXES" or newrule == "BB" then
375 rulestring = "BusyBoxes"
376 -- survivals and births are not used
377 NextGeneration = NextGenBusyBoxes
378 ovt{"setrule3d", "BB"}
379 return true
380 elseif newrule == "BUSYBOXESW" or newrule == "BBW" then
381 rulestring = "BusyBoxesW"
382 -- survivals and births are not used
383 NextGeneration = NextGenBusyBoxes
384 ovt{"setrule3d", "BBW"}
385 return true
386 end
387
388 if not newrule:find("^3D") then
389 g.warn("Rule must start with 3D.")
390 return false
391 else
392 -- strip off the 3D
393 newrule = newrule:sub(3)
394 end
395 if not newrule:find("/") then
396 g.warn("Rule must have a / separator.")
397 return false
398 end
399
400 -- use 3D Moore neighborhood unless rule ends with a special letter
401 local lastchar = newrule:sub(-1)
402 local maxcount = 26
403 if lastchar == "F" or lastchar == "V" then
404 -- 6-cell face neighborhood (aka von Neumann neighborhood)
405 lastchar = "F"
406 maxcount = 6
407 newrule = newrule:sub(1,-2)
408 elseif lastchar == "C" then
409 -- 8-cell corner neighborhood
410 maxcount = 8
411 newrule = newrule:sub(1,-2)
412 elseif lastchar == "E" then
413 -- 12-cell edge neighborhood
414 maxcount = 12
415 newrule = newrule:sub(1,-2)
416 elseif lastchar == "H" then
417 -- 12-cell hexahedral neighborhood
418 maxcount = 12
419 newrule = newrule:sub(1,-2)
420 else
421 lastchar = "" -- for Moore neighborhood
422 end
423
424 local s, b = split(newrule,"/")
425 if #s == 0 then s = {} else s = {split(s,",")} end
426 if b == nil or #b == 0 then b = {} else b = {split(b,",")} end
427 local news = {}
428 local newb = {}
429 if not AppendCounts(s, news, maxcount, false) then return false end
430 if not AppendCounts(b, newb, maxcount, true) then return false end
431
432 -- newrule is okay so set survivals and births
433 survivals = {}
434 births = {}
435 for _, i in ipairs(news) do survivals[i] = true end
436 for _, i in ipairs(newb) do births[i] = true end
437
438 -- set the rule
439 ovt{"setrule3d", lastchar, survivals, births}
440
441 -- set rulestring to the canonical form
442 rulestring = "3D"..CanonicalForm(survivals,maxcount).."/"..CanonicalForm(births,maxcount)..lastchar
443
444 -- set NextGeneration to the standard function
445 NextGeneration = NextGenStandard
446
447 return true
448end
449
450--------------------------------------------------------------------------------
451
452function ReadSettings()
453 local f = io.open(settingsfile, "r")
454 if f then
455 while true do
456 -- no need to worry about CRs here because file was created by WriteSettings
457 local line = f:read("*l")
458 if not line then break end
459 local keyword, value = split(line,"=")
460 -- look for a keyword used in WriteSettings
461 if not value then
462 -- ignore keyword
463 elseif keyword == "startup" then startup = tostring(value)
464 elseif keyword == "pattdir" then pattdir = tostring(value)
465 elseif keyword == "scriptdir" then scriptdir = tostring(value)
466 elseif keyword == "celltype" then SetCellTypeOnly(tostring(value))
467 elseif keyword == "perc" then randstring = tostring(value)
468 elseif keyword == "axes" then showaxes = tostring(value) == "true"
469 elseif keyword == "lines" then showlines = tostring(value) == "true"
470 elseif keyword == "shading" then depthshading = tostring(value) == "true"
471 elseif keyword == "history" then
472 showhistory = tonumber(value) or HISTORYOFF
473 if showhistory ~= HISTORYOFF and showhistory ~= DEFAULTHISTORY then showhistory = HISTORYOFF end
474 elseif keyword == "fadehistory" then fadehistory = tostring(value) == "true"
475 elseif keyword == "gridsize" then
476 N = tonumber(value) or DEFAULTN
477 if N < MINN then N = MINN end
478 if N > MAXN then N = MAXN end
479 elseif keyword == "rule" then
480 rulestring = tostring(value)
481 if not ParseRule(rulestring) then
482 g.warn("Resetting bad rule ("..rulestring..") to default.", false)
483 ParseRule(DEFAULT_RULE)
484 end
485 end
486 end
487 f:close()
488 end
489end
490
491--------------------------------------------------------------------------------
492
493function WriteSettings()
494 local f = io.open(settingsfile, "w")
495 if f then
496 -- keywords must match those in ReadSettings (but order doesn't matter)
497 f:write("startup=", startup, "\n")
498 f:write("pattdir=", pattdir, "\n")
499 f:write("scriptdir=", scriptdir, "\n")
500 f:write("gridsize=", tostring(N), "\n")
501 f:write("celltype=", celltype, "\n")
502 f:write("rule=", rulestring, "\n")
503 f:write("perc=", randstring, "\n")
504 f:write("axes="..tostring(showaxes), "\n")
505 f:write("lines="..tostring(showlines), "\n")
506 f:write("shading="..tostring(depthshading), "\n")
507 f:write("history="..tostring(showhistory), "\n")
508 f:write("fadehistory="..tostring(fadehistory), "\n")
509 f:close()
510 end
511end
512
513--------------------------------------------------------------------------------
514
515function SaveGollyState()
516 local oldstate = {}
517 oldstate.scroll = g.setoption("showscrollbars", 0)
518 oldstate.time = g.setoption("showtimeline", 0)
519 oldstate.tool = g.setoption("showtoolbar", 0)
520 oldstate.status = g.setoption("showstatusbar", 0)
521 oldstate.layer = g.setoption("showlayerbar", 0)
522 oldstate.edit = g.setoption("showeditbar", 0)
523 oldstate.tile = g.setoption("tilelayers", 0)
524 oldstate.stack = g.setoption("stacklayers", 0)
525 oldstate.files = g.setoption("showfiles", 0)
526 oldstate.filesdir = g.getdir("files")
527 return oldstate
528end
529
530--------------------------------------------------------------------------------
531
532function RestoreGollyState(oldstate)
533 ov("delete")
534 g.setoption("showscrollbars", oldstate.scroll)
535 g.setoption("showtimeline", oldstate.time)
536 g.setoption("showtoolbar", oldstate.tool)
537 g.setoption("showstatusbar", oldstate.status)
538 g.setoption("showlayerbar", oldstate.layer)
539 g.setoption("showeditbar", oldstate.edit)
540 g.setoption("tilelayers", oldstate.tile)
541 g.setoption("stacklayers", oldstate.stack)
542 g.setoption("showfiles", oldstate.files)
543 g.setdir("files", oldstate.filesdir)
544end
545
546--------------------------------------------------------------------------------
547
548local function TransformPoint(point)
549 -- rotate given 3D point
550 local x, y, z = point[1], point[2], point[3]
551 local newx = (x*xixo + y*xiyo + z*xizo)
552 local newy = (x*yixo + y*yiyo + z*yizo)
553 local newz = (x*zixo + y*ziyo + z*zizo)
554 --[[ or do this to rotate about grid axes
555 local newx = (x*xixo + y*yixo + z*zixo)
556 local newy = (x*xiyo + y*yiyo + z*ziyo)
557 local newz = (x*xizo + y*yizo + z*zizo)
558 --]]
559 return newx, newy, newz
560end
561
562--------------------------------------------------------------------------------
563
564local function HorizontalLine(x1, x2, y)
565 -- draw a horizontal line of pixels from x1,y to x2,y
566 ovt{"line", x1, y, x2, y}
567end
568
569--------------------------------------------------------------------------------
570
571local function DrawEdge(start, finish)
572 DrawLine(projectedx[start], projectedy[start],
573 projectedx[finish], projectedy[finish])
574end
575
576--------------------------------------------------------------------------------
577
578local function FillFace(ax, ay, bx, by, cx, cy, dx, dy, shade)
579 -- fill given convex quadrilateral using modified code from here:
580 -- http://www-users.mat.uni.torun.pl/~wrona/3d_tutor/tri_fillers.html
581
582 -- relabel points so that ay <= by <= cy <= dy;
583 -- note that given vertices are in cyclic order, so a is opposite c
584 -- and b is opposite d
585 if ay > cy then
586 -- swap a and c (diagonally opposite)
587 ax, cx = cx, ax
588 ay, cy = cy, ay
589 end
590 if by > dy then
591 -- swap b and d (diagonally opposite)
592 bx, dx = dx, bx
593 by, dy = dy, by
594 end
595 -- we now have ay <= cy and by <= dy
596 if ay > by then
597 -- swap a and b
598 ax, bx = bx, ax
599 ay, by = by, ay
600 end
601 if cy > dy then
602 -- swap c and d
603 cx, dx = dx, cx
604 cy, dy = dy, cy
605 end
606
607 -- if top and/or bottom line is horizontal then we need to ensure
608 -- that lines a->c and b->d don't intersect
609 if ay == by then
610 if (ax - bx) * (cx - dx) < 0 then
611 -- swap ax and bx
612 ax, bx = bx, ax
613 end
614 elseif cy == dy then
615 if (ax - bx) * (cx - dx) < 0 then
616 -- swap cx and dx
617 cx, dx = dx, cx
618 end
619 end
620
621 -- calculate deltas for interpolation
622 local delta1 = 0.0
623 local delta2 = 0.0
624 local delta3 = 0.0
625 local delta4 = 0.0
626 if by-ay > 0 then delta1 = (bx-ax) / (by-ay) end
627 if cy-ay > 0 then delta2 = (cx-ax) / (cy-ay) end
628 if dy-by > 0 then delta3 = (dx-bx) / (dy-by) end
629 if dy-cy > 0 then delta4 = (dx-cx) / (dy-cy) end
630
631 -- draw horizontal segments from sx to ex (start and end X coords)
632 ov(shade)
633 local sx = ax
634 local sy = ay
635 local ex = sx
636 while sy < by do
637 HorizontalLine(round(sx), round(ex), sy)
638 sy = sy + 1
639 sx = sx + delta1
640 ex = ex + delta2
641 end
642 sx = bx
643 while sy < cy do
644 HorizontalLine(round(sx), round(ex), sy)
645 sy = sy + 1
646 sx = sx + delta3
647 ex = ex + delta2
648 end
649 ex = cx
650 while sy < dy do
651 HorizontalLine(round(sx), round(ex), sy)
652 sy = sy + 1
653 sx = sx + delta3
654 ex = ex + delta4
655 end
656end
657
658--------------------------------------------------------------------------------
659
660local function CheckFaces(f1v1, f1v2, f1v3, f1v4,
661 f2v1, f2v2, f2v3, f2v4)
662
663 local function ComputeShade(v1, v2)
664 -- return a shade of gray assuming a light source in front of grid
665 local x1, y1, z1 = rotx[v1], roty[v1], rotz[v1]
666 local x2, y2, z2 = rotx[v2], roty[v2], rotz[v2]
667 local costheta = (z1-z2) / sqrt( (x1-x2)*(x1-x2) +
668 (y1-y2)*(y1-y2) +
669 (z1-z2)*(z1-z2) )
670 -- costheta ranges from -1.0 to 1.0
671 local shade = 255 - int((costheta + 1.0) / 2.0 * 128)
672 return "rgba "..shade.." "..shade.." "..shade.." 255"
673 end
674
675 -- test rotated z coords to see which face is in front
676 if rotz[f1v1] < rotz[f2v1] then
677 FillFace(projectedx[f1v1], projectedy[f1v1],
678 projectedx[f1v2], projectedy[f1v2],
679 projectedx[f1v3], projectedy[f1v3],
680 projectedx[f1v4], projectedy[f1v4],
681 ComputeShade(f1v1, f2v1))
682 else
683 FillFace(projectedx[f2v1], projectedy[f2v1],
684 projectedx[f2v2], projectedy[f2v2],
685 projectedx[f2v3], projectedy[f2v3],
686 projectedx[f2v4], projectedy[f2v4],
687 ComputeShade(f2v1, f1v1))
688 end
689end
690
691--------------------------------------------------------------------------------
692
693local function CreateCube(x, y, z)
694 -- create cube at given grid position
695 x = x * CELLSIZE + BORDER - MIDGRID
696 y = y * CELLSIZE + BORDER - MIDGRID
697 z = z * CELLSIZE + BORDER - MIDGRID
698
699 -- WARNING: vertex order used here is assumed by other code:
700 -- vertices 1,3,5,7 and 2,4,6,8 are front and back faces,
701 -- vertices 1,2,8,7 and 3,4,6,5 are bottom and top faces,
702 -- vertices 1,2,4,3 and 7,8,6,5 are left and right faces
703 --
704 -- +y
705 -- |
706 -- v4_________v6
707 -- /| /|
708 -- / | / |
709 -- v3__|______v5 |
710 -- | | | |
711 -- | v2_____|___v8___+x
712 -- | / | /
713 -- | / | /
714 -- v1_________v7
715 -- /
716 -- +z
717 --
718 return { {x , y , z+LEN}, -- v1
719 {x , y , z }, -- v2
720 {x , y+LEN, z+LEN}, -- v3
721 {x , y+LEN, z }, -- v4
722 {x+LEN, y+LEN, z+LEN}, -- v5
723 {x+LEN, y+LEN, z }, -- v6
724 {x+LEN, y , z+LEN}, -- v7
725 {x+LEN, y , z } -- v8
726 }
727end
728
729--------------------------------------------------------------------------------
730
731local function DisplayLine(startpt, endpt)
732 -- use orthographic projection to transform line's start and end points
733 local newx, newy = TransformPoint(startpt)
734 local x1 = round(newx) + midx
735 local y1 = round(newy) + midy
736
737 newx, newy = TransformPoint(endpt)
738 local x2 = round(newx) + midx
739 local y2 = round(newy) + midy
740
741 DrawLine(x1, y1, x2, y2)
742end
743
744----------------------------------------------------------------------
745local function AddLineToBatch(startpt, endpt)
746 -- use orthographic projection to transform line's start and end points
747 local x, y, z = startpt[1], startpt[2], startpt[3]
748 xyline[xyln] = (x*xixo + y*xiyo + z*xizo) + midx
749 xyline[xyln + 1] = (x*yixo + y*yiyo + z*yizo) + midy
750 xyln = xyln + 2
751 x, y, z = endpt[1], endpt[2], endpt[3]
752 xyline[xyln] = (x*xixo + y*xiyo + z*xizo) + midx
753 xyline[xyln + 1] = (x*yixo + y*yiyo + z*yizo) + midy
754 xyln = xyln + 2
755end
756
757--------------------------------------------------------------------------------
758
759local function DrawBatchLines()
760 if xyln > 2 then
761 xyline[xyln] = nil
762 ovt(xyline)
763 xyln = 2
764 end
765end
766
767--------------------------------------------------------------------------------
768
769function DrawRearAxes()
770 -- draw lattice lines and/or axes that are behind rotated reference cube
771 -- assuming vertex order set in CreateCube
772 local z1 = rotrefz[1]
773 local z2 = rotrefz[2]
774 local z3 = rotrefz[3]
775 local z7 = rotrefz[7]
776
777 if showlines then
778 ov(LINE_COLOR)
779 if z1 < z2 then
780 -- front face of rotated refcube is visible
781 for _, pt in ipairs(xylattice) do AddLineToBatch(pt[1], pt[2]) end
782 end
783 if z1 >= z7 then
784 -- right face of rotated refcube is visible
785 for _, pt in ipairs(yzlattice) do AddLineToBatch(pt[1], pt[2]) end
786 end
787 if z1 >= z3 then
788 -- top face of rotated refcube is visible
789 for _, pt in ipairs(xzlattice) do AddLineToBatch(pt[1], pt[2]) end
790 end
791 DrawBatchLines()
792 end
793
794 if showaxes then
795 -- draw darker anti-aliased lines for rear axes
796 ov("blend 1")
797 ov("lineoption width 3")
798
799 if z1 < z2 or z1 >= z3 then ov(REARX_COLOR); DisplayLine(xaxes[1], xaxes[2]) end
800 if z1 < z2 or z1 >= z7 then ov(REARY_COLOR); DisplayLine(yaxes[1], yaxes[2]) end
801 if z1 >= z7 or z1 >= z3 then ov(REARZ_COLOR); DisplayLine(zaxes[1], zaxes[2]) end
802
803 if z1 < z2 or z1 < z3 then ov(REARX_COLOR); DisplayLine(xaxes[3], xaxes[4]) end
804 if z1 >= z2 or z1 >= z7 then ov(REARY_COLOR); DisplayLine(yaxes[3], yaxes[4]) end
805 if z1 < z7 or z1 >= z3 then ov(REARZ_COLOR); DisplayLine(zaxes[3], zaxes[4]) end
806
807 if z1 >= z2 or z1 < z3 then ov(REARX_COLOR); DisplayLine(xaxes[5], xaxes[6]) end
808 if z1 >= z2 or z1 < z7 then ov(REARY_COLOR); DisplayLine(yaxes[5], yaxes[6]) end
809 if z1 < z3 or z1 < z7 then ov(REARZ_COLOR); DisplayLine(zaxes[5], zaxes[6]) end
810
811 if z1 >= z2 or z1 >= z3 then ov(REARX_COLOR); DisplayLine(xaxes[7], xaxes[8]) end
812 if z1 < z2 or z1 < z7 then ov(REARY_COLOR); DisplayLine(yaxes[7], yaxes[8]) end
813 if z1 >= z7 or z1 < z3 then ov(REARZ_COLOR); DisplayLine(zaxes[7], zaxes[8]) end
814
815 ov("lineoption width 1")
816 ov("blend 0")
817 end
818end
819
820--------------------------------------------------------------------------------
821
822function DrawFrontAxes()
823 -- draw lattice lines and/or axes that are in front of rotated reference cube
824 -- assuming vertex order set in CreateCube
825 local z1 = rotrefz[1]
826 local z2 = rotrefz[2]
827 local z3 = rotrefz[3]
828 local z7 = rotrefz[7]
829
830 if showlines then
831 ov(LINE_COLOR)
832 if z1 >= z2 then
833 -- back face of rotated refcube is visible
834 for _, pt in ipairs(xylattice) do AddLineToBatch(pt[1], pt[2]) end
835 end
836 if z1 < z7 then
837 -- left face of rotated refcube is visible
838 for _, pt in ipairs(yzlattice) do AddLineToBatch(pt[1], pt[2]) end
839 end
840 if z1 < z3 then
841 -- bottom face of rotated refcube is visible
842 for _, pt in ipairs(xzlattice) do AddLineToBatch(pt[1], pt[2]) end
843 end
844 DrawBatchLines()
845 end
846
847 if showaxes then
848 -- draw brighter anti-aliased lines for front axes
849 ov("blend 1")
850 ov("lineoption width 3")
851
852 if z1 >= z2 or z1 < z3 then ov(X_COLOR); DisplayLine(xaxes[1], xaxes[2]) end
853 if z1 >= z2 or z1 < z7 then ov(Y_COLOR); DisplayLine(yaxes[1], yaxes[2]) end
854 if z1 < z7 or z1 < z3 then ov(Z_COLOR); DisplayLine(zaxes[1], zaxes[2]) end
855
856 if z1 >= z2 or z1 >= z3 then ov(X_COLOR); DisplayLine(xaxes[3], xaxes[4]) end
857 if z1 < z2 or z1 < z7 then ov(Y_COLOR); DisplayLine(yaxes[3], yaxes[4]) end
858 if z1 >= z7 or z1 < z3 then ov(Z_COLOR); DisplayLine(zaxes[3], zaxes[4]) end
859
860 if z1 < z2 or z1 >= z3 then ov(X_COLOR); DisplayLine(xaxes[5], xaxes[6]) end
861 if z1 < z2 or z1 >= z7 then ov(Y_COLOR); DisplayLine(yaxes[5], yaxes[6]) end
862 if z1 >= z3 or z1 >= z7 then ov(Z_COLOR); DisplayLine(zaxes[5], zaxes[6]) end
863
864 if z1 < z2 or z1 < z3 then ov(X_COLOR); DisplayLine(xaxes[7], xaxes[8]) end
865 if z1 >= z2 or z1 >= z7 then ov(Y_COLOR); DisplayLine(yaxes[7], yaxes[8]) end
866 if z1 < z7 or z1 >= z3 then ov(Z_COLOR); DisplayLine(zaxes[7], zaxes[8]) end
867
868 ov("lineoption width 1")
869 ov("blend 0")
870 end
871end
872
873--------------------------------------------------------------------------------
874
875function CreateAxes()
876 -- create axes and lattice lines
877
878 -- put axes origin at most -ve corner of grid
879 local o = -MIDGRID
880 local endpt = o + N*CELLSIZE
881
882 xaxes = { {o,o,o}, {endpt,o,o},
883 {o,endpt,o}, {endpt,endpt,o},
884 {o,endpt,endpt}, {endpt,endpt,endpt},
885 {o,o,endpt}, {endpt,o,endpt}
886 }
887 yaxes = { {o,o,o}, {o,endpt,o},
888 {o,o,endpt}, {o,endpt,endpt},
889 {endpt,o,endpt}, {endpt,endpt,endpt},
890 {endpt,o,o}, {endpt,endpt,o}
891 }
892 zaxes = { {o,o,o}, {o,o,endpt},
893 {endpt,o,o}, {endpt,o,endpt},
894 {endpt,endpt,o}, {endpt,endpt,endpt},
895 {o,endpt,o}, {o,endpt,endpt}
896 }
897
898 xylattice = {}
899 for i = 0, N do
900 local offset = i*CELLSIZE
901 xylattice[#xylattice+1] = {{o+offset,o,o}, {o+offset,endpt,o}}
902 xylattice[#xylattice+1] = {{o,o+offset,o}, {endpt,o+offset,o}}
903 end
904
905 xzlattice = {}
906 for i = 0, N do
907 local offset = i*CELLSIZE
908 xzlattice[#xzlattice+1] = {{o,o,o+offset}, {endpt,o,o+offset}}
909 xzlattice[#xzlattice+1] = {{o+offset,o,o}, {o+offset,o,endpt}}
910 end
911
912 yzlattice = {}
913 for i = 0, N do
914 local offset = i*CELLSIZE
915 yzlattice[#yzlattice+1] = {{o,o+offset,o}, {o,o+offset,endpt}}
916 yzlattice[#yzlattice+1] = {{o,o,o+offset}, {o,endpt,o+offset}}
917 end
918end
919
920--------------------------------------------------------------------------------
921
922function CreateTranslucentCell(clipname, color)
923 -- create a clip containing a translucent cube with given color
924 ov("create "..(CELLSIZE*2).." "..(CELLSIZE*2).." "..clipname)
925 ov("target "..clipname)
926 ov(color)
927
928 -- temporarily change BORDER and LEN so CreateCube fills cell
929 local oldBORDER = BORDER
930 local oldLEN = LEN
931 BORDER = 1
932 LEN = CELLSIZE-2 - (CELLSIZE%2)
933
934 local midpos = N//2
935 local cube = CreateCube(midpos, midpos, midpos)
936
937 -- create cube's projected vertices (within clip)
938 for i = 1, 8 do
939 rotx[i], roty[i], rotz[i] = TransformPoint(cube[i])
940 projectedx[i] = round( rotx[i] ) + CELLSIZE
941 projectedy[i] = round( roty[i] ) + CELLSIZE
942 end
943
944 -- draw outer edges of cube then flood from center
945 if rotz[1] < rotz[2] then
946 -- front face is visible
947 if rotz[5] < rotz[3] then
948 -- right face is visible
949 DrawEdge(1,3)
950 if rotz[5] < rotz[7] then
951 -- top face is visible
952 DrawEdge(3,4)
953 DrawEdge(4,6)
954 DrawEdge(6,8)
955 DrawEdge(8,7)
956 DrawEdge(7,1)
957 else
958 -- bottom face is visible
959 DrawEdge(3,5)
960 DrawEdge(5,6)
961 DrawEdge(6,8)
962 DrawEdge(8,2)
963 DrawEdge(2,1)
964 end
965 else
966 -- left face is visible
967 DrawEdge(5,7)
968 if rotz[5] < rotz[7] then
969 -- top face is visible
970 DrawEdge(7,1)
971 DrawEdge(1,2)
972 DrawEdge(2,4)
973 DrawEdge(4,6)
974 DrawEdge(6,5)
975 else
976 -- bottom face is visible
977 DrawEdge(7,8)
978 DrawEdge(8,2)
979 DrawEdge(2,4)
980 DrawEdge(4,3)
981 DrawEdge(3,5)
982 end
983 end
984 else
985 -- back face is visible
986 if rotz[5] < rotz[3] then
987 -- right face is visible
988 DrawEdge(5,7)
989 if rotz[5] < rotz[7] then
990 -- top face is visible
991 DrawEdge(7,8)
992 DrawEdge(8,2)
993 DrawEdge(2,4)
994 DrawEdge(4,3)
995 DrawEdge(3,5)
996 else
997 -- bottom face is visible
998 DrawEdge(7,1)
999 DrawEdge(1,2)
1000 DrawEdge(2,4)
1001 DrawEdge(4,6)
1002 DrawEdge(6,5)
1003 end
1004 else
1005 -- left face is visible
1006 DrawEdge(1,3)
1007 if rotz[5] < rotz[7] then
1008 -- top face is visible
1009 DrawEdge(3,5)
1010 DrawEdge(5,6)
1011 DrawEdge(6,8)
1012 DrawEdge(8,2)
1013 DrawEdge(2,1)
1014 else
1015 -- bottom face is visible
1016 DrawEdge(3,4)
1017 DrawEdge(4,6)
1018 DrawEdge(6,8)
1019 DrawEdge(8,7)
1020 DrawEdge(7,1)
1021 end
1022 end
1023 end
1024
1025 ov("flood "..CELLSIZE.." "..CELLSIZE)
1026
1027 -- restore BORDER and LEN
1028 BORDER = oldBORDER
1029 LEN = oldLEN
1030
1031 ov("optimize "..clipname)
1032 ov("target")
1033end
1034
1035--------------------------------------------------------------------------------
1036
1037function DrawCubeEdges()
1038 if LEN > 4 then
1039 -- draw anti-aliased edges around visible face(s)
1040 if LEN == 5 then
1041 ovt{"rgba", 150, 150, 150, 255}
1042 elseif LEN == 6 then
1043 ovt{"rgba", 110, 110, 110, 255}
1044 elseif LEN == 7 then
1045 ovt{"rgba", 80, 80, 80, 255}
1046 else
1047 ovt{"rgba", 60, 60, 60, 255}
1048 end
1049 ov("blend 1")
1050 ov("lineoption width "..(1 + int(LEN / 40.0)))
1051
1052 if rotz[1] < rotz[2] then
1053 -- draw all edges around front face
1054 DrawEdge(1,3)
1055 DrawEdge(3,5)
1056 DrawEdge(5,7)
1057 DrawEdge(7,1)
1058 if rotz[1] < rotz[3] then
1059 -- draw remaining 3 edges around bottom face
1060 DrawEdge(1,2)
1061 DrawEdge(2,8)
1062 DrawEdge(8,7)
1063 if rotz[1] < rotz[7] then
1064 -- draw remaining 2 edges around left face
1065 DrawEdge(3,4)
1066 DrawEdge(4,2)
1067 else
1068 -- draw remaining 2 edges around right face
1069 DrawEdge(5,6)
1070 DrawEdge(6,8)
1071 end
1072 else
1073 -- draw 3 remaining edges around top face
1074 DrawEdge(3,4)
1075 DrawEdge(4,6)
1076 DrawEdge(6,5)
1077 if rotz[1] < rotz[7] then
1078 -- draw remaining 2 edges around left face
1079 DrawEdge(4,2)
1080 DrawEdge(2,1)
1081 else
1082 -- draw remaining 2 edges around right face
1083 DrawEdge(6,8)
1084 DrawEdge(8,7)
1085 end
1086 end
1087 else
1088 -- draw all edges around back face
1089 DrawEdge(2,4)
1090 DrawEdge(4,6)
1091 DrawEdge(6,8)
1092 DrawEdge(8,2)
1093 if rotz[1] < rotz[3] then
1094 -- draw remaining 3 edges around bottom face
1095 DrawEdge(2,1)
1096 DrawEdge(1,7)
1097 DrawEdge(7,8)
1098 if rotz[1] < rotz[7] then
1099 -- draw remaining 2 edges around left face
1100 DrawEdge(1,3)
1101 DrawEdge(3,4)
1102 else
1103 -- draw remaining 2 edges around right face
1104 DrawEdge(7,5)
1105 DrawEdge(5,6)
1106 end
1107 else
1108 -- draw 3 remaining edges around top face
1109 DrawEdge(6,5)
1110 DrawEdge(5,3)
1111 DrawEdge(3,4)
1112 if rotz[1] < rotz[7] then
1113 -- draw remaining 2 edges around left face
1114 DrawEdge(2,1)
1115 DrawEdge(1,3)
1116 else
1117 -- draw remaining 2 edges around right face
1118 DrawEdge(8,7)
1119 DrawEdge(7,5)
1120 end
1121 end
1122 end
1123
1124 ov("lineoption width 1")
1125 ov("blend 0")
1126 end
1127end
1128
1129--------------------------------------------------------------------------------
1130
1131lastHistorySize = -1
1132
1133function CreateHistoryCells(clip, color)
1134 -- only create bitmaps if cell size has changed
1135 if CELLSIZE == lastHistorySize then return end
1136 lastHistorySize = CELLSIZE
1137
1138 -- create translucent history cell
1139 CreateTranslucentCell(clip, color)
1140
1141 -- create alpha fading clips if required
1142 if fadehistory then
1143 local _, _, _, _, starta = split(color)
1144 local stopa = MINHISTORYALPHA
1145 local adjust = (starta - stopa + 1) / showhistory
1146 local alpha = starta
1147 ov("target "..clip)
1148 for i = 1, showhistory do
1149 ov("target "..clip)
1150 ov("copy 0 0 0 0 "..clip..i)
1151 ov("target "..clip..i)
1152 ovt{"rgba", 0, 0, 0, alpha}
1153 ov("replace !0# 0# 0# *")
1154 ov("optimize "..clip..i)
1155 alpha = alpha - adjust
1156 end
1157 ov("target")
1158 end
1159end
1160
1161--------------------------------------------------------------------------------
1162
1163function CreateLayers(clip)
1164 local adjust = depthrange / (maxdepth - mindepth + 1)
1165 local total = 0
1166 local rgb
1167 ov("target "..clip)
1168 ov("copy 0 0 0 0 "..clip.."1")
1169 ov("target "..clip.."1")
1170 ov("optimize "..clip.."1")
1171 for i = 2, maxdepth do
1172 total = total + adjust
1173 rgb = floor(total)
1174 ov("target "..clip)
1175 ov("copy 0 0 0 0 "..clip..i)
1176 ov("target "..clip..i)
1177 ov("replace *#-"..rgb.." *#-"..rgb.." *#-"..rgb.." *#")
1178 ov("optimize "..clip..i)
1179 end
1180 total = 0
1181 for i = 1, mindepth, -1 do
1182 total = total + adjust
1183 rgb = floor(total)
1184 ov("target "..clip)
1185 ov("copy 0 0 0 0 "..clip..i)
1186 ov("target "..clip..i)
1187 ov("replace *#+"..rgb.." *#+"..rgb.." *#+"..rgb.." *#")
1188 ov("optimize "..clip..i)
1189 end
1190 ov("target")
1191end
1192
1193--------------------------------------------------------------------------------
1194
1195local HALFCUBECLIP -- half the wd/ht of the clip containing a live cube
1196lastCubeSize = -1
1197
1198function CreateLiveCube()
1199 -- only create bitmaps if cell size has changed
1200 if CELLSIZE == lastCubeSize then return end
1201 lastCubeSize = CELLSIZE
1202
1203 -- create a clip containing one rotated cube that will be used later
1204 -- to draw all live cells (this only works because all cubes are identical
1205 -- in appearance when using orthographic projection)
1206
1207 -- largest size of a rotated cube with edge length L is sqrt(3)*L
1208 HALFCUBECLIP = round(sqrt(3) * LEN / 2.0)
1209 ov("create "..(HALFCUBECLIP*2).." "..(HALFCUBECLIP*2).." L")
1210 ov("target L")
1211
1212 local midpos = N//2
1213 local cube = CreateCube(midpos, midpos, midpos)
1214
1215 -- create cube's projected vertices (within clip)
1216 for i = 1, 8 do
1217 rotx[i], roty[i], rotz[i] = TransformPoint(cube[i])
1218 projectedx[i] = round( rotx[i] ) + HALFCUBECLIP
1219 projectedy[i] = round( roty[i] ) + HALFCUBECLIP
1220 end
1221
1222 -- draw up to 3 visible faces of cube, using cyclic vertex order set in CreateCube
1223 CheckFaces(1,3,5,7, 2,4,6,8) -- front or back
1224 CheckFaces(1,2,8,7, 3,4,6,5) -- bottom or top
1225 CheckFaces(1,2,4,3, 7,8,6,5) -- left or right
1226
1227 DrawCubeEdges()
1228
1229 ov("optimize L")
1230
1231 -- create faded versions of the clip if depth shading
1232 if depthshading then
1233 CreateLayers("L")
1234 end
1235
1236 ov("target")
1237end
1238
1239--------------------------------------------------------------------------------
1240
1241lastSphereSize = -1
1242
1243function CreateLiveSphere()
1244 -- only create bitmaps if cell size has changed
1245 if CELLSIZE == lastSphereSize then return end
1246 lastSphereSize = CELLSIZE
1247
1248 -- create a clip containing one sphere that will be used later
1249 -- to draw all live cells
1250 local diameter = CELLSIZE+1 -- so orthogonally adjacent spheres touch
1251 ov("create "..diameter.." "..diameter.." L")
1252 ov("target L")
1253 ov("blend 1")
1254
1255 local x = 0
1256 local y = 0
1257 local gray = 0 -- start with black outline
1258 local grayinc = 127
1259 local r = (diameter+1)//2
1260 if r > 2 then grayinc = 127/(r-2) end
1261 while true do
1262 local grayrgb = floor(gray)
1263 ovt{"rgba", grayrgb, grayrgb, grayrgb, 255}
1264 -- draw a solid circle by setting the line width to the radius
1265 ov("lineoption width "..r)
1266 ov("ellipse "..x.." "..y.." "..diameter.." "..diameter)
1267 diameter = diameter - 2
1268 r = r - 1
1269 if r < 2 then break end
1270 x = x + 1
1271 y = y + 1
1272 if gray == 0 then gray = 128 end
1273 gray = gray + grayinc
1274 if gray > 255 then gray = 255 end
1275 end
1276
1277 ov("blend 0")
1278 ov("lineoption width 1")
1279 ov("optimize L")
1280
1281 -- create faded versions of the clip if depth shading
1282 if depthshading then
1283 CreateLayers("L")
1284 end
1285
1286 ov("target")
1287end
1288
1289--------------------------------------------------------------------------------
1290
1291function DisplayCells(editing)
1292 -- find the rotated reference cube vertex with maximum Z coordinate
1293 local z1 = rotrefz[1]
1294 local z2 = rotrefz[2]
1295 local z3 = rotrefz[3]
1296 local z4 = rotrefz[4]
1297 local z5 = rotrefz[5]
1298 local z6 = rotrefz[6]
1299 local z7 = rotrefz[7]
1300 local z8 = rotrefz[8]
1301 local maxZ = max(z1,z2,z3,z4,z5,z6,z7,z8)
1302
1303 local testcell = editing or selcount > 0 or pastecount > 0
1304
1305 -- find the extended boundary of all live/active/selected/paste cells
1306 local MINX, MINY, MINZ, MAXX, MAXY, MAXZ = minx, miny, minz, maxx, maxy, maxz
1307 if testcell then
1308 if editing then
1309 if minactivex < MINX then MINX = minactivex end
1310 if minactivey < MINY then MINY = minactivey end
1311 if minactivez < MINZ then MINZ = minactivez end
1312 if maxactivex > MAXX then MAXX = maxactivex end
1313 if maxactivey > MAXY then MAXY = maxactivey end
1314 if maxactivez > MAXZ then MAXZ = maxactivez end
1315 end
1316 if selcount > 0 then
1317 if minselx < MINX then MINX = minselx end
1318 if minsely < MINY then MINY = minsely end
1319 if minselz < MINZ then MINZ = minselz end
1320 if maxselx > MAXX then MAXX = maxselx end
1321 if maxsely > MAXY then MAXY = maxsely end
1322 if maxselz > MAXZ then MAXZ = maxselz end
1323 end
1324 if pastecount > 0 then
1325 if minpastex < MINX then MINX = minpastex end
1326 if minpastey < MINY then MINY = minpastey end
1327 if minpastez < MINZ then MINZ = minpastez end
1328 if maxpastex > MAXX then MAXX = maxpastex end
1329 if maxpastey > MAXY then MAXY = maxpastey end
1330 if maxpastez > MAXZ then MAXZ = maxpastez end
1331 end
1332 end
1333
1334 -- determine order to traverse x, y and z in the grid;
1335 -- note that we need to draw cells from back to front
1336 -- (assumes vertex order set in CreateCube)
1337 local fromz, toz, stepz, fromy, toy, stepy, fromx, tox, stepx
1338 if maxZ == z1 then
1339 fromx, fromy, fromz = MINX, MINY, MAXZ
1340 elseif maxZ == z2 then
1341 fromx, fromy, fromz = MINX, MINY, MINZ
1342 elseif maxZ == z3 then
1343 fromx, fromy, fromz = MINX, MAXY, MAXZ
1344 elseif maxZ == z4 then
1345 fromx, fromy, fromz = MINX, MAXY, MINZ
1346 elseif maxZ == z5 then
1347 fromx, fromy, fromz = MAXX, MAXY, MAXZ
1348 elseif maxZ == z6 then
1349 fromx, fromy, fromz = MAXX, MAXY, MINZ
1350 elseif maxZ == z7 then
1351 fromx, fromy, fromz = MAXX, MINY, MAXZ
1352 elseif maxZ == z8 then
1353 fromx, fromy, fromz = MAXX, MINY, MINZ
1354 end
1355
1356 if (fromx == MINX) then tox, stepx = MAXX, 1 else tox, stepx = MINX, -1 end
1357 if (fromy == MINY) then toy, stepy = MAXY, 1 else toy, stepy = MINY, -1 end
1358 if (fromz == MINZ) then toz, stepz = MAXZ, 1 else toz, stepz = MINZ, -1 end
1359
1360 -- update the select, paste and active plane
1361 if editing then
1362 ovt{"setselpasact3d", selected, pastepatt, active}
1363 else
1364 ovt{"setselpasact3d", selected, pastepatt, {}}
1365 end
1366
1367 -- display the cells
1368 ovt{"displaycells3d", fromx, tox, stepx, fromy, toy, stepy, fromz, toz, stepz, CELLSIZE, editing, toolbarht}
1369end
1370
1371--------------------------------------------------------------------------------
1372
1373lastBusyCubeSize = {E = -1, O = -1}
1374
1375function CreateBusyCube(clipname)
1376 -- only create bitmaps if cell size has changed
1377 if CELLSIZE == lastBusyCubeSize[clipname] then return end
1378 lastBusyCubeSize[clipname] = CELLSIZE
1379
1380 -- create a clip containing a cube for odd/even cells
1381 -- largest size of a rotated cube with edge length L is sqrt(3)*L
1382 HALFCUBECLIP = round(sqrt(3) * LEN / 2.0)
1383 ov("create "..(HALFCUBECLIP*2).." "..(HALFCUBECLIP*2).." "..clipname)
1384 ov("target "..clipname)
1385
1386 local midpos = N//2
1387 local cube = CreateCube(midpos, midpos, midpos)
1388
1389 -- create cube's projected vertices (within clip)
1390 for i = 1, 8 do
1391 rotx[i], roty[i], rotz[i] = TransformPoint(cube[i])
1392 projectedx[i] = round( rotx[i] ) + HALFCUBECLIP
1393 projectedy[i] = round( roty[i] ) + HALFCUBECLIP
1394 end
1395
1396 -- draw up to 3 visible faces of cube, using cyclic vertex order set in CreateCube
1397 CheckFaces(1,3,5,7, 2,4,6,8) -- front or back
1398 CheckFaces(1,2,8,7, 3,4,6,5) -- bottom or top
1399 CheckFaces(1,2,4,3, 7,8,6,5) -- left or right
1400
1401 -- adjust grayscale colors using the replace command
1402 if clipname == "E" then
1403 ov(EVEN_CUBE)
1404 else
1405 ov(ODD_CUBE)
1406 end
1407
1408 DrawCubeEdges()
1409
1410 ov("optimize "..clipname)
1411
1412 -- create faded versions of the clip if depth shading
1413 if depthshading then
1414 CreateLayers(clipname)
1415 end
1416
1417 ov("target")
1418end
1419
1420--------------------------------------------------------------------------------
1421
1422lastBusySphereSize = { E = -1, O = -1 }
1423
1424function CreateBusySphere(clipname)
1425 -- only create bitmaps if cell size has changed
1426 if CELLSIZE == lastBusySphereSize[clipname] then return end
1427 lastBusySphereSize[clipname] = CELLSIZE
1428
1429 -- create a clip containing a sphere for odd/even cells
1430 local diameter = CELLSIZE+1
1431 local d1 = diameter
1432 ov("create "..diameter.." "..diameter.." "..clipname)
1433 ov("target "..clipname)
1434 ov("blend 1")
1435
1436 local R, G, B
1437 if clipname == "E" then
1438 local _, red, green, blue = split(EVEN_COLOR)
1439 R = tonumber(red)
1440 G = tonumber(green)
1441 B = tonumber(blue)
1442 else
1443 local _, red, green, blue = split(ODD_COLOR)
1444 R = tonumber(red)
1445 G = tonumber(green)
1446 B = tonumber(blue)
1447 end
1448
1449 local x = 0
1450 local y = 0
1451 local inc = 3
1452 if diameter < 50 then inc = 8 - diameter//10 end
1453 local r = (diameter+1)//2
1454 while true do
1455 ovt{"rgba", R, G, B, 255}
1456 -- draw a solid circle by setting the line width to the radius
1457 ov("lineoption width "..r)
1458 ov("ellipse "..x.." "..y.." "..diameter.." "..diameter)
1459 diameter = diameter - 2
1460 r = r - 1
1461 if r < 2 then break end
1462 x = x + 1
1463 y = y + 1
1464 R = R + inc
1465 G = G + inc
1466 B = B + inc
1467 if R > 255 then R = 255 end
1468 if G > 255 then G = 255 end
1469 if B > 255 then B = 255 end
1470 end
1471
1472 -- draw black outline
1473 ovt{"rgba", 0, 0, 0, 255}
1474 ov("lineoption width 1")
1475 ov("ellipse 0 0 "..d1.." "..d1)
1476 ov("blend 0")
1477 ov("optimize "..clipname)
1478
1479 -- create faded versions of the clip if depth shading
1480 if depthshading then
1481 CreateLayers(clipname)
1482 end
1483
1484 ov("target")
1485end
1486
1487--------------------------------------------------------------------------------
1488
1489function CreatePoint(clipname, color)
1490 ov("create 1 1 "..clipname)
1491 ov("target "..clipname)
1492 -- set pixel to the given color
1493 ov(color)
1494 ovt{"set", 0, 0}
1495 ov("target")
1496end
1497
1498--------------------------------------------------------------------------------
1499
1500function EnableControls(bool)
1501 -- disable/enable unsafe menu items so that user scripts can call op.process
1502 -- File menu:
1503 mbar.enableitem(1, 1, bool) -- New Pattern
1504 mbar.enableitem(1, 2, bool) -- Random Pattern
1505 mbar.enableitem(1, 3, bool) -- Open Pattern
1506 mbar.enableitem(1, 4, bool) -- Open Clipboard
1507 mbar.enableitem(1, 5, bool) -- Save Pattern
1508 mbar.enableitem(1, 7, bool) -- Run Script
1509 mbar.enableitem(1, 8, bool) -- Run Clipboard
1510 mbar.enableitem(1, 9, bool) -- Set Startup Script
1511 if bool then
1512 -- ExitScript will abort 3D.lua
1513 mbar.setitem(1, 11, "Exit 3D.lua")
1514 else
1515 -- ExitScript will abort the user script
1516 mbar.setitem(1, 11, "Exit Script")
1517 end
1518 -- Edit menu:
1519 mbar.enableitem(2, 1, bool) -- Undo
1520 mbar.enableitem(2, 2, bool) -- Redo
1521 mbar.enableitem(2, 4, bool) -- Cut
1522 mbar.enableitem(2, 5, bool) -- Copy
1523 mbar.enableitem(2, 6, bool) -- Paste
1524 mbar.enableitem(2, 7, bool) -- Cancel Paste
1525 mbar.enableitem(2, 8, bool) -- Clear
1526 mbar.enableitem(2, 9, bool) -- Clear Outside
1527 mbar.enableitem(2, 11, bool) -- Select All
1528 mbar.enableitem(2, 12, bool) -- Cancel Selection
1529 mbar.enableitem(2, 14, bool) -- Middle Pattern
1530 mbar.enableitem(2, 15, bool) -- Middle Selection
1531 mbar.enableitem(2, 16, bool) -- Middle Paste
1532 -- Control menu:
1533 mbar.enableitem(3, 1, bool) -- Start/Stop Generating
1534 mbar.enableitem(3, 2, bool) -- Next Generation
1535 mbar.enableitem(3, 3, bool) -- Next Step
1536 mbar.enableitem(3, 4, bool) -- Reset
1537 mbar.enableitem(3, 6, bool) -- Set Rule
1538 -- View menu:
1539 mbar.enableitem(4, 3, bool) -- Set Grid Size
1540
1541 -- disable/enable unsafe buttons
1542 ssbutton.enable(bool)
1543 s1button.enable(bool)
1544 resetbutton.enable(bool)
1545 undobutton.enable(bool)
1546 redobutton.enable(bool)
1547
1548 -- disable/enable unsafe radio buttons
1549 drawbox.enable(bool)
1550 selectbox.enable(bool)
1551 movebox.enable(bool)
1552end
1553
1554--------------------------------------------------------------------------------
1555
1556function DrawMenuBar()
1557 if scriptlevel == 0 then
1558 mbar.enableitem(2, 1, #undostack > 0) -- Undo
1559 mbar.enableitem(2, 2, #redostack > 0) -- Redo
1560 mbar.enableitem(2, 4, selcount > 0) -- Cut
1561 mbar.enableitem(2, 5, selcount > 0) -- Copy
1562 mbar.enableitem(2, 7, pastecount > 0) -- Cancel Paste
1563 mbar.enableitem(2, 8, selcount > 0) -- Clear
1564 mbar.enableitem(2, 9, selcount > 0) -- Clear Outside
1565 mbar.enableitem(2, 11, popcount > 0) -- Select All
1566 mbar.enableitem(2, 12, selcount > 0) -- Cancel Selection
1567 mbar.enableitem(2, 14, popcount > 0) -- Middle Pattern
1568 mbar.enableitem(2, 15, selcount > 0) -- Middle Selection
1569 mbar.enableitem(2, 16, pastecount > 0) -- Middle Paste
1570 mbar.enableitem(3, 1, popcount > 0) -- Start/Stop Generating
1571 mbar.enableitem(3, 2, popcount > 0) -- Next Generation
1572 mbar.enableitem(3, 3, popcount > 0) -- Next Step
1573 mbar.enableitem(3, 4, gencount > startcount) -- Reset
1574 end
1575
1576 mbar.radioitem(4, 5, celltype == "cube")
1577 mbar.radioitem(4, 6, celltype == "sphere")
1578 mbar.radioitem(4, 7, celltype == "point")
1579 mbar.tickitem(4, 9, showaxes)
1580 mbar.tickitem(4, 10, showlines)
1581 mbar.tickitem(4, 11, depthshading)
1582 mbar.tickitem(4, 12, showhistory > 0)
1583 mbar.tickitem(4, 13, fadehistory)
1584
1585 mbar.show(0, 0, ovwd, mbarht)
1586end
1587
1588--------------------------------------------------------------------------------
1589
1590function DrawToolBar()
1591 ovt{"rgba", 230, 230, 230, 255}
1592 ovt{"fill", 0, 0, ovwd, toolbarht}
1593
1594 DrawMenuBar()
1595
1596 -- draw line at bottom edge of tool bar
1597 ov(op.gray)
1598 DrawLine(0, toolbarht-1, ovwd-1, toolbarht-1)
1599
1600 if scriptlevel == 0 then
1601 -- enable/disable buttons
1602 ssbutton.enable(popcount > 0)
1603 s1button.enable(popcount > 0)
1604 resetbutton.enable(gencount > startcount)
1605 undobutton.enable(#undostack > 0)
1606 redobutton.enable(#redostack > 0)
1607 end
1608
1609 local x = gap
1610 local y = mbarht + gap
1611 local biggap = gap * 3
1612
1613 ssbutton.show(x, y)
1614 x = x + ssbutton.wd + gap
1615 s1button.show(x, y)
1616 x = x + s1button.wd + gap
1617 resetbutton.show(x, y)
1618 x = x + resetbutton.wd + gap
1619 fitbutton.show(x, y)
1620 x = x + fitbutton.wd + biggap
1621 undobutton.show(x, y)
1622 x = x + undobutton.wd + gap
1623 redobutton.show(x, y)
1624 x = x + redobutton.wd + biggap
1625 drawbox.show(x, y, currcursor == drawcursor)
1626 x = x + drawbox.wd + gap
1627 selectbox.show(x, y, currcursor == selectcursor)
1628 x = x + selectbox.wd + gap
1629 movebox.show(x, y, currcursor == movecursor)
1630
1631 -- show step slider to right of radio buttons
1632 stepslider.show(x + movebox.wd + biggap, y, stepsize)
1633
1634 -- show stepsize at right end of slider
1635 ov(op.black)
1636 local oldfont
1637 if g.os() == "Linux" then
1638 oldfont = ov("font 10 default")
1639 else
1640 oldfont = ov("font 10 default-bold")
1641 end
1642 local oldbg = ov("textoption background 230 230 230 255")
1643 local _, _ = op.maketext("Step="..stepsize)
1644 op.pastetext(stepslider.x + stepslider.wd + 2, y + 1)
1645 ov("textoption background "..oldbg)
1646 ov("font "..oldfont)
1647
1648 -- last 2 buttons are at right end of tool bar
1649 x = ovwd - gap - exitbutton.wd
1650 exitbutton.show(x, y)
1651 x = x - gap - helpbutton.wd
1652 helpbutton.show(x, y)
1653end
1654
1655--------------------------------------------------------------------------------
1656
1657function UpdateWindowTitle()
1658 -- set window title if it has changed
1659 local newtitle = string.format("%s [%s]", pattname, rulestring)
1660 if dirty then newtitle = "*"..newtitle end
1661 if g.os() ~= "Mac" then newtitle = newtitle.." - Golly" end
1662 if newtitle ~= currtitle then
1663 g.settitle(newtitle)
1664 currtitle = newtitle
1665 end
1666end
1667
1668--------------------------------------------------------------------------------
1669
1670function Refresh(update)
1671 if scriptlevel > 0 and not update then
1672 -- user scripts need to call Update() when they want to refresh
1673 return
1674 end
1675
1676 -- turn off event checking temporarily to avoid partial updates of overlay
1677 -- (eg. due to user resizing window while a pattern is generating)
1678 g.check(false)
1679
1680 -- if the pattern has been modified then update
1681 if update_grid then
1682 ovt{"setpattern3d", grid1, false}
1683 update_grid = false
1684 end
1685
1686 -- fill overlay with background color
1687 ov(BACK_COLOR)
1688 ovt{"fill"}
1689
1690 -- get Z coordinates of the vertices of a rotated reference cube
1691 -- (for doing various depth tests)
1692 for i = 1, 8 do
1693 local _, _, z = TransformPoint(refcube[i])
1694 rotrefz[i] = z
1695 end
1696
1697 if showaxes or showlines then DrawRearAxes() end
1698
1699 --[[
1700 All types of cell are defined as named clips and are
1701 the same for Cubes, Spheres or Points.
1702 Only the clips required for the current settings need to be
1703 created (algo, history, depth shading, mode, etc.).
1704
1705 Name Description
1706 a active plane (Draw or Select mode)
1707 h history cell
1708 h1..hn fading history cell
1709 hN history cell not in active plane (Draw or Select mode)
1710 p paste target cell
1711 s selected cell
1712 sN selected cell not in active plane (Draw or Select mode)
1713
1714 Moore, Face, Corner, Edge and Hexahedral algos:
1715 L live cell without depth shading
1716 L1..Ln live cell with depth shading
1717 LN live cell not in active plane (Draw or Select mode)
1718
1719 BusyBoxes algo:
1720 E live even cell without depth shading
1721 O live odd cell without depth shading
1722 E1..En live even cell with depth shading
1723 O1..En live odd cell with depth shading
1724 EN live even cell not in active plane (Draw or Select Mode)
1725 ON live odd cell not in active plane (Draw or Select Mode)
1726 ]]
1727
1728 local editing = currcursor ~= movecursor
1729 if popcount > 0 or pastecount > 0 or selcount > 0 or editing then
1730 if showhistory > HISTORYOFF then
1731 -- history cells will be translucent
1732 CreateHistoryCells("h", HISTORY_COLOR)
1733 end
1734 if pastecount > 0 then
1735 -- paste cells will be translucent
1736 CreateTranslucentCell("p", PASTE_COLOR)
1737 end
1738 if selcount > 0 then
1739 -- selected cells will be translucent
1740 CreateTranslucentCell("s", SELECT_COLOR)
1741 end
1742 if editing then
1743 -- cells in active plane will be translucent
1744 CreateTranslucentCell("a", ACTIVE_COLOR)
1745
1746 -- live cells not in active plane will be points
1747 if rulestring:find("^BusyBoxes") then
1748 CreatePoint("EN", EVEN_COLOR)
1749 CreatePoint("ON", ODD_COLOR)
1750 else
1751 CreatePoint("LN", POINT_COLOR)
1752 end
1753
1754 -- selected cells not in active plane will be points
1755 CreatePoint("sN", SELPT_COLOR)
1756
1757 -- history cells not in active plane will be points
1758 if showhistory > HISTORYOFF then
1759 CreatePoint("hN", HISTORY_COLOR)
1760 end
1761 end
1762 if rulestring:find("^BusyBoxes") then
1763 if celltype == "cube" then
1764 CreateBusyCube("E")
1765 CreateBusyCube("O")
1766 elseif celltype == "sphere" then
1767 CreateBusySphere("E")
1768 CreateBusySphere("O")
1769 else -- celltype == "point"
1770 CreatePoint("E", EVEN_COLOR)
1771 CreatePoint("O", ODD_COLOR)
1772 end
1773 else
1774 if celltype == "cube" then
1775 CreateLiveCube()
1776 elseif celltype == "sphere" then
1777 CreateLiveSphere()
1778 else -- celltype == "point" then
1779 CreatePoint("L", POINT_COLOR)
1780 end
1781 end
1782 DisplayCells(editing)
1783 end
1784
1785 if showaxes or showlines then DrawFrontAxes() end
1786
1787 -- show info in top left corner
1788 local info =
1789 "Generation = "..gencount.."\n"..
1790 "Population = "..popcount
1791 if selcount > 0 then
1792 info = info.."\nSelected cells = "..selcount
1793 end
1794 if editing then
1795 -- show cell coords of mouse if it's inside the active plane
1796 info = info.."\nx,y,z = "..activecell
1797 end
1798 ov(INFO_COLOR)
1799 local _, ht = op.maketext(info)
1800 op.pastetext(10, toolbarht + 10)
1801 if message then
1802 ov(MSG_COLOR)
1803 op.maketext(message)
1804 op.pastetext(10, toolbarht + 10 + ht + 10)
1805 end
1806
1807 if toolbarht > 0 then DrawToolBar() end
1808
1809 -- the overlay is 100% opaque and covers entire viewport
1810 -- so we can call ov("update") rather than slower g.update()
1811 ov("update")
1812
1813 UpdateWindowTitle()
1814
1815 g.check(true) -- restore event checking
1816end
1817
1818--------------------------------------------------------------------------------
1819
1820function RefreshIfNotGenerating()
1821 if not generating then Refresh() end
1822end
1823
1824--------------------------------------------------------------------------------
1825
1826function SetActivePlane(orientation, pos)
1827 -- set the active plane; default is an XY plane half way along the Z axis
1828 activeplane = orientation or "XY"
1829 activepos = pos or 0
1830 active = {}
1831 local M = N-1
1832 if activeplane == "XY" then
1833 local z = N//2 + activepos
1834 local zNN = z * N * N
1835 for y = 0, M do
1836 local yN = y * N
1837 for x = 0, M do
1838 active[ x + yN + zNN ] = true
1839 end
1840 end
1841 minactivex = 0
1842 maxactivex = M
1843 minactivey = 0
1844 maxactivey = M
1845 minactivez = z
1846 maxactivez = z
1847 elseif activeplane == "YZ" then
1848 local x = N//2 + activepos
1849 for z = 0, M do
1850 for y = 0, M do
1851 active[ x + N * (y + N * z) ] = true
1852 end
1853 end
1854 minactivex = x
1855 maxactivex = x
1856 minactivey = 0
1857 maxactivey = M
1858 minactivez = 0
1859 maxactivez = M
1860 else -- activeplane == "XZ"
1861 local y = N//2 + activepos
1862 for z = 0, M do
1863 for x = 0, M do
1864 active[ x + N * (y + N * z) ] = true
1865 end
1866 end
1867 minactivex = 0
1868 maxactivex = M
1869 minactivey = y
1870 maxactivey = y
1871 minactivez = 0
1872 maxactivez = M
1873 end
1874end
1875
1876--------------------------------------------------------------------------------
1877
1878function UpdateStartButton()
1879 -- change label in ssbutton without changing the button's width,
1880 -- and also update 1st item in Control menu
1881 if generating then
1882 ssbutton.customcolor = STOP_COLOR
1883 ssbutton.darkcustomcolor = SELSTOP_COLOR
1884 ssbutton.setlabel("Stop", false)
1885 mbar.setitem(3, 1, "Stop Generating")
1886 else
1887 ssbutton.customcolor = START_COLOR
1888 ssbutton.darkcustomcolor = SELSTART_COLOR
1889 ssbutton.setlabel("Start", false)
1890 mbar.setitem(3, 1, "Start Generating")
1891 end
1892end
1893
1894--------------------------------------------------------------------------------
1895
1896function StopGenerating()
1897 if generating then
1898 generating = false
1899 UpdateStartButton()
1900 end
1901end
1902
1903--------------------------------------------------------------------------------
1904
1905function SaveState()
1906 -- return a table containing current state
1907 local state = {}
1908
1909 -- save current grid size
1910 state.saveN = N
1911
1912 -- save current active plane
1913 state.saveplane = activeplane
1914 state.savepos = activepos
1915
1916 -- save current cursor
1917 state.savecursor = currcursor
1918
1919 -- save current rule
1920 state.saverule = rulestring
1921
1922 -- save current step size
1923 state.savestep = stepsize
1924
1925 -- save current pattern
1926 state.savedirty = dirty
1927 state.savename = pattname
1928 state.savegencount = gencount
1929 state.savepopcount = popcount
1930 state.savecells = {}
1931 if popcount > 0 then
1932 for k,_ in pairs(grid1) do state.savecells[k] = 1 end
1933 end
1934 state.saveminx = minx
1935 state.saveminy = miny
1936 state.saveminz = minz
1937 state.savemaxx = maxx
1938 state.savemaxy = maxy
1939 state.savemaxz = maxz
1940 state.saveminimallive = minimal_live_bounds
1941
1942 -- save current selection
1943 state.saveselcount = selcount
1944 state.saveselected = {}
1945 if selcount > 0 then
1946 for k,_ in pairs(selected) do state.saveselected[k] = true end
1947 end
1948 state.saveminselx = minselx
1949 state.saveminsely = minsely
1950 state.saveminselz = minselz
1951 state.savemaxselx = maxselx
1952 state.savemaxsely = maxsely
1953 state.savemaxselz = maxselz
1954 state.saveminimalsel = minimal_sel_bounds
1955
1956 -- save current paste pattern
1957 state.savepcount = pastecount
1958 state.savepaste = {}
1959 if pastecount > 0 then
1960 for k,_ in pairs(pastepatt) do state.savepaste[k] = true end
1961 end
1962 state.saveminpastex = minpastex
1963 state.saveminpastey = minpastey
1964 state.saveminpastez = minpastez
1965 state.savemaxpastex = maxpastex
1966 state.savemaxpastey = maxpastey
1967 state.savemaxpastez = maxpastez
1968
1969 return state
1970end
1971
1972--------------------------------------------------------------------------------
1973
1974function RestoreState(state)
1975 -- restore state from given info (created earlier by SaveState)
1976
1977 -- restore grid size and active plane if necessary
1978 if N ~= state.saveN then
1979 SetGridSizeOnly(state.saveN)
1980 CreateAxes()
1981 -- active plane also depends on N
1982 SetActivePlane(state.saveplane, state.savepos)
1983 elseif activeplane ~= state.saveplane or activepos ~= state.savepos then
1984 SetActivePlane(state.saveplane, state.savepos)
1985 end
1986
1987 -- restore cursor (determines whether active plane is displayed)
1988 currcursor = state.savecursor
1989 if not arrow_cursor then ov("cursor "..currcursor) end
1990
1991 -- restore rule if necessary
1992 if rulestring ~= state.saverule then
1993 ParseRule(state.saverule)
1994 end
1995
1996 -- restore step size
1997 SetStepSize(state.savestep)
1998
1999 -- restore pattern
2000 dirty = state.savedirty
2001 pattname = state.savename
2002 gencount = state.savegencount
2003 popcount = state.savepopcount
2004 grid1 = {}
2005 if popcount > 0 then
2006 for k,_ in pairs(state.savecells) do grid1[k] = 1 end
2007 end
2008 minx = state.saveminx
2009 miny = state.saveminy
2010 minz = state.saveminz
2011 maxx = state.savemaxx
2012 maxy = state.savemaxy
2013 maxz = state.savemaxz
2014 minimal_live_bounds = state.saveminimallive
2015
2016 -- restore selection
2017 selcount = state.saveselcount
2018 selected = {}
2019 if selcount > 0 then
2020 for k,_ in pairs(state.saveselected) do selected[k] = true end
2021 end
2022 minselx = state.saveminselx
2023 minsely = state.saveminsely
2024 minselz = state.saveminselz
2025 maxselx = state.savemaxselx
2026 maxsely = state.savemaxsely
2027 maxselz = state.savemaxselz
2028 minimal_sel_bounds = state.saveminimalsel
2029
2030 -- restore paste pattern
2031 pastecount = state.savepcount
2032 pastepatt = {}
2033 if pastecount > 0 then
2034 for k,_ in pairs(state.savepaste) do pastepatt[k] = true end
2035 end
2036 minpastex = state.saveminpastex
2037 minpastey = state.saveminpastey
2038 minpastez = state.saveminpastez
2039 maxpastex = state.savemaxpastex
2040 maxpastey = state.savemaxpastey
2041 maxpastez = state.savemaxpastez
2042
2043 ovt{"setpattern3d", grid1, false} -- nicer not to clear history???
2044 update_grid = false
2045end
2046
2047--------------------------------------------------------------------------------
2048
2049function SameState(state)
2050 -- return true if given state matches the current state
2051 if N ~= state.saveN then return false end
2052 if activeplane ~= state.saveplane then return false end
2053 if activepos ~= state.savepos then return false end
2054 if currcursor ~= state.savecursor then return false end
2055 if rulestring ~= state.saverule then return false end
2056 if dirty ~= state.savedirty then return false end
2057 if pattname ~= state.savename then return false end
2058 if gencount ~= state.savegencount then return false end
2059 if popcount ~= state.savepopcount then return false end
2060 if selcount ~= state.saveselcount then return false end
2061 if pastecount ~= state.savepcount then return false end
2062
2063 -- note that we don't compare stepsize with state.savestep
2064 -- (we don't call RememberCurrentState when the user changes the step size)
2065
2066 for k,_ in pairs(state.savecells) do if not grid1[k] then return false end end
2067 for k,_ in pairs(state.saveselected) do if not selected[k] then return false end end
2068 for k,_ in pairs(state.savepaste) do if not pastepatt[k] then return false end end
2069
2070 return true
2071end
2072
2073--------------------------------------------------------------------------------
2074
2075function ClearUndoRedo()
2076 -- this might be called if a user script is running (eg. if it calls NewPattern)
2077 undostack = {}
2078 redostack = {}
2079 dirty = false
2080end
2081
2082--------------------------------------------------------------------------------
2083
2084function Undo()
2085 -- ignore if user script is running
2086 -- (scripts can call SaveState and RestoreState if they need to undo stuff)
2087 if scriptlevel > 0 then return end
2088
2089 if #undostack > 0 then
2090 StopGenerating()
2091 -- push current state onto redostack
2092 redostack[#redostack+1] = SaveState()
2093 -- pop state off undostack and restore it
2094 RestoreState( table.remove(undostack) )
2095 Refresh()
2096 end
2097end
2098
2099--------------------------------------------------------------------------------
2100
2101function Redo()
2102 -- ignore if user script is running
2103 if scriptlevel > 0 then return end
2104
2105 if #redostack > 0 then
2106 StopGenerating()
2107 -- push current state onto undostack
2108 undostack[#undostack+1] = SaveState()
2109 -- pop state off redostack and restore it
2110 RestoreState( table.remove(redostack) )
2111 Refresh()
2112 end
2113end
2114
2115--------------------------------------------------------------------------------
2116
2117function RememberCurrentState()
2118 -- ignore if user script is running
2119 if scriptlevel > 0 then return end
2120
2121 redostack = {}
2122 undostack[#undostack+1] = SaveState()
2123end
2124
2125--------------------------------------------------------------------------------
2126
2127function CheckIfGenerating()
2128 if generating and popcount > 0 and gencount > startcount then
2129 -- NextGeneration will be called soon
2130 RememberCurrentState()
2131 end
2132end
2133
2134--------------------------------------------------------------------------------
2135
2136function InitLiveBoundary()
2137 -- check for cells on grid edge
2138 liveedge = (minx == 0 or miny == 0 or minz == 0 or maxx == N-1 or maxy == N-1 or maxz == N-1)
2139
2140 -- reset boundary
2141 minx = math.maxinteger
2142 miny = math.maxinteger
2143 minz = math.maxinteger
2144 maxx = math.mininteger
2145 maxy = math.mininteger
2146 maxz = math.mininteger
2147 -- SetLiveCell/SetCellState/NextGeneration will update all values
2148
2149 -- set the flag for MinimizeLiveBoundary
2150 minimal_live_bounds = true
2151end
2152
2153--------------------------------------------------------------------------------
2154
2155function MinimizeLiveBoundary()
2156 if popcount > 0 and not minimal_live_bounds then
2157 InitLiveBoundary()
2158 -- minimal_live_bounds is now true
2159 local NN = N*N
2160 for k,_ in pairs(grid1) do
2161 -- grid1[k] is a live cell
2162 local x = k % N
2163 local y = k % NN
2164 if x < minx then minx = x end
2165 if y < miny then miny = y end
2166 if k < minz then minz = k end
2167 if x > maxx then maxx = x end
2168 if y > maxy then maxy = y end
2169 if k > maxz then maxz = k end
2170 end
2171 miny = miny // N
2172 maxy = maxy // N
2173 minz = minz // NN
2174 maxz = maxz // NN
2175 end
2176end
2177
2178--------------------------------------------------------------------------------
2179
2180function InitSelectionBoundary()
2181 minselx = math.maxinteger
2182 minsely = math.maxinteger
2183 minselz = math.maxinteger
2184 maxselx = math.mininteger
2185 maxsely = math.mininteger
2186 maxselz = math.mininteger
2187
2188 -- set the flag for MinimizeSelectionBoundary
2189 minimal_sel_bounds = true
2190end
2191
2192--------------------------------------------------------------------------------
2193
2194local function UpdateSelectionBoundary(x, y, z)
2195 if x < minselx then minselx = x end
2196 if y < minsely then minsely = y end
2197 if z < minselz then minselz = z end
2198 if x > maxselx then maxselx = x end
2199 if y > maxsely then maxsely = y end
2200 if z > maxselz then maxselz = z end
2201end
2202
2203--------------------------------------------------------------------------------
2204
2205function MinimizeSelectionBoundary()
2206 if selcount > 0 and not minimal_sel_bounds then
2207 -- find minimal bounding box of all selected cells
2208 InitSelectionBoundary()
2209 -- minimal_sel_bounds is now true
2210 local NN = N*N
2211 for k,_ in pairs(selected) do
2212 UpdateSelectionBoundary(k % N, (k // N) % N, k // NN)
2213 end
2214 end
2215end
2216
2217--------------------------------------------------------------------------------
2218
2219function ClearCells()
2220 grid1 = {}
2221 popcount = 0
2222 InitLiveBoundary()
2223 -- remove selection
2224 selcount = 0
2225 selected = {}
2226 InitSelectionBoundary()
2227 -- remove paste pattern
2228 pastecount = 0
2229 pastepatt = {}
2230 collectgarbage() -- good place to force a gc
2231end
2232
2233--------------------------------------------------------------------------------
2234
2235function MoveActivePlane(newpos, refresh)
2236 if currcursor ~= movecursor then
2237 local mid = N//2
2238 if newpos + mid < 0 then
2239 newpos = -mid
2240 elseif newpos + mid >= N then
2241 newpos = N-1-mid
2242 end
2243 if newpos == activepos then return end
2244
2245 -- note that refresh is false if called from DragActivePlane
2246 -- (in which case RememberCurrentState has already been called
2247 -- by StartDraggingPlane)
2248 if refresh then RememberCurrentState() end
2249
2250 -- use the same orientation
2251 SetActivePlane(activeplane, newpos)
2252
2253 if refresh then
2254 CheckIfGenerating()
2255 Refresh()
2256 end
2257 end
2258end
2259
2260--------------------------------------------------------------------------------
2261
2262function CycleActivePlane()
2263 if currcursor ~= movecursor then
2264 RememberCurrentState()
2265 -- cycle to next orientation of active plane
2266 if activeplane == "XY" then
2267 SetActivePlane("YZ", activepos)
2268 elseif activeplane == "YZ" then
2269 SetActivePlane("XZ", activepos)
2270 else -- activeplane == "XZ"
2271 SetActivePlane("XY", activepos)
2272 end
2273 CheckIfGenerating()
2274 Refresh()
2275 end
2276end
2277
2278--------------------------------------------------------------------------------
2279
2280local function SetSelection(x, y, z, sel)
2281 local pos = x + N * (y + N * z)
2282 if sel then
2283 if not selected[pos] then
2284 selected[pos] = true
2285 selcount = selcount + 1
2286 UpdateSelectionBoundary(x, y, z)
2287 end
2288 elseif selected[pos] then
2289 selected[pos] = nil
2290 selcount = selcount - 1
2291 -- tell MinimizeSelectionBoundary that it needs to update the selection boundary
2292 minimal_sel_bounds = false
2293 end
2294end
2295
2296--------------------------------------------------------------------------------
2297
2298local function SetCellState(x, y, z, state)
2299 local pos = x + N * (y + N * z)
2300 if state > 0 then
2301 if not grid1[pos] then
2302 grid1[pos] = state
2303 popcount = popcount + 1
2304 dirty = true
2305 update_grid = true
2306 -- boundary might expand
2307 if x < minx then minx = x end
2308 if y < miny then miny = y end
2309 if z < minz then minz = z end
2310 if x > maxx then maxx = x end
2311 if y > maxy then maxy = y end
2312 if z > maxz then maxz = z end
2313 end
2314 else
2315 -- state is 0
2316 if grid1[pos] then
2317 -- kill a live cell
2318 grid1[pos] = nil
2319 popcount = popcount - 1
2320 dirty = true
2321 update_grid = true
2322 -- tell MinimizeLiveBoundary that it needs to update the live boundary
2323 minimal_live_bounds = false
2324 end
2325 end
2326end
2327
2328--------------------------------------------------------------------------------
2329
2330local function SetLiveCell(x, y, z)
2331 -- this must only be called to create a live cell
2332 grid1[ x + N * (y + N * z) ] = 1
2333 popcount = popcount + 1
2334 dirty = true
2335 update_grid = true
2336 -- boundary might expand
2337 if x < minx then minx = x end
2338 if y < miny then miny = y end
2339 if z < minz then minz = z end
2340 if x > maxx then maxx = x end
2341 if y > maxy then maxy = y end
2342 if z > maxz then maxz = z end
2343end
2344
2345--------------------------------------------------------------------------------
2346
2347function SaveCells()
2348 -- return an array of live cell positions relative to mid cell
2349 local livecells = {}
2350 if popcount > 0 then
2351 local mid = N//2
2352 local NN = N*N
2353 for k,_ in pairs(grid1) do
2354 -- grid1[k] is a live cell
2355 local x = k % N
2356 local y = (k // N) % N
2357 local z = k // NN
2358 livecells[#livecells+1] = {x-mid, y-mid, z-mid}
2359 end
2360 end
2361 return livecells
2362end
2363
2364--------------------------------------------------------------------------------
2365
2366function RestoreCells(livecells)
2367 -- restore pattern saved earlier by SaveCells
2368 -- (note that grid must currently be empty)
2369 if popcount > 0 then
2370 g.warn("Bug in RestoreCells: grid is not empty!")
2371 end
2372 local clipped = 0
2373 local mid = N//2
2374 for _, xyz in ipairs(livecells) do
2375 local x, y, z = xyz[1]+mid, xyz[2]+mid, xyz[3]+mid
2376 if x >= 0 and x < N and
2377 y >= 0 and y < N and
2378 z >= 0 and z < N then
2379 SetLiveCell(x, y, z)
2380 else
2381 clipped = clipped + 1
2382 end
2383 end
2384 return clipped
2385end
2386
2387--------------------------------------------------------------------------------
2388
2389function AllDead()
2390 -- this function is called at the start of every NextGen* function
2391 if popcount == 0 then
2392 StopGenerating()
2393 message = "All cells are dead."
2394 Refresh()
2395 return true -- return from NextGen*
2396 else
2397 if gencount == startcount then
2398 -- remember starting state for later use in Reset()
2399 if scriptlevel > 0 then
2400 -- can't use undostack if user script is running
2401 startstate = SaveState()
2402 else
2403 -- starting state is on top of undostack
2404 startstate = undostack[#undostack]
2405 end
2406 end
2407 popcount = 0 -- incremented in NextGen*
2408 InitLiveBoundary() -- updated in NextGen*
2409 return false -- calculate the next generation
2410 end
2411end
2412
2413--------------------------------------------------------------------------------
2414
2415function NextGenStandard(single)
2416 if AllDead() then return end
2417
2418 local oldstep = stepsize
2419 if single then SetStepSize(1) end
2420 grid1, popcount, gencount, minx, maxx, miny, maxy, minz, maxz = ovt{"nextgen3d", gencount, liveedge}
2421 if single then SetStepSize(oldstep) end
2422 if popcount == 0 then StopGenerating() end
2423 Refresh()
2424end
2425
2426--------------------------------------------------------------------------------
2427
2428function NextGenBusyBoxes(single)
2429 if N%2 == 1 then
2430 -- BusyBoxes requires an even number grid size
2431 SetGridSize(N+1)
2432 end
2433 if AllDead() then return end
2434
2435 local oldstep = stepsize
2436 if single then SetStepSize(1) end
2437 grid1, popcount, gencount, minx, maxx, miny, maxy, minz, maxz = ovt{"nextgen3d", gencount}
2438 if single then SetStepSize(oldstep) end
2439 if popcount == 0 then StopGenerating() end
2440 Refresh()
2441end
2442
2443--------------------------------------------------------------------------------
2444
2445function NewPattern(title)
2446 pattname = title or "untitled"
2447 SetCursor(drawcursor)
2448 gencount = 0
2449 startcount = 0
2450 SetStepSize(1)
2451 StopGenerating()
2452 ClearCells()
2453 ClearUndoRedo()
2454
2455 ovt{"setpattern3d", grid1, true} -- clear history
2456 update_grid = false
2457
2458 -- avoid unnecessary work if user script calls NewPattern
2459 if scriptlevel == 0 then
2460 SetActivePlane() -- restore default active plane
2461 InitialView() -- calls Refresh
2462 end
2463end
2464
2465--------------------------------------------------------------------------------
2466
2467function ReadPattern(filepath)
2468 local f = io.open(filepath,"r")
2469 if not f then return "Failed to open file:\n"..filepath end
2470
2471 -- Lua's f:read("*l") doesn't detect CR as EOL so we do this ugly stuff:
2472 -- read entire file and convert any CRs to LFs
2473 local all = f:read("*a"):gsub("\r", "\n").."\n"
2474 local nextline = all:gmatch("(.-)\n")
2475
2476 f:close()
2477
2478 local line = nextline()
2479 if line == nil or not line:find("^3D") then
2480 return "Invalid RLE3 file (first line must start with 3D)."
2481 end
2482
2483 -- read pattern into a temporary grid in case an error occurs
2484 local tsize = MAXN
2485 local trule = DEFAULT_RULE
2486 local tgens = 0
2487 local tpop = 0
2488 local tminx = math.maxinteger
2489 local tminy = math.maxinteger
2490 local tminz = math.maxinteger
2491 local tmaxx = math.mininteger
2492 local tmaxy = math.mininteger
2493 local tmaxz = math.mininteger
2494 local tgrid = {}
2495 local x0, y0, z0 = 0, 0, 0
2496
2497 -- parse 1st line (format is "3D key=val key=val key=val ...")
2498 local keys_and_vals = { split(line) }
2499 -- keys_and_vals[1] == "3D"
2500 for i = 2, #keys_and_vals do
2501 local keyword, value = split(keys_and_vals[i],"=")
2502 if value == nil then
2503 -- ignore keyword
2504 elseif keyword == "version" then
2505 if value ~= "1" then
2506 return "Unexpected version: "..value
2507 end
2508 elseif keyword == "size" then
2509 tsize = tonumber(value) or MAXN
2510 if tsize < MINN then tsize = MINN end
2511 if tsize > MAXN then tsize = MAXN end
2512 elseif keyword == "pos" then
2513 x0, y0, z0 = split(value,",")
2514 x0 = tonumber(x0) or 0
2515 y0 = tonumber(y0) or 0
2516 z0 = tonumber(z0) or 0
2517 elseif keyword == "gen" then
2518 tgens = tonumber(value) or 0
2519 if tgens < 0 then tgens = 0 end
2520 end
2521 end
2522
2523 local wd, ht, dp
2524 local x, y, z = 0, 0, 0
2525 local runcount = 0
2526 local comments = ""
2527
2528 while true do
2529 line = nextline()
2530 if not line then break end
2531 local ch = line:sub(1,1)
2532 if #ch == 0 then
2533 -- ignore blank line
2534 elseif ch == "#" then
2535 comments = comments..line.."\n"
2536 elseif ch == "x" then
2537 -- parse header
2538 wd, ht, dp, trule = line:match("x=(.+) y=(.+) z=(.+) rule=(.+)$")
2539 wd = tonumber(wd)
2540 ht = tonumber(ht)
2541 dp = tonumber(dp)
2542 if wd and ht and dp then
2543 if max(wd, ht, dp) > tsize then
2544 return "The pattern size is bigger than the grid size!"
2545 end
2546 x = x0
2547 y = y0
2548 z = z0
2549 -- check that pattern is positioned within given grid size
2550 if x < 0 or x + wd > tsize or
2551 y < 0 or y + ht > tsize or
2552 z < 0 or z + dp > tsize then
2553 return "The pattern is positioned outside the grid!"
2554 end
2555 else
2556 return "Bad number in header line:\n"..line
2557 end
2558 local saverule = rulestring
2559 if not ParseRule(trule) then
2560 return "Unknown rule: "..trule
2561 end
2562 trule = rulestring
2563 -- restore rulestring etc in case there is a later error
2564 -- or we're doing a paste and want to ignore the specified rule
2565 ParseRule(saverule)
2566 else
2567 -- parse RLE3 data
2568 for i = 1, #line do
2569 ch = line:sub(i,i)
2570 if ch >= "0" and ch <= "9" then
2571 runcount = runcount * 10 + tonumber(ch)
2572 else
2573 if runcount == 0 then runcount = 1 end
2574 if ch == "b" then
2575 x = x + runcount
2576 elseif ch == "o" then
2577 repeat
2578 tgrid[ x + tsize * (y + tsize * z) ] = 1
2579 tpop = tpop + 1
2580 -- update boundary
2581 if x < tminx then tminx = x end
2582 if y < tminy then tminy = y end
2583 if z < tminz then tminz = z end
2584 if x > tmaxx then tmaxx = x end
2585 if y > tmaxy then tmaxy = y end
2586 if z > tmaxz then tmaxz = z end
2587 x = x + 1
2588 runcount = runcount - 1
2589 until runcount == 0
2590 elseif ch == "$" then
2591 x = x0
2592 y = y + runcount
2593 elseif ch == "/" then
2594 x = x0
2595 y = y0
2596 z = z + runcount
2597 elseif ch == "!" then
2598 break
2599 else
2600 return "Unexpected character: "..ch
2601 end
2602 runcount = 0
2603 end
2604 end
2605 end
2606 end
2607
2608 -- success
2609 local newpattern = {
2610 newsize = tsize,
2611 newrule = trule,
2612 newgens = tgens,
2613 newpop = tpop,
2614 newminx = tminx,
2615 newminy = tminy,
2616 newminz = tminz,
2617 newmaxx = tmaxx,
2618 newmaxy = tmaxy,
2619 newmaxz = tmaxz,
2620 newgrid = tgrid
2621 }
2622 return nil, newpattern, comments
2623end
2624
2625--------------------------------------------------------------------------------
2626
2627function UpdateCurrentGrid(newpattern)
2628 -- called by OpenPattern/OpenClipboard
2629 SetGridSizeOnly(newpattern.newsize)
2630 CreateAxes()
2631 ClearCells() -- resets grid1, popcount and selection info
2632
2633 grid1 = newpattern.newgrid
2634 popcount = newpattern.newpop
2635 minx = newpattern.newminx
2636 miny = newpattern.newminy
2637 minz = newpattern.newminz
2638 maxx = newpattern.newmaxx
2639 maxy = newpattern.newmaxy
2640 maxz = newpattern.newmaxz
2641 -- note that ClearCells has set minimal_live_bounds = true
2642
2643 ParseRule(newpattern.newrule) -- sets rulestring, survivals, births, NextGeneration
2644 gencount = newpattern.newgens
2645 startcount = gencount -- for Reset
2646 SetStepSize(1)
2647 StopGenerating()
2648 SetCursor(movecursor)
2649 ClearUndoRedo() -- dirty = false
2650
2651 ovt{"setpattern3d", grid1, true} -- clear history
2652 update_grid = false
2653
2654 if scriptlevel == 0 then
2655 SetActivePlane()
2656 InitialView() -- calls Refresh
2657 end
2658end
2659
2660--------------------------------------------------------------------------------
2661
2662function OpenPattern(filepath)
2663 if filepath then
2664 local err, newpattern, comments = ReadPattern(filepath)
2665 if err then
2666 g.warn(err, false)
2667 else
2668 -- pattern ok so use info in newpattern to update current grid;
2669 -- set pattname to file name at end of filepath
2670 pattname = filepath:match("^.+"..pathsep.."(.+)$")
2671 if #comments > 0 then
2672 message = comments
2673 end
2674 UpdateCurrentGrid(newpattern)
2675 end
2676 else
2677 -- prompt user for a .rle3 file to open
2678 local filetype = "RLE3 file (*.rle3)|*.rle3"
2679 local path = g.opendialog("Open a pattern", filetype, pattdir, "")
2680 if #path > 0 then
2681 -- update pattdir by stripping off the file name
2682 pattdir = path:gsub("[^"..pathsep.."]+$","")
2683 -- open the chosen pattern
2684 OpenPattern(path)
2685 end
2686 end
2687end
2688
2689--------------------------------------------------------------------------------
2690
2691function CopyClipboardToFile()
2692 -- create a temporary file containing clipboard text
2693 local filepath = g.getdir("temp").."clipboard.rle3"
2694 local f = io.open(filepath,"w")
2695 if not f then
2696 g.warn("Failed to create temporary clipboard file!", false)
2697 return nil
2698 end
2699 -- NOTE: we can't do f:write(string.gsub(g.getclipstr(),"\r","\n"))
2700 -- because gsub returns 2 results and we'd get count appended to file!
2701 local clip = string.gsub(g.getclipstr(),"\r","\n")
2702 f:write(clip)
2703 f:close()
2704 return filepath
2705end
2706
2707--------------------------------------------------------------------------------
2708
2709function OpenClipboard()
2710 local filepath = CopyClipboardToFile()
2711 if filepath then
2712 local err, newpattern, comments = ReadPattern(filepath)
2713 if err then
2714 g.warn(err, false)
2715 else
2716 -- pattern ok so use info in newpattern to update current grid
2717 pattname = "clipboard"
2718 if #comments > 0 then
2719 message = comments
2720 end
2721 UpdateCurrentGrid(newpattern)
2722 end
2723 end
2724end
2725
2726--------------------------------------------------------------------------------
2727
2728function PatternHeader(xpos, ypos, zpos)
2729 -- return RLE3 header line
2730 local header = "3D version=1 size="..N
2731 if popcount > 0 and (xpos ~= 0 or ypos ~= 0 or zpos ~= 0) then
2732 header = header..string.format(" pos=%d,%d,%d", xpos, ypos, zpos)
2733 end
2734 if gencount > 0 then
2735 header = header.." gen="..gencount
2736 end
2737 -- note that we let caller append \n if necessary
2738 return header
2739end
2740
2741--------------------------------------------------------------------------------
2742
2743function WritePattern(filepath, comments)
2744 local f = io.open(filepath,"w")
2745 if not f then return "Failed to create RLE3 file:\n"..filepath end
2746
2747 MinimizeLiveBoundary()
2748 f:write(PatternHeader(minx, miny, minz), "\n")
2749
2750 if #comments > 0 then
2751 -- each comment line should start with # and end with \n
2752 f:write(comments)
2753 end
2754 if popcount == 0 then
2755 f:write(string.format("x=0 y=0 z=0 rule=%s\n", rulestring))
2756 f:write("!\n")
2757 else
2758 local wd = maxx - minx + 1
2759 local ht = maxy - miny + 1
2760 local dp = maxz - minz + 1
2761 f:write(string.format("x=%d y=%d z=%d rule=%s\n", wd, ht, dp, rulestring))
2762
2763 local line = ""
2764 local orun = 0
2765 local brun = 0
2766 local dollrun = 0
2767 local slashrun = 0
2768
2769 local function AddRun(count, ch)
2770 if #line >= 67 then f:write(line,"\n"); line = "" end
2771 if count > 2 then
2772 line = line..count..ch
2773 elseif count == 2 then
2774 line = line..ch..ch
2775 else
2776 line = line..ch
2777 end
2778 end
2779
2780 -- traverse all cells within live boundary sorted by Z,Y,X coords
2781 for z = minz, maxz do
2782 for y = miny, maxy do
2783 for x = minx, maxx do
2784 if grid1[ x + N * (y + N * z) ] then
2785 -- live cell
2786 orun = orun + 1
2787 if slashrun > 0 then
2788 AddRun(slashrun, "/")
2789 slashrun = 0
2790 end
2791 if dollrun > 0 then
2792 AddRun(dollrun, "$")
2793 dollrun = 0
2794 end
2795 if brun > 0 then
2796 AddRun(brun, "b")
2797 brun = 0
2798 end
2799 else
2800 -- dead cell
2801 brun = brun + 1
2802 if orun > 0 then
2803 if slashrun > 0 then
2804 AddRun(slashrun, "/")
2805 slashrun = 0
2806 end
2807 if dollrun > 0 then
2808 AddRun(dollrun, "$")
2809 dollrun = 0
2810 end
2811 AddRun(orun, "o")
2812 orun = 0
2813 end
2814 end
2815 end
2816 if orun > 0 then
2817 if slashrun > 0 then
2818 AddRun(slashrun, "/")
2819 slashrun = 0
2820 end
2821 if dollrun > 0 then
2822 AddRun(dollrun, "$")
2823 dollrun = 0
2824 end
2825 AddRun(orun, "o")
2826 orun = 0
2827 end
2828 brun = 0
2829 dollrun = dollrun + 1
2830 end
2831 dollrun = 0
2832 if z < maxz then
2833 slashrun = slashrun + 1
2834 else
2835 if #line >= 70 then f:write(line,"\n"); line = "" end
2836 line = line.."!"
2837 end
2838 end
2839 if #line > 0 then f:write(line,"\n") end
2840 end
2841 f:close()
2842 return nil -- success
2843end
2844
2845--------------------------------------------------------------------------------
2846
2847function GetComments(f)
2848 local comments = ""
2849
2850 -- Lua's f:read("*l") doesn't detect CR as EOL so we do this ugly stuff:
2851 -- read entire file and convert any CRs to LFs
2852 local all = f:read("*a"):gsub("\r", "\n").."\n"
2853 local nextline = all:gmatch("(.-)\n")
2854
2855 while true do
2856 local line = nextline()
2857 if not line then break end
2858 local ch = line:sub(1,1)
2859 if ch == "#" then
2860 comments = comments..line.."\n"
2861 elseif ch == "x" then
2862 -- end of RLE3 header info
2863 break
2864 end
2865 end
2866
2867 return comments
2868end
2869
2870--------------------------------------------------------------------------------
2871
2872function SavePattern(filepath)
2873 if filepath then
2874 -- if filepath exists then extract any comment lines from the header
2875 -- info and copy them into the new file
2876 local comments = ""
2877 local f = io.open(filepath,"r")
2878 if f then
2879 comments = GetComments(f)
2880 f:close()
2881 end
2882 local err = WritePattern(filepath, comments)
2883 if err then
2884 g.warn(err, false)
2885 else
2886 -- set pattname to file name at end of filepath
2887 pattname = filepath:match("^.+"..pathsep.."(.+)$")
2888 dirty = false
2889 Refresh()
2890 end
2891 else
2892 -- prompt user for file name and location
2893 local filetype = "RLE3 file (*.rle3)|*.rle3"
2894 local path = g.savedialog("Save pattern", filetype, pattdir, pattname)
2895 if #path > 0 then
2896 -- update pattdir by stripping off the file name
2897 pattdir = path:gsub("[^"..pathsep.."]+$","")
2898 -- ensure file name ends with ".rle3"
2899 if not path:find("%.rle3$") then path = path..".rle3" end
2900 -- save the current pattern
2901 SavePattern(path)
2902 end
2903 end
2904end
2905
2906--------------------------------------------------------------------------------
2907
2908function CallScript(func, fromclip)
2909 -- avoid infinite recursion
2910 if scriptlevel == 100 then
2911 g.warn("Script is too recursive!", false)
2912 return
2913 end
2914
2915 if scriptlevel == 0 then
2916 RememberCurrentState() -- #undostack is > 0
2917 EnableControls(false) -- disable most menu items and buttons
2918 end
2919
2920 scriptlevel = scriptlevel + 1
2921 local status, err = pcall(func)
2922 scriptlevel = scriptlevel - 1
2923
2924 if scriptlevel == 0 then
2925 -- note that if the script called NewPattern/RandomPattern/OpenPattern
2926 -- or any other function that called ClearUndoRedo then the undostack
2927 -- is empty and dirty should be false
2928 if #undostack == 0 then
2929 -- a later SetCell call might have set dirty to true, so reset it
2930 dirty = false
2931 if gencount > startcount then
2932 -- script called Step after NewPattern/RandomPattern/OpenPattern
2933 -- so push startstate onto undostack so user can Reset/Undo
2934 undostack[1] = startstate
2935 startstate.savedirty = false
2936 end
2937 elseif SameState(undostack[#undostack]) then
2938 -- script didn't change the current state so pop undostack
2939 table.remove(undostack)
2940 end
2941 end
2942
2943 if err then
2944 g.continue("")
2945 if err == "GOLLY: ABORT SCRIPT" then
2946 -- user hit escape
2947 message = "Script aborted."
2948 else
2949 if fromclip then
2950 g.warn("Runtime error in clipboard script:\n\n"..err, false)
2951 else
2952 g.warn("Runtime error in script:\n\n"..err, false)
2953 end
2954 end
2955 end
2956
2957 if scriptlevel == 0 then
2958 EnableControls(true) -- enable menu items and buttons that were disabled above
2959 CheckIfGenerating()
2960 Refresh() -- calls DrawToolBar
2961 end
2962end
2963
2964--------------------------------------------------------------------------------
2965
2966function RunScript(filepath)
2967 if filepath then
2968 local f = io.open(filepath, "r")
2969 if f then
2970 -- Lua's f:read("*l") doesn't detect CR as EOL so we do this ugly stuff:
2971 -- read entire file and convert any CRs to LFs
2972 local all = f:read("*a"):gsub("\r", "\n").."\n"
2973 local nextline = all:gmatch("(.-)\n")
2974 local line1 = nextline()
2975 f:close()
2976 if not (line1 and line1:find("3D.lua")) then
2977 g.warn("3D.lua was not found on first line of script.", false)
2978 return
2979 end
2980 else
2981 g.warn("Script file could not be opened:\n"..filepath, false)
2982 return
2983 end
2984 local func, msg = loadfile(filepath)
2985 if func then
2986 CallScript(func, false)
2987 else
2988 g.warn("Syntax error in script:\n\n"..msg, false)
2989 end
2990 else
2991 -- prompt user for a .lua file to run
2992 local filetype = "Lua file (*.lua)|*.lua"
2993 local path = g.opendialog("Choose a Lua script", filetype, scriptdir, "")
2994 if #path > 0 then
2995 -- update scriptdir by stripping off the file name
2996 scriptdir = path:gsub("[^"..pathsep.."]+$","")
2997 -- run the chosen script
2998 RunScript(path)
2999 end
3000 end
3001end
3002
3003--------------------------------------------------------------------------------
3004
3005function RunClipboard()
3006 local cliptext = g.getclipstr()
3007 local eol = cliptext:find("[\n\r]")
3008 if not (eol and cliptext:sub(1,eol):find("3D.lua")) then
3009 g.warn("3D.lua was not found on first line of clipboard.", false)
3010 return
3011 end
3012 local func, msg = load(cliptext)
3013 if func then
3014 CallScript(func, true)
3015 else
3016 g.warn("Syntax error in clipboard script:\n\n"..msg, false)
3017 end
3018end
3019
3020--------------------------------------------------------------------------------
3021
3022function SetStartupScript()
3023 -- prompt user for a .lua file to run automatically when 3D.lua starts up
3024 local filetype = "Lua file (*.lua)|*.lua"
3025 local path = g.opendialog("Select your startup script", filetype, scriptdir, "")
3026 if #path > 0 then
3027 -- update scriptdir by stripping off the file name
3028 scriptdir = path:gsub("[^"..pathsep.."]+$","")
3029 startup = path
3030 -- the above path will be saved by WriteSettings
3031 end
3032end
3033
3034--------------------------------------------------------------------------------
3035
3036function RandomPattern(percentage, fill, sphere)
3037 local function getperc()
3038 local initstring = randstring
3039 ::try_again::
3040 local s = g.getstring("Enter density as a percentage (from 0 to 100).\n"..
3041 "Append \"f\" to fill the grid.\n"..
3042 "Append \"s\" to create a sphere.",
3043 initstring, "Random pattern")
3044 initstring = s
3045 fill = s:find("f")
3046 sphere = s:find("s")
3047 s = s:gsub("[fs]","")
3048 if s:find("[^%d]") then
3049 g.warn("Only digits and the letters f and s are allowed.\nTry again.")
3050 goto try_again
3051 end
3052 if validint(s) and (tonumber(s) >= 0) and (tonumber(s) <= 100) then
3053 perc = tonumber(s)
3054 randstring = initstring
3055 else
3056 g.warn("Percentage must be an integer from 0 to 100.\nTry again.")
3057 goto try_again
3058 end
3059 end
3060
3061 if percentage then
3062 perc = percentage
3063 if perc < 0 then perc = 0 end
3064 if perc > 100 then perc = 100 end
3065 else
3066 -- prompt user for the percentage and fill/sphere options;
3067 -- if user hits Cancel button we want to avoid aborting script
3068 local status, err = pcall(getperc)
3069 if err then
3070 g.continue("") -- don't show error when script finishes
3071 return
3072 end
3073 end
3074
3075 pattname = "untitled"
3076 SetCursor(movecursor)
3077 gencount = 0
3078 startcount = 0
3079 SetStepSize(1)
3080 StopGenerating()
3081 ClearCells()
3082
3083 local minval, maxval
3084 if fill or N < 8 then
3085 minval = 0
3086 maxval = N-1
3087 else
3088 minval = N//8
3089 maxval = (N-1) - minval
3090 end
3091 if sphere then
3092 local mid = N//2
3093 if N % 2 == 0 then mid = mid - 0.5 end -- ensure symmetry for even N
3094 local rsq = (maxval-minval+1)/2.0
3095 rsq = round(rsq*rsq)
3096 for z = minval, maxval do
3097 local dz = z-mid
3098 local dz2 = round(dz*dz)
3099 for y = minval, maxval do
3100 local dy = y-mid
3101 local dy2 = round(dy*dy)
3102 for x = minval, maxval do
3103 local dx = x-mid
3104 local d = round(dx*dx) + dy2 + dz2
3105 if d <= rsq and rand(0,99) < perc then
3106 SetLiveCell(x, y, z)
3107 end
3108 end
3109 end
3110 end
3111 else
3112 for z = minval, maxval do
3113 for y = minval, maxval do
3114 for x = minval, maxval do
3115 if rand(0,99) < perc then
3116 SetLiveCell(x, y, z)
3117 end
3118 end
3119 end
3120 end
3121 end
3122
3123 ClearUndoRedo()
3124
3125 ovt{"setpattern3d", grid1, true} -- clear history
3126 update_grid = false
3127
3128 if scriptlevel == 0 then
3129 SetActivePlane()
3130 InitialView() -- calls Refresh
3131 end
3132end
3133
3134--------------------------------------------------------------------------------
3135
3136function GetSelectedCells()
3137 -- return an array of selected cell positions relative to mid cell
3138 local selcells = {}
3139 if selcount > 0 then
3140 local mid = N//2
3141 local NN = N*N
3142 for k,_ in pairs(selected) do
3143 -- selected[k] is a selected cell
3144 local x = k % N
3145 local y = (k // N) % N
3146 local z = k // NN
3147 selcells[#selcells+1] = {x-mid, y-mid, z-mid}
3148 end
3149 end
3150 return selcells
3151end
3152
3153--------------------------------------------------------------------------------
3154
3155function GetSelectedLiveCells()
3156 -- return an array of selected *live* cell positions relative to mid cell
3157 local livecells = {}
3158 if selcount > 0 then
3159 local mid = N//2
3160 local NN = N*N
3161 for k,_ in pairs(grid1) do
3162 -- grid1[k] is a live cell
3163 if selected[k] then
3164 local x = k % N
3165 local y = (k // N) % N
3166 local z = k // NN
3167 livecells[#livecells+1] = {x-mid, y-mid, z-mid}
3168 end
3169 end
3170 end
3171 return livecells
3172end
3173
3174--------------------------------------------------------------------------------
3175
3176function GetPasteCells()
3177 -- return an array of paste cell positions relative to mid cell
3178 local pcells = {}
3179 if pastecount > 0 then
3180 local mid = N//2
3181 local NN = N*N
3182 for k,_ in pairs(pastepatt) do
3183 -- pastepatt[k] is in paste pattern
3184 local x = k % N
3185 local y = (k // N) % N
3186 local z = k // NN
3187 pcells[#pcells+1] = {x-mid, y-mid, z-mid}
3188 end
3189 end
3190 return pcells
3191end
3192
3193--------------------------------------------------------------------------------
3194
3195function SetTemporaryGridSize(newsize)
3196 N = newsize
3197 -- do not call ovt{"setsize3d", N} here as newsize can be > MAXN
3198 MIDGRID = (N+1-(N%2))*HALFCELL
3199 MIDCELL = HALFCELL-MIDGRID
3200end
3201
3202--------------------------------------------------------------------------------
3203
3204function SetGridSizeOnly(newsize)
3205 N = newsize
3206 ovt{"setsize3d", N}
3207 MIDGRID = (N+1-(N%2))*HALFCELL
3208 MIDCELL = HALFCELL-MIDGRID
3209end
3210
3211--------------------------------------------------------------------------------
3212
3213function SetGridSize(newsize)
3214 -- change grid size to newsize or prompt user if newsize is nil
3215
3216 local function getsize()
3217 ::try_again::
3218 local s = g.getstring("Enter the new size (from "..MINN.." to "..MAXN.."):",
3219 tostring(N), "Grid size")
3220 if validint(s) and (tonumber(s) >= MINN) and (tonumber(s) <= MAXN) then
3221 newsize = tonumber(s)
3222 else
3223 g.warn("Grid size must be an integer from "..MINN.." to "..MAXN..".")
3224 -- note that if user hit the Cancel button then the next g.* call
3225 -- (in this case g.getstring) will cause pcall to abort with an error
3226 goto try_again
3227 end
3228 end
3229
3230 if newsize then
3231 if newsize < MINN then newsize = MINN end
3232 if newsize > MAXN then newsize = MAXN end
3233 else
3234 -- if user hits Cancel button we want to avoid aborting script
3235 local status, err = pcall(getsize)
3236 if err then
3237 g.continue("") -- don't show error when script finishes
3238 return
3239 end
3240 end
3241
3242 if newsize == N then return end
3243
3244 RememberCurrentState()
3245
3246 -- save current pattern as an array of positions relative to mid cell
3247 local livecells = SaveCells()
3248
3249 -- save any selected cells as an array of positions relative to mid cell
3250 local oldselcount = selcount
3251 local selcells = GetSelectedCells()
3252
3253 -- save any paste cells as an array of positions relative to mid cell
3254 local oldpastecount = pastecount
3255 local pcells = GetPasteCells()
3256
3257 SetGridSizeOnly(newsize)
3258 CreateAxes()
3259
3260 -- active plane may need adjusting
3261 local mid = N//2
3262 if (mid + activepos >= N) or (mid + activepos < 0) then
3263 activepos = 0
3264 end
3265 SetActivePlane(activeplane, activepos)
3266
3267 -- restore pattern, clipping any cells outside the new grid
3268 ClearCells()
3269 local olddirty = dirty
3270 local clipcount = RestoreCells(livecells)
3271 if clipcount > 0 then
3272 dirty = true
3273 update_grid = true
3274 else
3275 -- RestoreCells sets dirty true if there are live cells,
3276 -- but pattern hasn't really changed if no cells were clipped
3277 dirty = olddirty
3278 end
3279
3280 -- restore selection, clipping any cells outside the new grid
3281 local selclipped = 0
3282 selcount = 0
3283 selected = {}
3284 InitSelectionBoundary()
3285 if oldselcount > 0 then
3286 for _, xyz in ipairs(selcells) do
3287 local x, y, z = xyz[1]+mid, xyz[2]+mid, xyz[3]+mid
3288 if x >= 0 and x < N and
3289 y >= 0 and y < N and
3290 z >= 0 and z < N then
3291 selected[ x + N * (y + N * z) ] = true
3292 selcount = selcount + 1
3293 UpdateSelectionBoundary(x, y, z)
3294 else
3295 selclipped = selclipped + 1
3296 end
3297 end
3298 end
3299
3300 -- restore paste pattern, clipping any cells outside the new grid
3301 local pclipped = 0
3302 pastecount = 0
3303 pastepatt = {}
3304 minpastex = math.maxinteger
3305 minpastey = math.maxinteger
3306 minpastez = math.maxinteger
3307 maxpastex = math.mininteger
3308 maxpastey = math.mininteger
3309 maxpastez = math.mininteger
3310 if oldpastecount > 0 then
3311 for _, xyz in ipairs(pcells) do
3312 local x, y, z = xyz[1]+mid, xyz[2]+mid, xyz[3]+mid
3313 if x >= 0 and x < N and
3314 y >= 0 and y < N and
3315 z >= 0 and z < N then
3316 pastepatt[ x + N * (y + N * z) ] = true
3317 pastecount = pastecount + 1
3318 if x < minpastex then minpastex = x end
3319 if y < minpastey then minpastey = y end
3320 if z < minpastez then minpastez = z end
3321 if x > maxpastex then maxpastex = x end
3322 if y > maxpastey then maxpastey = y end
3323 if z > maxpastez then maxpastez = z end
3324 else
3325 pclipped = pclipped + 1
3326 end
3327 end
3328 end
3329
3330 if clipcount > 0 or selclipped > 0 or pclipped > 0 then
3331 message = ""
3332 if clipcount > 0 then message = "Clipped live cells = "..clipcount.."\n" end
3333 if selclipped > 0 then message = message.."Clipped selection cells = "..selclipped.."\n" end
3334 if pclipped > 0 then message = message.."Clipped paste cells = "..pclipped end
3335 end
3336
3337 -- reload the grid since it will have changed after resize
3338 ovt{"setpattern3d", grid1, true} -- also clear history
3339 update_grid = false
3340
3341 CheckIfGenerating()
3342 FitGrid() -- calls Refresh
3343end
3344
3345--------------------------------------------------------------------------------
3346
3347function ChangeRule()
3348 -- let user enter new rule as a string of the form 3Ds,s,.../b,b,...
3349 -- (the notation used at http://www.cse.sc.edu/~bays/d4d4d4/)
3350
3351 local function getrule()
3352 local newrule = rulestring
3353 ::try_again::
3354 newrule = g.getstring("Enter the new rule in the form 3Ds,s,s,.../b,b,b,...\n" ..
3355 "where s values are the neighbor counts for survival\n" ..
3356 "and b values are the neighbor counts for birth.\n" ..
3357 "\n" ..
3358 "Contiguous counts can be specified as a range,\n" ..
3359 "so a rule like 3D4,5,6,7,9/4,5,7 can be entered as\n" ..
3360 "3D4..7,9/4,5,7 (this is the canonical version).\n" ..
3361 "\n" ..
3362 "Append F for the 6-cell face neighborhood,\n" ..
3363 "or C for the 8-cell corner neighborhood,\n" ..
3364 "or E for the 12-cell edge neighborhood,\n" ..
3365 "or H for the 12-cell hexahedral neighborhood.\n" ..
3366 "\n" ..
3367 "Another rule you might like to try is BusyBoxes\n" ..
3368 "(just enter \"bb\", or \"bbw\" to wrap edges).\n",
3369 newrule, "Set rule")
3370 if not ParseRule(newrule) then goto try_again end
3371 end
3372
3373 local oldrule = rulestring
3374
3375 -- if user hits Cancel button we want to avoid aborting script
3376 local status, err = pcall(getrule)
3377 if err then
3378 g.continue("") -- don't show error when script finishes
3379 return
3380 end
3381
3382 if oldrule ~= rulestring then
3383 -- ParseRule has set rulestring so we need to temporarily switch
3384 -- it back to oldrule and call RememberCurrentState
3385 local newrule = rulestring
3386 rulestring = oldrule
3387 if newrule:find("^BusyBoxes") and N%2 == 1 then
3388 -- BusyBoxes requires an even numbered grid size
3389 SetGridSize(N+1)
3390 -- above calls RememberCurrentState()
3391 else
3392 RememberCurrentState()
3393 end
3394 rulestring = newrule
3395 CheckIfGenerating()
3396 Refresh()
3397 end
3398end
3399
3400--------------------------------------------------------------------------------
3401
3402function CreateRLE3Selection()
3403 -- convert selection to lines of RLE3 data
3404 -- (note that selcount > 0 and at least one live cell is selected)
3405 MinimizeSelectionBoundary()
3406 local wd = maxselx - minselx + 1
3407 local ht = maxsely - minsely + 1
3408 local dp = maxselz - minselz + 1
3409
3410 local lines = {}
3411 lines[1] = PatternHeader(minselx, minsely, minselz)
3412 lines[2] = string.format("x=%d y=%d z=%d rule=%s", wd, ht, dp, rulestring)
3413
3414 local line = ""
3415 local orun = 0
3416 local brun = 0
3417 local dollrun = 0
3418 local slashrun = 0
3419
3420 local function AddRun(count, ch)
3421 if #line >= 67 then lines[#lines+1] = line; line = "" end
3422 if count > 2 then
3423 line = line..count..ch
3424 elseif count == 2 then
3425 line = line..ch..ch
3426 else
3427 line = line..ch
3428 end
3429 end
3430
3431 -- traverse selected cells sorted by Z,Y,X coords
3432 for z = minselz, maxselz do
3433 for y = minsely, maxsely do
3434 for x = minselx, maxselx do
3435 local pos = x + N * (y + N * z)
3436 if selected[pos] and grid1[pos] then
3437 -- this is a selected live cell
3438 orun = orun + 1
3439 if slashrun > 0 then
3440 AddRun(slashrun, "/")
3441 slashrun = 0
3442 end
3443 if dollrun > 0 then
3444 AddRun(dollrun, "$")
3445 dollrun = 0
3446 end
3447 if brun > 0 then
3448 AddRun(brun, "b")
3449 brun = 0
3450 end
3451 else
3452 -- dead cell or unselected live cell
3453 brun = brun + 1
3454 if orun > 0 then
3455 if slashrun > 0 then
3456 AddRun(slashrun, "/")
3457 slashrun = 0
3458 end
3459 if dollrun > 0 then
3460 AddRun(dollrun, "$")
3461 dollrun = 0
3462 end
3463 AddRun(orun, "o")
3464 orun = 0
3465 end
3466 end
3467 end
3468 if orun > 0 then
3469 if slashrun > 0 then
3470 AddRun(slashrun, "/")
3471 slashrun = 0
3472 end
3473 if dollrun > 0 then
3474 AddRun(dollrun, "$")
3475 dollrun = 0
3476 end
3477 AddRun(orun, "o")
3478 orun = 0
3479 end
3480 brun = 0
3481 dollrun = dollrun + 1
3482 end
3483 dollrun = 0
3484 if z < maxselz then
3485 slashrun = slashrun + 1
3486 else
3487 if #line >= 70 then lines[#lines+1] = line; line = "" end
3488 line = line.."!"
3489 end
3490 end
3491 if #line > 0 then lines[#lines+1] = line end
3492
3493 return lines
3494end
3495
3496--------------------------------------------------------------------------------
3497
3498function CopySelection()
3499 if selcount > 0 then
3500 -- save the selected live cells as an RLE3 pattern in clipboard,
3501 -- but only if there is at least one live cell selected
3502 local livecells = GetSelectedLiveCells()
3503 if #livecells == 0 then
3504 message = "There are no live cells selected."
3505 Refresh()
3506 return false
3507 end
3508 local lines = CreateRLE3Selection()
3509 -- append empty string so we get \n at end of last line
3510 lines[#lines+1] = ""
3511 g.setclipstr(table.concat(lines,"\n"))
3512 return true
3513 else
3514 return false
3515 end
3516end
3517
3518--------------------------------------------------------------------------------
3519
3520function ClearSelection()
3521 if selcount > 0 then
3522 RememberCurrentState()
3523 -- kill all selected live cells
3524 for k,_ in pairs(selected) do
3525 -- selected[k] is a selected cell (live or dead)
3526 if grid1[k] then
3527 grid1[k] = nil
3528 popcount = popcount - 1
3529 dirty = true
3530 update_grid = true
3531 minimal_live_bounds = false
3532 end
3533 end
3534 CheckIfGenerating()
3535 Refresh()
3536 end
3537end
3538
3539--------------------------------------------------------------------------------
3540
3541function ClearOutside()
3542 if selcount > 0 then
3543 RememberCurrentState()
3544 -- kill all unselected live cells
3545 for k,_ in pairs(grid1) do
3546 -- grid1[k] is a live cell
3547 if not selected[k] then
3548 grid1[k] = nil
3549 popcount = popcount - 1
3550 dirty = true
3551 update_grid = true
3552 minimal_live_bounds = false
3553 end
3554 end
3555 CheckIfGenerating()
3556 Refresh()
3557 end
3558end
3559
3560--------------------------------------------------------------------------------
3561
3562function CutSelection()
3563 if selcount > 0 then
3564 -- save the selected live cells as an RLE3 pattern in clipboard
3565 -- then kill them
3566 if CopySelection() then
3567 ClearSelection() -- calls RememberCurrentState and Refresh
3568 return true
3569 end
3570 end
3571 return false
3572end
3573
3574--------------------------------------------------------------------------------
3575
3576function CancelSelection()
3577 if selcount > 0 then
3578 RememberCurrentState()
3579 selcount = 0
3580 selected = {}
3581 InitSelectionBoundary()
3582 CheckIfGenerating()
3583 Refresh()
3584 end
3585end
3586
3587--------------------------------------------------------------------------------
3588
3589function SelectAll()
3590 if popcount > 0 then
3591 RememberCurrentState()
3592 selcount = 0
3593 selected = {}
3594 for k,_ in pairs(grid1) do
3595 selected[k] = true
3596 selcount = selcount + 1
3597 end
3598 -- selection boundary matches live cell boundary
3599 minselx = minx
3600 minsely = miny
3601 minselz = minz
3602 maxselx = maxx
3603 maxsely = maxy
3604 maxselz = maxz
3605 minimal_sel_bounds = minimal_live_bounds
3606 CheckIfGenerating()
3607 Refresh()
3608 else
3609 -- there are no live cells so remove any existing selection
3610 CancelSelection()
3611 end
3612end
3613
3614--------------------------------------------------------------------------------
3615
3616function FlipSelectionX()
3617 if selcount > 0 then
3618 RememberCurrentState()
3619 -- reflect selected cells' X coords across YZ plane thru middle of selection
3620 MinimizeSelectionBoundary()
3621 local fmidx = minselx + (maxselx - minselx) / 2
3622 local cells = {}
3623 local NN = N*N
3624 for k,_ in pairs(selected) do
3625 local x = k % N
3626 local y = (k // N) % N
3627 local z = k // NN
3628 cells[#cells+1] = {round(fmidx*2) - x, y, z, grid1[k]}
3629 if grid1[k] then
3630 grid1[k] = nil
3631 popcount = popcount - 1
3632 -- SetLiveCell below will set dirty = true
3633 minimal_live_bounds = false
3634 end
3635 end
3636 selected = {}
3637 -- flip doesn't change selection boundary so no need to call
3638 -- InitSelectionBoundary and UpdateSelectionBoundary
3639 for _,xyzs in ipairs(cells) do
3640 local x, y, z, live = xyzs[1], xyzs[2], xyzs[3], xyzs[4]
3641 local k = x + N * (y + N * z)
3642 selected[k] = true
3643 if live and not grid1[k] then
3644 -- best to use OR mode for selection actions
3645 SetLiveCell(x, y, z)
3646 end
3647 end
3648 CheckIfGenerating()
3649 Refresh()
3650 end
3651end
3652
3653--------------------------------------------------------------------------------
3654
3655function FlipSelectionY()
3656 if selcount > 0 then
3657 RememberCurrentState()
3658 -- reflect selected cells' Y coords across XZ plane thru middle of selection
3659 MinimizeSelectionBoundary()
3660 local fmidy = minsely + (maxsely - minsely) / 2
3661 local cells = {}
3662 local NN = N*N
3663 for k,_ in pairs(selected) do
3664 local x = k % N
3665 local y = (k // N) % N
3666 local z = k // NN
3667 cells[#cells+1] = {x, round(fmidy*2) - y, z, grid1[k]}
3668 if grid1[k] then
3669 grid1[k] = nil
3670 popcount = popcount - 1
3671 -- SetLiveCell below will set dirty = true
3672 minimal_live_bounds = false
3673 end
3674 end
3675 selected = {}
3676 -- flip doesn't change selection boundary so no need to call
3677 -- InitSelectionBoundary and UpdateSelectionBoundary
3678 for _,xyzs in ipairs(cells) do
3679 local x, y, z, live = xyzs[1], xyzs[2], xyzs[3], xyzs[4]
3680 local k = x + N * (y + N * z)
3681 selected[k] = true
3682 if live and not grid1[k] then
3683 -- best to use OR mode for selection actions
3684 SetLiveCell(x, y, z)
3685 end
3686 end
3687 CheckIfGenerating()
3688 Refresh()
3689 end
3690end
3691
3692--------------------------------------------------------------------------------
3693
3694function FlipSelectionZ()
3695 if selcount > 0 then
3696 RememberCurrentState()
3697 -- reflect selected cells' Z coords across XY plane thru middle of selection
3698 MinimizeSelectionBoundary()
3699 local fmidz = minselz + (maxselz - minselz) / 2
3700 local cells = {}
3701 local NN = N*N
3702 for k,_ in pairs(selected) do
3703 local x = k % N
3704 local y = (k // N) % N
3705 local z = k // NN
3706 cells[#cells+1] = {x, y, round(fmidz*2) - z, grid1[k]}
3707 if grid1[k] then
3708 grid1[k] = nil
3709 popcount = popcount - 1
3710 -- SetLiveCell below will set dirty = true
3711 minimal_live_bounds = false
3712 end
3713 end
3714 selected = {}
3715 -- flip doesn't change selection boundary so no need to call
3716 -- InitSelectionBoundary and UpdateSelectionBoundary
3717 for _,xyzs in ipairs(cells) do
3718 local x, y, z, live = xyzs[1], xyzs[2], xyzs[3], xyzs[4]
3719 local k = x + N * (y + N * z)
3720 selected[k] = true
3721 if live and not grid1[k] then
3722 -- best to use OR mode for selection actions
3723 SetLiveCell(x, y, z)
3724 end
3725 end
3726 CheckIfGenerating()
3727 Refresh()
3728 end
3729end
3730
3731--------------------------------------------------------------------------------
3732
3733function RotateSelectionX()
3734 if selcount > 0 then
3735 RememberCurrentState()
3736 -- rotate selection clockwise about its X axis by 90 degrees
3737 MinimizeSelectionBoundary()
3738 local fmidy = minsely + (maxsely - minsely) // 2
3739 local fmidz = minselz + (maxselz - minselz) // 2
3740 local y0 = fmidy - fmidz
3741 local z0 = fmidz + fmidy + (maxselz - minselz) % 2 -- avoids drift
3742 local cells = {}
3743 local NN = N*N
3744 for k,_ in pairs(selected) do
3745 local x = k % N
3746 local y = (k // N) % N
3747 local z = k // NN
3748 cells[#cells+1] = {x, (y0+z) % N, (z0-y) % N, grid1[k]}
3749 if grid1[k] then
3750 grid1[k] = nil
3751 popcount = popcount - 1
3752 -- SetLiveCell below will set dirty = true
3753 minimal_live_bounds = false
3754 end
3755 end
3756 selected = {}
3757 InitSelectionBoundary()
3758 for _,xyzs in ipairs(cells) do
3759 local x, y, z, live = xyzs[1], xyzs[2], xyzs[3], xyzs[4]
3760 local k = x + N * (y + N * z)
3761 selected[k] = true
3762 UpdateSelectionBoundary(x, y, z)
3763 if live and not grid1[k] then
3764 -- best to use OR mode for selection actions
3765 SetLiveCell(x, y, z)
3766 end
3767 end
3768 CheckIfGenerating()
3769 Refresh()
3770 end
3771end
3772
3773--------------------------------------------------------------------------------
3774
3775function RotateSelectionY()
3776 if selcount > 0 then
3777 RememberCurrentState()
3778 -- rotate selection clockwise about its Y axis by 90 degrees
3779 MinimizeSelectionBoundary()
3780 local fmidx = minselx + (maxselx - minselx) // 2
3781 local fmidz = minselz + (maxselz - minselz) // 2
3782 local x0 = fmidx + fmidz + (maxselx - minselx) % 2 -- avoids drift
3783 local z0 = fmidz - fmidx
3784 local cells = {}
3785 local NN = N*N
3786 for k,_ in pairs(selected) do
3787 local x = k % N
3788 local y = (k // N) % N
3789 local z = k // NN
3790 cells[#cells+1] = {(x0-z) % N, y, (z0+x) % N, grid1[k]}
3791 if grid1[k] then
3792 grid1[k] = nil
3793 popcount = popcount - 1
3794 -- SetLiveCell below will set dirty = true
3795 minimal_live_bounds = false
3796 end
3797 end
3798 selected = {}
3799 InitSelectionBoundary()
3800 for _,xyzs in ipairs(cells) do
3801 local x, y, z, live = xyzs[1], xyzs[2], xyzs[3], xyzs[4]
3802 local k = x + N * (y + N * z)
3803 selected[k] = true
3804 UpdateSelectionBoundary(x, y, z)
3805 if live and not grid1[k] then
3806 -- best to use OR mode for selection actions
3807 SetLiveCell(x, y, z)
3808 end
3809 end
3810 CheckIfGenerating()
3811 Refresh()
3812 end
3813end
3814
3815--------------------------------------------------------------------------------
3816
3817function RotateSelectionZ()
3818 if selcount > 0 then
3819 RememberCurrentState()
3820 -- rotate selection clockwise about its Z axis by 90 degrees
3821 MinimizeSelectionBoundary()
3822 local fmidx = minselx + (maxselx - minselx) // 2
3823 local fmidy = minsely + (maxsely - minsely) // 2
3824 local x0 = fmidx - fmidy
3825 local y0 = fmidy + fmidx + (maxsely - minsely) % 2 -- avoids drift
3826 local cells = {}
3827 local NN = N*N
3828 for k,_ in pairs(selected) do
3829 local x = k % N
3830 local y = (k // N) % N
3831 local z = k // NN
3832 cells[#cells+1] = {(x0+y) % N, (y0-x) % N, z, grid1[k]}
3833 if grid1[k] then
3834 grid1[k] = nil
3835 popcount = popcount - 1
3836 -- SetLiveCell below will set dirty = true
3837 minimal_live_bounds = false
3838 end
3839 end
3840 selected = {}
3841 InitSelectionBoundary()
3842 for _,xyzs in ipairs(cells) do
3843 local x, y, z, live = xyzs[1], xyzs[2], xyzs[3], xyzs[4]
3844 local k = x + N * (y + N * z)
3845 selected[k] = true
3846 UpdateSelectionBoundary(x, y, z)
3847 if live and not grid1[k] then
3848 -- best to use OR mode for selection actions
3849 SetLiveCell(x, y, z)
3850 end
3851 end
3852 CheckIfGenerating()
3853 Refresh()
3854 end
3855end
3856
3857--------------------------------------------------------------------------------
3858
3859function CancelPaste()
3860 if pastecount > 0 then
3861 RememberCurrentState()
3862 pastecount = 0
3863 pastepatt = {}
3864 CheckIfGenerating()
3865 Refresh()
3866 end
3867end
3868
3869--------------------------------------------------------------------------------
3870
3871function Read2DPattern(filepath)
3872 -- call g.load via pcall to catch any error
3873 local cellarray
3874 local function loadcells()
3875 cellarray = g.load(filepath)
3876 -- if g.load detected an error then force pcall to abort
3877 g.doevent("")
3878 end
3879 local status, err = pcall(loadcells)
3880 if err then
3881 g.continue("")
3882 return err
3883 end
3884
3885 -- copy cells from cellarray into a temporary grid
3886 local tsize = MAXN
3887 local trule = rulestring
3888 local tgens = 0
3889 local tpop = 0
3890 local tminx = math.maxinteger
3891 local tminy = math.maxinteger
3892 local tminz = 0
3893 local tmaxx = math.mininteger
3894 local tmaxy = math.mininteger
3895 local tmaxz = 0
3896 local tgrid = {}
3897
3898 -- determine if cellarray is one-state or multi-state
3899 local len = #cellarray
3900 local inc = 2
3901 if (len & 1) == 1 then
3902 inc = 3
3903 -- ignore padding int if present
3904 if len % 3 == 1 then len = len - 1 end
3905 end
3906
3907 local M = tsize-1
3908 for i = 1, len, inc do
3909 local x = cellarray[i]
3910 local y = M - cellarray[i+1] -- invert y coord
3911 if x < 0 or x >= tsize or
3912 y < 0 or y >= tsize then
3913 return "too big" -- detected by caller
3914 end
3915 -- note that z is 0
3916 tgrid[x + tsize * y] = 1
3917 tpop = tpop + 1
3918 -- update boundary (tminz = tmaxz = 0)
3919 if x < tminx then tminx = x end
3920 if y < tminy then tminy = y end
3921 if x > tmaxx then tmaxx = x end
3922 if y > tmaxy then tmaxy = y end
3923 end
3924
3925 -- success
3926 local newpattern = {
3927 newsize = tsize,
3928 newrule = trule,
3929 newgens = tgens,
3930 newpop = tpop,
3931 newminx = tminx,
3932 newminy = tminy,
3933 newminz = tminz,
3934 newmaxx = tmaxx,
3935 newmaxy = tmaxy,
3936 newmaxz = tmaxz,
3937 newgrid = tgrid
3938 }
3939 return nil, newpattern
3940end
3941
3942--------------------------------------------------------------------------------
3943
3944function Paste()
3945 -- if a paste pattern already exists then cancel it
3946 local savedstate = false
3947 if pastecount > 0 then
3948 CancelPaste()
3949 savedstate = true
3950 end
3951
3952 -- if the clipboard contains a valid RLE3 or 2D pattern then create a paste pattern
3953 local filepath = CopyClipboardToFile()
3954 if not filepath then
3955 return false
3956 end
3957
3958 local err, newpattern = ReadPattern(filepath)
3959 if err then
3960 -- invalid RLE3 pattern so try loading a 2D pattern
3961 err, newpattern = Read2DPattern(filepath)
3962 if err then
3963 if err == "too big" then
3964 message = "2D pattern in clipboard is too big."
3965 else
3966 message = "Clipboard does not contain a valid pattern."
3967 end
3968 Refresh()
3969 return false
3970 end
3971 end
3972 if newpattern.newpop == 0 then
3973 message = "Clipboard pattern is empty."
3974 Refresh()
3975 return false
3976 end
3977
3978 -- newpattern contains valid pattern, but might be too big
3979 local minpx = newpattern.newminx
3980 local minpy = newpattern.newminy
3981 local minpz = newpattern.newminz
3982 local pwd = newpattern.newmaxx - minpx + 1
3983 local pht = newpattern.newmaxy - minpy + 1
3984 local pdp = newpattern.newmaxz - minpz + 1
3985 if pwd > N or pht > N or pdp > N then
3986 message = "Clipboard pattern is too big ("..pwd.." x "..pht.." x "..pdp..")."
3987 Refresh()
3988 return false
3989 end
3990
3991 if not savedstate then RememberCurrentState() end
3992
3993 -- set pastecount and pastepatt
3994 pastecount = newpattern.newpop
3995 pastepatt = {}
3996 minpastex = math.maxinteger
3997 minpastey = math.maxinteger
3998 minpastez = math.maxinteger
3999 maxpastex = math.mininteger
4000 maxpastey = math.mininteger
4001 maxpastez = math.mininteger
4002 local P = newpattern.newsize
4003 local PP = P*P
4004 if currcursor ~= movecursor and min(pwd,pht,pdp) == 1 then
4005 -- put 1-cell thick paste pattern in middle of active plane,
4006 -- rotating pattern if necessary to match orientation
4007 local xx, xy, xz = 1, 0, 0
4008 local yx, yy, yz = 0, 1, 0
4009 local zx, zy, zz = 0, 0, 1
4010 local mid = N//2
4011 local xoffset
4012 local yoffset
4013 local zoffset
4014 if pdp == 1 then
4015 if activeplane == "XY" then
4016 -- no rotation needed
4017 xoffset = (N - pwd + 1) // 2
4018 yoffset = (N - pht + 1) // 2
4019 zoffset = mid + activepos
4020 elseif activeplane == "YZ" then
4021 -- rotate pattern about its Y axis
4022 xx, xy, xz = 0, 0, 1
4023 yx, yy, yz = 0, 1, 0
4024 zx, zy, zz = -1, 0, 0
4025 xoffset = mid + activepos
4026 yoffset = (N - pht + 1) // 2
4027 zoffset = (N - pwd + 1) // 2 + pwd - 1
4028 else -- activeplane == "XZ"
4029 -- rotate pattern about its X axis
4030 xx, xy, xz = 1, 0, 0
4031 yx, yy, yz = 0, 0, -1
4032 zx, zy, zz = 0, 1, 0
4033 xoffset = (N - pwd + 1) // 2
4034 yoffset = mid + activepos
4035 zoffset = (N - pht + 1) // 2
4036 end
4037 elseif pht == 1 then
4038 if activeplane == "XY" then
4039 -- rotate pattern about its X axis
4040 xx, xy, xz = 1, 0, 0
4041 yx, yy, yz = 0, 0, -1
4042 zx, zy, zz = 0, 1, 0
4043 xoffset = (N - pwd + 1) // 2
4044 yoffset = (N - pdp + 1) // 2 + pdp - 1
4045 zoffset = mid + activepos
4046 elseif activeplane == "YZ" then
4047 -- rotate pattern about its Z axis
4048 xx, xy, xz = 0, -1, 0
4049 yx, yy, yz = 1, 0, 0
4050 zx, zy, zz = 0, 0, 1
4051 xoffset = mid + activepos
4052 yoffset = (N - pwd + 1) // 2
4053 zoffset = (N - pdp + 1) // 2
4054 else -- activeplane == "XZ"
4055 -- no rotation needed
4056 xoffset = (N - pwd + 1) // 2
4057 yoffset = mid + activepos
4058 zoffset = (N - pdp + 1) // 2
4059 end
4060 else -- pwd == 1
4061 if activeplane == "XY" then
4062 -- rotate pattern about its Y axis
4063 xx, xy, xz = 0, 0, 1
4064 yx, yy, yz = 0, 1, 0
4065 zx, zy, zz = -1, 0, 0
4066 xoffset = (N - pdp + 1) // 2
4067 yoffset = (N - pht + 1) // 2
4068 zoffset = mid + activepos
4069 elseif activeplane == "YZ" then
4070 -- no rotation needed
4071 xoffset = mid + activepos
4072 yoffset = (N - pht + 1) // 2
4073 zoffset = (N - pdp + 1) // 2
4074 else -- activeplane == "XZ"
4075 -- rotate pattern about its Z axis
4076 xx, xy, xz = 0, -1, 0
4077 yx, yy, yz = 1, 0, 0
4078 zx, zy, zz = 0, 0, 1
4079 xoffset = (N - pht + 1) // 2 + pht - 1
4080 yoffset = mid + activepos
4081 zoffset = (N - pdp + 1) // 2
4082 end
4083 end
4084 for k,_ in pairs(newpattern.newgrid) do
4085 -- move pattern to origin
4086 local x = (k % P) - minpx
4087 local y = (k // P % P) - minpy
4088 local z = (k // PP) - minpz
4089
4090 -- do the rotation (if any)
4091 local newx = x*xx + y*xy + z*xz
4092 local newy = x*yx + y*yy + z*yz
4093 local newz = x*zx + y*zy + z*zz
4094
4095 -- now shift to middle of active plane
4096 x = newx + xoffset
4097 y = newy + yoffset
4098 z = newz + zoffset
4099
4100 pastepatt[ x + N * (y + N * z) ] = 1
4101 -- update paste boundary
4102 if x < minpastex then minpastex = x end
4103 if y < minpastey then minpastey = y end
4104 if z < minpastez then minpastez = z end
4105 if x > maxpastex then maxpastex = x end
4106 if y > maxpastey then maxpastey = y end
4107 if z > maxpastez then maxpastez = z end
4108 end
4109 else
4110 -- put paste pattern in middle of grid
4111 local xoffset = minpx - (N - pwd + 1) // 2
4112 local yoffset = minpy - (N - pht + 1) // 2
4113 local zoffset = minpz - (N - pdp + 1) // 2
4114 for k,_ in pairs(newpattern.newgrid) do
4115 -- newpattern.newgrid[k] is a live cell
4116 local x = (k % P) - xoffset
4117 local y = (k // P % P) - yoffset
4118 local z = (k // PP) - zoffset
4119 pastepatt[ x + N * (y + N * z) ] = 1
4120 -- update paste boundary
4121 if x < minpastex then minpastex = x end
4122 if y < minpastey then minpastey = y end
4123 if z < minpastez then minpastez = z end
4124 if x > maxpastex then maxpastex = x end
4125 if y > maxpastey then maxpastey = y end
4126 if z > maxpastez then maxpastez = z end
4127 end
4128 end
4129 CheckIfGenerating()
4130 Refresh()
4131 return true
4132end
4133
4134--------------------------------------------------------------------------------
4135
4136function PasteOR()
4137 if pastecount > 0 then
4138 RememberCurrentState()
4139 -- SetLiveCell will set dirty = true
4140 local NN = N*N
4141 for k,_ in pairs(pastepatt) do
4142 if not grid1[k] then
4143 SetLiveCell(k % N, (k // N) % N, k // NN)
4144 end
4145 end
4146 pastecount = 0
4147 pastepatt = {}
4148 CheckIfGenerating()
4149 Refresh()
4150 end
4151end
4152
4153--------------------------------------------------------------------------------
4154
4155function PasteXOR()
4156 if pastecount > 0 then
4157 RememberCurrentState()
4158 local NN = N*N
4159 for k,_ in pairs(pastepatt) do
4160 if grid1[k] then
4161 grid1[k] = nil
4162 popcount = popcount - 1
4163 dirty = true
4164 update_grid = true
4165 minimal_live_bounds = false
4166 else
4167 SetLiveCell(k % N, (k // N) % N, k // NN)
4168 end
4169 end
4170 pastecount = 0
4171 pastepatt = {}
4172 CheckIfGenerating()
4173 Refresh()
4174 end
4175end
4176
4177--------------------------------------------------------------------------------
4178
4179function FlipPasteX()
4180 if pastecount > 0 then
4181 RememberCurrentState()
4182 -- reflect X coords across YZ plane thru middle of paste pattern
4183 local fmidx = minpastex + (maxpastex - minpastex) / 2
4184 local cells = {}
4185 local NN = N*N
4186 for k,_ in pairs(pastepatt) do
4187 local x = k % N
4188 local y = (k // N) % N
4189 local z = k // NN
4190 cells[#cells+1] = {round(fmidx*2) - x, y, z}
4191 end
4192 -- flip doesn't change paste boundary
4193 pastepatt = {}
4194 for _,xyz in ipairs(cells) do
4195 local x, y, z = xyz[1], xyz[2], xyz[3]
4196 pastepatt[x + N * (y + N * z)] = 1
4197 end
4198 CheckIfGenerating()
4199 Refresh()
4200 end
4201end
4202
4203--------------------------------------------------------------------------------
4204
4205function FlipPasteY()
4206 if pastecount > 0 then
4207 RememberCurrentState()
4208 -- reflect Y coords across XZ plane thru middle of paste pattern
4209 local fmidy = minpastey + (maxpastey - minpastey) / 2
4210 local cells = {}
4211 local NN = N*N
4212 for k,_ in pairs(pastepatt) do
4213 local x = k % N
4214 local y = (k // N) % N
4215 local z = k // NN
4216 cells[#cells+1] = {x, round(fmidy*2) - y, z}
4217 end
4218 -- flip doesn't change paste boundary
4219 pastepatt = {}
4220 for _,xyz in ipairs(cells) do
4221 local x, y, z = xyz[1], xyz[2], xyz[3]
4222 pastepatt[x + N * (y + N * z)] = 1
4223 end
4224 CheckIfGenerating()
4225 Refresh()
4226 end
4227end
4228
4229--------------------------------------------------------------------------------
4230
4231function FlipPasteZ()
4232 if pastecount > 0 then
4233 RememberCurrentState()
4234 -- reflect Z coords across XY plane thru middle of paste pattern
4235 local fmidz = minpastez + (maxpastez - minpastez) / 2
4236 local cells = {}
4237 local NN = N*N
4238 for k,_ in pairs(pastepatt) do
4239 local x = k % N
4240 local y = (k // N) % N
4241 local z = k // NN
4242 cells[#cells+1] = {x, y, round(fmidz*2) - z}
4243 end
4244 -- flip doesn't change paste boundary
4245 pastepatt = {}
4246 for _,xyz in ipairs(cells) do
4247 local x, y, z = xyz[1], xyz[2], xyz[3]
4248 pastepatt[x + N * (y + N * z)] = 1
4249 end
4250 CheckIfGenerating()
4251 Refresh()
4252 end
4253end
4254
4255--------------------------------------------------------------------------------
4256
4257function RotatePasteX()
4258 if pastecount > 0 then
4259 RememberCurrentState()
4260 -- rotate paste pattern clockwise about its X axis by 90 degrees
4261 local fmidy = minpastey + (maxpastey - minpastey) // 2
4262 local fmidz = minpastez + (maxpastez - minpastez) // 2
4263 local y0 = fmidy - fmidz
4264 local z0 = fmidz + fmidy + (maxpastez - minpastez) % 2 -- avoids drift
4265 local cells = {}
4266 local NN = N*N
4267 for k,_ in pairs(pastepatt) do
4268 local x = k % N
4269 local y = (k // N) % N
4270 local z = k // NN
4271 cells[#cells+1] = {x, (y0+z) % N, (z0-y) % N}
4272 end
4273 pastepatt = {}
4274 -- minpastex, maxpastex don't change
4275 minpastey = math.maxinteger
4276 minpastez = math.maxinteger
4277 maxpastey = math.mininteger
4278 maxpastez = math.mininteger
4279 for _,xyz in ipairs(cells) do
4280 local x, y, z = xyz[1], xyz[2], xyz[3]
4281 pastepatt[x + N * (y + N * z)] = 1
4282 if y < minpastey then minpastey = y end
4283 if y > maxpastey then maxpastey = y end
4284 if z < minpastez then minpastez = z end
4285 if z > maxpastez then maxpastez = z end
4286 end
4287 CheckIfGenerating()
4288 Refresh()
4289 end
4290end
4291
4292--------------------------------------------------------------------------------
4293
4294function RotatePasteY()
4295 if pastecount > 0 then
4296 RememberCurrentState()
4297 -- rotate paste pattern clockwise about its Y axis by 90 degrees
4298 local fmidx = minpastex + (maxpastex - minpastex) // 2
4299 local fmidz = minpastez + (maxpastez - minpastez) // 2
4300 local x0 = fmidx + fmidz + (maxpastex - minpastex) % 2 -- avoids drift
4301 local z0 = fmidz - fmidx
4302 local cells = {}
4303 local NN = N*N
4304 for k,_ in pairs(pastepatt) do
4305 local x = k % N
4306 local y = (k // N) % N
4307 local z = k // NN
4308 cells[#cells+1] = {(x0-z) % N, y, (z0+x) % N}
4309 end
4310 pastepatt = {}
4311 -- minpastey, maxpastey don't change
4312 minpastex = math.maxinteger
4313 minpastez = math.maxinteger
4314 maxpastex = math.mininteger
4315 maxpastez = math.mininteger
4316 for _,xyz in ipairs(cells) do
4317 local x, y, z = xyz[1], xyz[2], xyz[3]
4318 pastepatt[x + N * (y + N * z)] = 1
4319 if x < minpastex then minpastex = x end
4320 if x > maxpastex then maxpastex = x end
4321 if z < minpastez then minpastez = z end
4322 if z > maxpastez then maxpastez = z end
4323 end
4324 CheckIfGenerating()
4325 Refresh()
4326 end
4327end
4328
4329--------------------------------------------------------------------------------
4330
4331function RotatePasteZ()
4332 if pastecount > 0 then
4333 RememberCurrentState()
4334 -- rotate paste pattern clockwise about its Z axis by 90 degrees
4335 local fmidx = minpastex + (maxpastex - minpastex) // 2
4336 local fmidy = minpastey + (maxpastey - minpastey) // 2
4337 local x0 = fmidx - fmidy
4338 local y0 = fmidy + fmidx + (maxpastey - minpastey) % 2 -- avoids drift
4339 local cells = {}
4340 local NN = N*N
4341 for k,_ in pairs(pastepatt) do
4342 local x = k % N
4343 local y = (k // N) % N
4344 local z = k // NN
4345 cells[#cells+1] = {(x0+y) % N, (y0-x) % N, z}
4346 end
4347 pastepatt = {}
4348 -- minpastez, maxpastez don't change
4349 minpastex = math.maxinteger
4350 minpastey = math.maxinteger
4351 maxpastex = math.mininteger
4352 maxpastey = math.mininteger
4353 for _,xyz in ipairs(cells) do
4354 local x, y, z = xyz[1], xyz[2], xyz[3]
4355 pastepatt[x + N * (y + N * z)] = 1
4356 if x < minpastex then minpastex = x end
4357 if x > maxpastex then maxpastex = x end
4358 if y < minpastey then minpastey = y end
4359 if y > maxpastey then maxpastey = y end
4360 end
4361 CheckIfGenerating()
4362 Refresh()
4363 end
4364end
4365
4366--------------------------------------------------------------------------------
4367
4368function ChoosePasteAction(mousex, mousey)
4369 -- show red pop-up menu at mousex,mousey and let user choose a paste action
4370 pastemenu.setbgcolor(PASTE_MENU)
4371 pastemenu.show(mousex, mousey, ovwd, ovht)
4372end
4373
4374--------------------------------------------------------------------------------
4375
4376function ChooseSelectionAction(mousex, mousey)
4377 -- show green pop-up menu at mousex,mousey and let user choose a selection action
4378 selmenu.setbgcolor(SELECT_MENU)
4379 selmenu.show(mousex, mousey, ovwd, ovht)
4380end
4381
4382--------------------------------------------------------------------------------
4383
4384function ZoomDouble()
4385 if CELLSIZE < MAXSIZE then
4386 -- zoom in by doubling the cell size
4387 CELLSIZE = CELLSIZE*2
4388 if CELLSIZE > MAXSIZE then CELLSIZE = MAXSIZE end
4389 ZOOMSIZE = CELLSIZE
4390 HALFCELL = CELLSIZE/2.0
4391 MIDGRID = (N+1-(N%2))*HALFCELL
4392 MIDCELL = HALFCELL-MIDGRID
4393 LEN = CELLSIZE-BORDER*2
4394 CreateAxes()
4395 RefreshIfNotGenerating()
4396 end
4397end
4398
4399--------------------------------------------------------------------------------
4400
4401function ZoomHalf()
4402 if CELLSIZE > MINSIZE then
4403 -- zoom out by halving the cell size
4404 CELLSIZE = CELLSIZE//2
4405 if CELLSIZE < MINSIZE then CELLSIZE = MINSIZE end
4406 ZOOMSIZE = CELLSIZE
4407 HALFCELL = CELLSIZE/2.0
4408 MIDGRID = (N+1-(N%2))*HALFCELL
4409 MIDCELL = HALFCELL-MIDGRID
4410 LEN = CELLSIZE-BORDER*2
4411 CreateAxes()
4412 RefreshIfNotGenerating()
4413 end
4414end
4415
4416--------------------------------------------------------------------------------
4417
4418function ZoomIn()
4419 if CELLSIZE < MAXSIZE then
4420 -- zoom in by incrementing the cell size
4421 CELLSIZE = CELLSIZE+1
4422 ZOOMSIZE = CELLSIZE
4423 HALFCELL = CELLSIZE/2.0
4424 MIDGRID = (N+1-(N%2))*HALFCELL
4425 MIDCELL = HALFCELL-MIDGRID
4426 LEN = CELLSIZE-BORDER*2
4427 CreateAxes()
4428 RefreshIfNotGenerating()
4429 end
4430end
4431
4432--------------------------------------------------------------------------------
4433
4434function ZoomOut()
4435 if CELLSIZE > MINSIZE then
4436 -- zoom out by decrementing the cell size
4437 CELLSIZE = CELLSIZE-1
4438 ZOOMSIZE = CELLSIZE
4439 HALFCELL = CELLSIZE/2.0
4440 MIDGRID = (N+1-(N%2))*HALFCELL
4441 MIDCELL = HALFCELL-MIDGRID
4442 LEN = CELLSIZE-BORDER*2
4443 CreateAxes()
4444 RefreshIfNotGenerating()
4445 end
4446end
4447
4448--------------------------------------------------------------------------------
4449
4450function ZoomInPower()
4451 if CELLSIZE < MAXSIZE then
4452 -- zoom in by increasing the cell size by a percentage
4453 ZOOMSIZE = ZOOMSIZE * (1 + 1 / MINSIZE)
4454 if ZOOMSIZE > MAXSIZE then ZOOMSIZE = MAXSIZE end
4455 CELLSIZE = ZOOMSIZE // 1 | 0
4456 HALFCELL = CELLSIZE/2.0
4457 MIDGRID = (N+1-(N%2))*HALFCELL
4458 MIDCELL = HALFCELL-MIDGRID
4459 LEN = CELLSIZE-BORDER*2
4460 CreateAxes()
4461 RefreshIfNotGenerating()
4462 end
4463end
4464
4465--------------------------------------------------------------------------------
4466
4467function ZoomOutPower()
4468 if CELLSIZE > MINSIZE then
4469 -- zoom out by reducing the cell size by a percentage
4470 ZOOMSIZE = ZOOMSIZE / (1 + 1 / MINSIZE)
4471 if ZOOMSIZE < MINSIZE then ZOOMSIZE = MINSIZE end
4472 CELLSIZE = ZOOMSIZE // 1 | 0
4473 HALFCELL = CELLSIZE/2.0
4474 MIDGRID = (N+1-(N%2))*HALFCELL
4475 MIDCELL = HALFCELL-MIDGRID
4476 LEN = CELLSIZE-BORDER*2
4477 CreateAxes()
4478 RefreshIfNotGenerating()
4479 end
4480end
4481
4482--------------------------------------------------------------------------------
4483
4484function OpenFile(filepath)
4485 if filepath:find("%.rle3$") then
4486 OpenPattern(filepath)
4487 elseif filepath:find("%.lua$") then
4488 RunScript(filepath)
4489 else
4490 g.warn("Unexpected file:\n"..filepath.."\n\n"..
4491 "3D.lua can only handle files ending with .rle3 or .lua.", false)
4492 end
4493end
4494
4495--------------------------------------------------------------------------------
4496
4497function StartStop()
4498 generating = not generating
4499 UpdateStartButton()
4500 Refresh()
4501 if generating and popcount > 0 then
4502 RememberCurrentState()
4503 -- EventLoop will call NextGeneration
4504 end
4505end
4506
4507--------------------------------------------------------------------------------
4508
4509function Step1()
4510 if generating then
4511 StopGenerating()
4512 Refresh()
4513 else
4514 -- NextGeneration does nothing (except display a message) if popcount is 0
4515 if popcount > 0 then
4516 RememberCurrentState()
4517 end
4518 NextGeneration(true)
4519 end
4520end
4521
4522--------------------------------------------------------------------------------
4523
4524function NextStep()
4525 if popcount == 0 or stepsize == 1 then
4526 Step1()
4527 else
4528 if generating then
4529 StopGenerating()
4530 Refresh()
4531 else
4532 -- NextGeneration does nothing (except display a message) if popcount is 0
4533 if popcount > 0 then
4534 RememberCurrentState()
4535 end
4536 NextGeneration()
4537 end
4538 end
4539end
4540
4541--------------------------------------------------------------------------------
4542
4543function Faster()
4544 if stepsize < 100 then
4545 SetStepSize(stepsize + 1)
4546 RefreshIfNotGenerating()
4547 end
4548end
4549
4550--------------------------------------------------------------------------------
4551
4552function Slower()
4553 if stepsize > 1 then
4554 SetStepSize(stepsize - 1)
4555 RefreshIfNotGenerating()
4556 end
4557end
4558
4559--------------------------------------------------------------------------------
4560
4561function SetStepSize(newval)
4562 if newval >= 1 and newval <= 100 then
4563 stepsize = newval
4564 ovt{"setstep3d", stepsize}
4565 end
4566end
4567
4568--------------------------------------------------------------------------------
4569
4570function StepChange(newval)
4571 -- called if stepslider position has changed
4572 SetStepSize(newval)
4573 Refresh()
4574end
4575
4576--------------------------------------------------------------------------------
4577
4578function SetStepSizeTo1()
4579 SetStepSize(1)
4580 RefreshIfNotGenerating()
4581end
4582
4583--------------------------------------------------------------------------------
4584
4585function Reset()
4586 if gencount > startcount then
4587 -- restore the starting state
4588 if scriptlevel > 0 then
4589 -- Reset was called by user script so don't modify undo/redo stacks
4590 RestoreState(startstate)
4591 else
4592 -- unwind undostack until gencount == startcount
4593 repeat
4594 -- push current state onto redostack
4595 redostack[#redostack+1] = SaveState()
4596 -- pop state off undostack and restore it
4597 RestoreState( table.remove(undostack) )
4598 until gencount == startcount
4599 end
4600 StopGenerating()
4601 Refresh()
4602 end
4603end
4604
4605--------------------------------------------------------------------------------
4606
4607local function GetMidPoint(x, y, z)
4608 -- return mid point of cell at given grid position
4609 x = x * CELLSIZE + MIDCELL
4610 y = y * CELLSIZE + MIDCELL
4611 z = z * CELLSIZE + MIDCELL
4612 -- transform point
4613 local newx = (x*xixo + y*xiyo + z*xizo)
4614 local newy = (x*yixo + y*yiyo + z*yizo)
4615 -- use orthographic projection
4616 x = round(newx) + midx
4617 y = round(newy) + midy
4618 return x, y
4619end
4620
4621--------------------------------------------------------------------------------
4622
4623function FitGrid(display)
4624 if display == nil then display = true end
4625
4626 local function Visible(x, y)
4627 -- return true if pixel at x,y is within area under tool bar
4628 if x < 0 or x >= ovwd then return false end
4629 if y <= toolbarht or y >= ovht then return false end
4630 return true
4631 end
4632
4633 -- find largest CELLSIZE at which all corners of grid are visible
4634 CELLSIZE = MAXSIZE + 1
4635 repeat
4636 CELLSIZE = CELLSIZE-1
4637 HALFCELL = CELLSIZE/2.0
4638 MIDGRID = (N+1-(N%2))*HALFCELL
4639 MIDCELL = HALFCELL-MIDGRID
4640 LEN = CELLSIZE-BORDER*2
4641 CreateAxes()
4642 if CELLSIZE == MINSIZE then break end
4643 local x1, y1 = GetMidPoint(0, 0, 0)
4644 local x2, y2 = GetMidPoint(0, 0, N-1)
4645 local x3, y3 = GetMidPoint(0, N-1, 0)
4646 local x4, y4 = GetMidPoint(N-1, 0, 0)
4647 local x5, y5 = GetMidPoint(0, N-1, N-1)
4648 local x6, y6 = GetMidPoint(N-1, N-1, 0)
4649 local x7, y7 = GetMidPoint(N-1, 0, N-1)
4650 local x8, y8 = GetMidPoint(N-1, N-1, N-1)
4651 local xmin = min(x1,x2,x3,x4,x5,x6,x7,x8) - CELLSIZE
4652 local xmax = max(x1,x2,x3,x4,x5,x6,x7,x8) + CELLSIZE
4653 local ymin = min(y1,y2,y3,y4,y5,y6,y7,y8) - CELLSIZE
4654 local ymax = max(y1,y2,y3,y4,y5,y6,y7,y8) + CELLSIZE
4655 until Visible(xmin,ymin) and Visible(xmin,ymax) and
4656 Visible(xmax,ymin) and Visible(xmax,ymax)
4657 ZOOMSIZE = CELLSIZE
4658
4659 if display then Refresh() end
4660end
4661
4662--------------------------------------------------------------------------------
4663
4664-- getters for user scripts
4665
4666function GetGeneration() return gencount end
4667function GetGridSize() return N end
4668function GetPercentage() return perc end
4669function GetPopulation() return popcount end
4670function GetRule() return rulestring end
4671function GetCellType() return celltype end
4672function GetStepSize() return stepsize end
4673function GetBarHeight() return toolbarht end
4674
4675--------------------------------------------------------------------------------
4676
4677-- for user scripts
4678function GetCells(selectedonly)
4679 -- return an array of live cell coordinates;
4680 -- if selected is true then only selected live cells will be returned
4681 local cellarray = {}
4682 if popcount > 0 then
4683 if selectedonly then
4684 cellarray = GetSelectedLiveCells()
4685 else
4686 -- get all live cells
4687 local mid = N//2
4688 local NN = N*N
4689 for k,_ in pairs(grid1) do
4690 -- grid1[k] is a live cell
4691 local x = k % N
4692 local y = (k // N) % N
4693 local z = k // NN
4694 cellarray[#cellarray+1] = {x-mid, y-mid, z-mid}
4695 end
4696 end
4697 end
4698 return cellarray
4699end
4700
4701--------------------------------------------------------------------------------
4702
4703-- for user scripts
4704function PutCells(cellarray, xoffset, yoffset, zoffset)
4705 -- use given array of live cell coordinates and offsets to set cells in grid
4706 -- (any cells outside the grid are silently clipped)
4707 xoffset = xoffset or 0
4708 yoffset = yoffset or 0
4709 zoffset = zoffset or 0
4710 local mid = N//2
4711 local midxv = mid + xoffset
4712 local midyv = mid + yoffset
4713 local midzv = mid + zoffset
4714 for _, xyz in ipairs(cellarray) do
4715 local x = xyz[1] + midxv
4716 local y = xyz[2] + midyv
4717 local z = xyz[3] + midzv
4718 if x >= 0 and x < N and
4719 y >= 0 and y < N and
4720 z >= 0 and z < N then
4721 SetCellState(x, y, z, 1)
4722 end
4723 end
4724end
4725
4726--------------------------------------------------------------------------------
4727
4728-- for user scripts
4729function GetBounds()
4730 if popcount > 0 then
4731 -- return the pattern's minimal bounding box
4732 MinimizeLiveBoundary()
4733 local mid = N//2
4734 return { minx-mid, maxx-mid,
4735 miny-mid, maxy-mid,
4736 minz-mid, maxz-mid }
4737 else
4738 return {}
4739 end
4740end
4741
4742--------------------------------------------------------------------------------
4743
4744-- for user scripts
4745function GetSelectionBounds()
4746 if selcount > 0 then
4747 -- return the selection's minimal bounding box
4748 MinimizeSelectionBoundary()
4749 local mid = N//2
4750 return { minselx-mid, maxselx-mid,
4751 minsely-mid, maxsely-mid,
4752 minselz-mid, maxselz-mid }
4753 else
4754 return {}
4755 end
4756end
4757
4758--------------------------------------------------------------------------------
4759
4760-- for user scripts
4761function GetPasteBounds()
4762 if pastecount > 0 then
4763 -- return the paste pattern's minimal bounding box
4764 local mid = N//2
4765 return { minpastex-mid, maxpastex-mid,
4766 minpastey-mid, maxpastey-mid,
4767 minpastez-mid, maxpastez-mid }
4768 else
4769 return {}
4770 end
4771end
4772
4773--------------------------------------------------------------------------------
4774
4775-- for user scripts
4776function Step(n)
4777 n = n or 1
4778
4779 -- script commands might have changed the pattern
4780 if update_grid then
4781 ovt{"setpattern3d", grid1, false}
4782 update_grid = false
4783 end
4784
4785 while popcount > 0 and n > 0 do
4786 NextGeneration()
4787 n = n - 1
4788 g.doevent("") -- null op to let user abort script
4789 end
4790end
4791
4792--------------------------------------------------------------------------------
4793
4794-- for user scripts
4795function SetRule(newrule)
4796 newrule = newrule or DEFAULT_RULE
4797 if not ParseRule(newrule) then
4798 error("Bad rule in SetRule: "..newrule, 2)
4799 end
4800end
4801
4802--------------------------------------------------------------------------------
4803
4804-- for user scripts
4805function GetCell(x, y, z)
4806 local mid = N//2
4807 if grid1[ (x+mid) + N * ((y+mid) + N * (z+mid)) ] then
4808 return 1
4809 else
4810 return 0
4811 end
4812end
4813
4814--------------------------------------------------------------------------------
4815
4816-- for user scripts
4817function SetCell(x, y, z, state)
4818 state = state or 1 -- default state is 1
4819 local mid = N//2
4820 SetCellState(x+mid, y+mid, z+mid, state)
4821end
4822
4823--------------------------------------------------------------------------------
4824
4825-- for user scripts
4826function SelectCell(x, y, z)
4827 local mid = N//2
4828 SetSelection(x+mid, y+mid, z+mid, true)
4829end
4830
4831--------------------------------------------------------------------------------
4832
4833-- for user scripts
4834function SelectedCell(x, y, z)
4835 if selcount == 0 then
4836 return false
4837 else
4838 local mid = N//2
4839 return selected[x+mid + N * (y+mid + N * (z+mid))]
4840 end
4841end
4842
4843--------------------------------------------------------------------------------
4844
4845-- for user scripts
4846function DeselectCell(x, y, z)
4847 local mid = N//2
4848 SetSelection(x+mid, y+mid, z+mid, false)
4849end
4850
4851--------------------------------------------------------------------------------
4852
4853-- for user scripts
4854function SetMessage(msg)
4855 message = msg
4856end
4857
4858--------------------------------------------------------------------------------
4859
4860-- for user scripts
4861function PasteExists()
4862 return pastecount > 0
4863end
4864
4865--------------------------------------------------------------------------------
4866
4867-- for user scripts
4868function SelectionExists()
4869 return selcount > 0
4870end
4871
4872--------------------------------------------------------------------------------
4873
4874-- for user scripts
4875function DoPaste(x, y, z, mode)
4876 if PasteExists() then
4877 -- move paste pattern to x,y,z
4878 local mid = N//2
4879 local deltax = x+mid - minpastex
4880 local deltay = y+mid - minpastey
4881 local deltaz = z+mid - minpastez
4882 MovePastePattern(deltax, deltay, deltaz)
4883 -- now do the paste using the given mode
4884 if mode == "or" then
4885 PasteOR()
4886 elseif mode == "xor" then
4887 PasteXOR()
4888 else
4889 error("Bad mode in DoPaste!", 2)
4890 end
4891 end
4892end
4893
4894--------------------------------------------------------------------------------
4895
4896-- for user scripts
4897function FlipPaste(coordinates)
4898 if coordinates == "x" then FlipPasteX()
4899 elseif coordinates == "y" then FlipPasteY()
4900 elseif coordinates == "z" then FlipPasteZ()
4901 else
4902 error("Bad coordinates in FlipPaste!", 2)
4903 end
4904end
4905
4906--------------------------------------------------------------------------------
4907
4908-- for user scripts
4909function RotatePaste(axis)
4910 if axis == "x" then RotatePasteX()
4911 elseif axis == "y" then RotatePasteY()
4912 elseif axis == "z" then RotatePasteZ()
4913 else
4914 error("Bad axis in RotatePaste!", 2)
4915 end
4916end
4917
4918--------------------------------------------------------------------------------
4919
4920-- for user scripts
4921function FlipSelection(coordinates)
4922 if coordinates == "x" then FlipSelectionX()
4923 elseif coordinates == "y" then FlipSelectionY()
4924 elseif coordinates == "z" then FlipSelectionZ()
4925 else
4926 error("Bad coordinates in FlipSelection!", 2)
4927 end
4928end
4929
4930--------------------------------------------------------------------------------
4931
4932-- for user scripts
4933function RotateSelection(axis)
4934 if axis == "x" then RotateSelectionX()
4935 elseif axis == "y" then RotateSelectionY()
4936 elseif axis == "z" then RotateSelectionZ()
4937 else
4938 error("Bad axis in RotateSelection!", 2)
4939 end
4940end
4941
4942--------------------------------------------------------------------------------
4943
4944-- for user scripts
4945function Update()
4946 Refresh(true)
4947end
4948
4949--------------------------------------------------------------------------------
4950
4951function ShowHelp()
4952 local htmldata = [[
4953<html><title>Golly Help: 3D.lua</title>
4954<body bgcolor="#FFFFCE">
4955
4956<p>
4957<dd><a href="#intro"><b>Introduction</b></a></dd>
4958<dd><a href="#mouse"><b>Mouse controls</b></a></dd>
4959<dd><a href="#keyboard"><b>Keyboard shortcuts</b></a></dd>
4960<dd><a href="#menus"><b>Menus</b></a></dd>
4961<dd> <a href="#file"><b>File menu</b></a></dd>
4962<dd> <a href="#edit"><b>Edit menu</b></a></dd>
4963<dd> <a href="#control"><b>Control menu</b></a></dd>
4964<dd> <a href="#view"><b>View menu</b></a></dd>
4965<dd><a href="#scripts"><b>Running scripts</b></a></dd>
4966<dd> <a href="#shortcuts"><b>Creating your own keyboard shortcuts</b></a></dd>
4967<dd> <a href="#functions"><b>Script functions</b></a></dd>
4968<dd> <a href="#coords"><b>Cell coordinates</b></a></dd>
4969<dd><a href="#rules"><b>Supported rules</b></a></dd>
4970<dd> <a href="#moore"><b>Moore neighborhood</b></a></dd>
4971<dd> <a href="#face"><b>Face neighborhood</b></a></dd>
4972<dd> <a href="#corner"><b>Corner neighborhood</b></a></dd>
4973<dd> <a href="#edge"><b>Edge neighborhood</b></a></dd>
4974<dd> <a href="#hex"><b>Hexahedral neighborhood</b></a></dd>
4975<dd> <a href="#busy"><b>Busy Boxes</b></a></dd>
4976<dd><a href="#rle3"><b>RLE3 file format</b></a></dd>
4977<dd><a href="#refs"><b>Credits and references</b></a></dd>
4978</p>
4979
4980<p><a name="intro"></a><br>
4981<font size=+1><b>Introduction</b></font>
4982
4983<p>
49843D.lua is a script that lets you explore three-dimensional cellular automata.
4985The script uses overlay commands to completely replace Golly's usual
4986interface (note that all your Golly settings will be restored when 3D.lua exits).
4987
4988<p><a name="mouse"></a><br>
4989<font size=+1><b>Mouse controls</b></font>
4990
4991<p>
4992If the Move option is ticked then you can use the hand cursor
4993to rotate the view by clicking and dragging.
4994Rotation occurs around the middle cell in the grid.
4995
4996<p>
4997The hand cursor can also be used to move a paste pattern (red cells)
4998or a selection (green cells) to a new location within the grid, but only
4999within a certain plane, depending on where the initial click occurred.
5000Imagine a box enclosing the paste pattern or all the selected cells.
5001One of the faces of this box will contain the initial click.
5002Movement is only allowed within the plane parallel to the clicked face.
5003
5004<p>
5005It's also possible to do some editing with the hand cursor.
5006You can alt-click on a live cell to erase it <em>and</em> any live cells
5007behind it, as long as their mid points are within a half-cell radius of the mouse click.
5008Or you can shift-click on a live cell to select it <em>and</em> any live cells behind it.
5009This makes it easy to quickly erase or select isolated objects.
5010
5011<p>
5012If the Draw option is ticked then you can use the pencil cursor
5013to draw or erase cells in the active plane (shown as translucent blue).
5014Note that any live cells outside the active plane are drawn as white points.
5015Click and drag outside the active plane to rotate the view.
5016
5017<p>
5018If the Select option is ticked then you can use the cross-hairs cursor
5019to select or deselect cells in the active plane.
5020Any selected cells outside the active plane are drawn as translucent green points.
5021Click and drag outside the active plane to rotate the view.
5022
5023<p>
5024To move the active plane to a different position, shift-click anywhere in the
5025active plane and drag the mouse. Or you can hit the "," or "." keys.
5026Hit shift-A to change the active plane's orientation.
5027
5028<p>
5029If a paste pattern is visible (red cells) then you can control-click or
5030right-click anywhere, using any cursor, to get a red pop-up menu that lets
5031you choose various paste actions.
5032The paste pattern can also be dragged to a different location using any cursor.
5033
5034<p>
5035If a selection exists (green cells) then you can control-click or right-click
5036to get a green pop-up menu with various selection actions.
5037If a paste pattern and a selection both exist then the paste menu takes precedence.
5038
5039<p>
5040Use the mouse wheel at any time to zoom in/out.
5041The zoom is always centered on the middle cell in the grid.
5042
5043<p><a name="keyboard"></a><br>
5044<font size=+1><b>Keyboard shortcuts</b></font>
5045
5046<p>
5047The following keyboard shortcuts are provided (but see <a href="#shortcuts">below</a>
5048for how you can write a script to create new shortcuts or override any of the supplied
5049shortcuts):
5050
5051<p>
5052<center>
5053<table cellspacing=1 border=2 cols=2 width="90%">
5054<tr><td align=right> Keys </td><td> Actions </td></tr>
5055<tr><td align=right> enter </td><td> start/stop generating pattern </td></tr>
5056<tr><td align=right> space </td><td> advance pattern by one generation </td></tr>
5057<tr><td align=right> tab </td><td> advance pattern to next multiple of step size </td></tr>
5058<tr><td align=right> - </td><td> decrease step size </td></tr>
5059<tr><td align=right> = </td><td> increase step size </td></tr>
5060<tr><td align=right> 1 </td><td> reset step size to 1 </td></tr>
5061<tr><td align=right> arrows </td><td> rotate about X/Y screen axes </td></tr>
5062<tr><td align=right> alt-arrows </td><td> rotate about Z screen axis </td></tr>
5063<tr><td align=right> ctrl-N </td><td> create a new, empty pattern </td></tr>
5064<tr><td align=right> 5 </td><td> create a new, random pattern </td></tr>
5065<tr><td align=right> ctrl-O </td><td> open a selected pattern file </td></tr>
5066<tr><td align=right> ctrl-S </td><td> save the current pattern in a file </td></tr>
5067<tr><td align=right> shift-O </td><td> open pattern in clipboard </td></tr>
5068<tr><td align=right> shift-R </td><td> run script in clipboard </td></tr>
5069<tr><td align=right> ctrl-R </td><td> reset to the starting pattern </td></tr>
5070<tr><td align=right> Z </td><td> undo </td></tr>
5071<tr><td align=right> shift-Z </td><td> redo </td></tr>
5072<tr><td align=right> ctrl-X </td><td> cut </td></tr>
5073<tr><td align=right> ctrl-C </td><td> copy </td></tr>
5074<tr><td align=right> ctrl-V </td><td> show paste pattern </td></tr>
5075<tr><td align=right> alt-V </td><td> cancel paste pattern </td></tr>
5076<tr><td align=right> ctrl-B </td><td> do the paste using OR mode </td></tr>
5077<tr><td align=right> ctrl-shift-B </td><td> do the paste using XOR mode </td></tr>
5078<tr><td align=right> delete </td><td> kill selected live cells </td></tr>
5079<tr><td align=right> shift-delete </td><td> kill unselected live cells </td></tr>
5080<tr><td align=right> A </td><td> select all </td></tr>
5081<tr><td align=right> K </td><td> remove selection </td></tr>
5082<tr><td align=right> B </td><td> back view (rotate 180 deg about Y axis) </td></tr>
5083<tr><td align=right> I </td><td> restore initial view </td></tr>
5084<tr><td align=right> F </td><td> fit entire grid within view </td></tr>
5085<tr><td align=right> [ </td><td> zoom out </td></tr>
5086<tr><td align=right> ] </td><td> zoom in </td></tr>
5087<tr><td align=right> { </td><td> halve current zoom </td></tr>
5088<tr><td align=right> } </td><td> double current zoom </td></tr>
5089<tr><td align=right> P </td><td> cycle live cells (cubes/spheres/points) </td></tr>
5090<tr><td align=right> L </td><td> toggle lattice lines </td></tr>
5091<tr><td align=right> shift-L </td><td> toggle axes </td></tr>
5092<tr><td align=right> alt-D </td><td> toggle depth shading </td></tr>
5093<tr><td align=right> Y </td><td> toggle cell history </td></tr>
5094<tr><td align=right> shift-Y </td><td> toggle history fade </td></tr>
5095<tr><td align=right> T </td><td> toggle the menu bar and tool bar </td></tr>
5096<tr><td align=right> G </td><td> change the grid size </td></tr>
5097<tr><td align=right> R </td><td> change the rule </td></tr>
5098<tr><td align=right> , </td><td> move active plane closer (in initial view) </td></tr>
5099<tr><td align=right> . </td><td> move active plane further away (in initial view) </td></tr>
5100<tr><td align=right> shift-A </td><td> cycle active plane's orientation (XY/XZ/YZ) </td></tr>
5101<tr><td align=right> D </td><td> switch cursor to draw mode </td></tr>
5102<tr><td align=right> S </td><td> switch cursor to select mode </td></tr>
5103<tr><td align=right> M </td><td> switch cursor to move mode </td></tr>
5104<tr><td align=right> C </td><td> cycle cursor mode (draw/select/move) </td></tr>
5105<tr><td align=right> shift-M </td><td> move pattern to middle of grid </td></tr>
5106<tr><td align=right> H </td><td> show this help </td></tr>
5107<tr><td align=right> Q </td><td> quit 3D.lua </td></tr>
5108</table>
5109</center>
5110
5111<p><a name="menus"></a><br>
5112<font size=+1><b>Menus</b></font>
5113
5114<p>
51153D.lua has its own menu bar. It contains menus with items that are
5116somewhat similar to those in Golly's menu bar.
5117
5118<p><a name="file"></a><br>
5119<font size=+1><b>File menu</b></font>
5120
5121<a name="new"></a><p><dt><b>New Pattern</b></dt>
5122<dd>
5123Create a new, empty pattern.
5124All undo/redo history is deleted and the step size is reset to 1.
5125The active plane is displayed, ready to be edited using the pencil cursor.
5126</dd>
5127
5128<a name="rand"></a><p><dt><b>Random Pattern...</b></dt>
5129<dd>
5130Create a new pattern randomly filled with live cells at a given density.
5131You have the option of filling the grid and creating a cube or a sphere.
5132All undo/redo history is deleted and the step size is reset to 1.
5133</dd>
5134
5135<a name="open"></a><p><dt><b>Open Pattern...</b></dt>
5136<dd>
5137Open a selected <a href="#rle3">RLE3</a> pattern file.
5138All undo/redo history is deleted and the step size is reset to 1.
5139</dd>
5140
5141<a name="openclip"></a><p><dt><b>Open Clipboard</b></dt>
5142<dd>
5143Open the <a href="#rle3">RLE3</a> pattern stored in the clipboard.
5144All undo/redo history is deleted and the step size is reset to 1.
5145</dd>
5146
5147<a name="save"></a><p><dt><b>Save Pattern...</b></dt>
5148<dd>
5149Save the current pattern in a <a href="#rle3">.rle3</a> file.
5150</dd>
5151
5152<a name="run"></a><p><dt><b>Run Script...</b></dt>
5153<dd>
5154Run a selected Lua script.
5155</dd>
5156
5157<a name="runclip"></a><p><dt><b>Run Clipboard</b></dt>
5158<dd>
5159Run the Lua code stored in the clipboard.
5160</dd>
5161
5162<a name="startup"></a><p><dt><b>Set Startup Script...</b></dt>
5163<dd>
5164Select a Lua script that will be run automatically every time 3D.lua starts up.
5165</dd>
5166
5167<a name="exit"></a><p><dt><b>Exit 3D.lua</b></dt>
5168<dd>
5169Terminate 3D.lua. If there are any unsaved changes (indicated by an asterisk at
5170the start of the pattern name) then you'll be asked if you want to save them.
5171Note that hitting the escape key will immediately abort 3D.lua without
5172doing any check for unsaved changes.
5173</dd>
5174
5175<p><a name="edit"></a><br>
5176<font size=+1><b>Edit menu</b></font>
5177
5178<a name="undo"></a><p><dt><b>Undo</b></dt>
5179<dd>
5180Undo the most recent change. This could be an editing change or a generating change.
5181</dd>
5182
5183<a name="redo"></a><p><dt><b>Redo</b></dt>
5184<dd>
5185Redo the most recently undone change.
5186</dd>
5187
5188<a name="cut"></a><p><dt><b>Cut</b></dt>
5189<dd>
5190Copy all selected live cells to the clipboard in <a href="#rle3">RLE3</a> format,
5191then delete those cells (but they remain selected).
5192</dd>
5193
5194<a name="copy"></a><p><dt><b>Copy</b></dt>
5195<dd>
5196Copy all selected live cells to the clipboard in <a href="#rle3">RLE3</a> format.
5197</dd>
5198
5199<a name="paste"></a><p><dt><b>Paste</b></dt>
5200<dd>
5201If the clipboard contains a valid, non-empty pattern (<a href="#rle3">RLE3</a> or 2D) that fits
5202within the current grid then a paste pattern (comprised of red cells) will appear.
5203If the active plane is visible and the clipboard pattern is one cell thick (in any direction)
5204then the paste pattern appears in the middle of the active plane, otherwise it will appear
5205in the middle of the grid. You can use any cursor to drag the paste pattern to any position
5206within the grid, then control-click or right-click anywhere to get a pop-up menu that lets
5207you flip/rotate the paste pattern or paste it into the grid using either OR mode or XOR mode.
5208</dd>
5209
5210<a name="cancelpaste"></a><p><dt><b>Cancel Paste</b></dt>
5211<dd>
5212Remove the paste pattern.
5213</dd>
5214
5215<a name="clear"></a><p><dt><b>Clear</b></dt>
5216<dd>
5217Delete all the selected live cells.
5218</dd>
5219
5220<a name="outside"></a><p><dt><b>Clear Outside</b></dt>
5221<dd>
5222Delete all the live cells that are not selected.
5223</dd>
5224
5225<a name="selall"></a><p><dt><b>Select All</b></dt>
5226<dd>
5227Select all the live cells. Selected cells appear green.
5228Assuming there is no paste pattern, you can control-click or right-click anywhere to get
5229a pop-up menu that lets you perform various actions on the selection.
5230</dd>
5231
5232<a name="cancelsel"></a><p><dt><b>Cancel Selection</b></dt>
5233<dd>
5234Remove the selection.
5235</dd>
5236
5237<a name="midpatt"></a><p><dt><b>Middle Pattern</b></dt>
5238<dd>
5239Move the pattern to the middle of the grid.
5240</dd>
5241
5242<a name="midsel"></a><p><dt><b>Middle Selection</b></dt>
5243<dd>
5244Move the selection to the middle of the grid.
5245</dd>
5246
5247<a name="midpaste"></a><p><dt><b>Middle Paste</b></dt>
5248<dd>
5249Move the paste pattern to the middle of the grid.
5250</dd>
5251
5252<p><a name="control"></a><br>
5253<font size=+1><b>Control menu</b></font>
5254
5255<a name="startstop"></a><p><dt><b>Start/Stop Generating</b></dt>
5256<dd>
5257Start or stop generating the current pattern.
5258You can only start generating if there is at least one live cell.
5259Generating stops automatically if the pattern dies out.
5260</dd>
5261
5262<a name="next"></a><p><dt><b>Next Generation</b></dt>
5263<dd>
5264If the pattern isn't empty then advance to the next generation.
5265</dd>
5266
5267<a name="nextstep"></a><p><dt><b>Next Step</b></dt>
5268<dd>
5269Advance the pattern to the next multiple of the current step size,
5270or until the pattern is empty. Only the final generation is displayed.
5271</dd>
5272
5273<a name="reset"></a><p><dt><b>Reset</b></dt>
5274<dd>
5275Restore the starting generation and step size.
5276</dd>
5277
5278<a name="setrule"></a><p><dt><b>Set Rule</b></dt>
5279<dd>
5280Show a dialog box that lets you change the current <a href="#rules">rule</a>.
5281</dd>
5282
5283<p><a name="view"></a><br>
5284<font size=+1><b>View menu</b></font>
5285
5286<a name="initial"></a><p><dt><b>Initial View</b></dt>
5287<dd>
5288Restore the scale and rotation used when 3D.lua started up.
5289Note that if you hit the up arrow 4 times and the right arrow 4 times
5290then you'll see a single XY face parallel with the screen.
5291</dd>
5292
5293<a name="fit"></a><p><dt><b>Fit Grid</b></dt>
5294<dd>
5295Change the scale so the entire grid just fits within the window.
5296</dd>
5297
5298<a name="gridsize"></a><p><dt><b>Set Grid Size</b></dt>
5299<dd>
5300Show a dialog box that lets you change the grid size.
5301If the current pattern doesn't fit inside the new size then
5302you'll see a message stating how many live cells were clipped.
5303</dd>
5304
5305<a name="cubes"></a><p><dt><b>Cubes</b></dt>
5306<dd>
5307If selected then live cells are displayed as cubes.
5308</dd>
5309
5310<a name="spheres"></a><p><dt><b>Spheres</b></dt>
5311<dd>
5312If selected then live cells are displayed as spheres.
5313</dd>
5314
5315<a name="points"></a><p><dt><b>Points</b></dt>
5316<dd>
5317If selected then live cells are displayed as points.
5318Note that if the active plane is shown then any live cells outside
5319that plane are always displayed as points.
5320</dd>
5321
5322<a name="axes"></a><p><dt><b>Show Axes</b></dt>
5323<dd>
5324If ticked then the edges of the grid are displayed
5325(X axes are red, Y axes are green, Z axes are blue).
5326</dd>
5327
5328<a name="lines"></a><p><dt><b>Show Lattice Lines</b></dt>
5329<dd>
5330If ticked then lattice lines are displayed on the three faces of the grid
5331that intersect at the corner with minimum <a href="#coords">cell coordinates</a>
5332(the far, bottom left corner in the initial view).
5333</dd>
5334
5335<a name="shading"></a><p><dt><b>Use Depth Shading</b></dt>
5336<dd>
5337If ticked then cubes and spheres are drawn slightly darker the further
5338away they are from the front of the screen. Depth shading is not done
5339when displaying points.
5340</dd>
5341
5342<a name="showhistory"></a><p><dt><b>Show History</b></dt>
5343<dd>
5344If ticked then history cells are shown.
5345</dd>
5346
5347<a name="fadehistory"></a><p><dt><b>Fade History</b></dt>
5348<dd>
5349If ticked then history cells fade each generation. They do not fade
5350away completely so you can always see where live cells have been.
5351</dd>
5352
5353<a name="help"></a><p><dt><b>Help</b></dt>
5354<dd>
5355Show this help.
5356</dd>
5357
5358<p><a name="scripts"></a><br>
5359<font size=+1><b>Running scripts</b></font>
5360
5361<p>
53623D.lua can run other Lua scripts, either by selecting File > Run Script
5363and choosing a .lua file, or by copying Lua code to the clipboard and
5364selecting File > Run Clipboard. Try the latter method with this example
5365that creates a small random pattern in the middle of the grid:
5366
5367<dd><table border=0><pre>
5368-- for 3D.lua (make sure you copy this line)
5369NewPattern("random pattern")
5370local perc = GetPercentage()
5371local quarter = GetGridSize()//4
5372for z = -quarter, quarter do
5373 for y = -quarter, quarter do
5374 for x = -quarter, quarter do
5375 if math.random(0,99) < perc then
5376 SetCell(x, y, z, 1)
5377 end
5378 end
5379 end
5380end
5381MoveMode() -- sets the hand cursor</pre></table></dd>
5382
5383<p>
5384Note that 3D.lua will only run a script if the clipboard or the file
5385has "3D.lua" somewhere in the first line. This avoids nasty problems
5386that can occur if you run a script not written for 3D.lua.
5387
5388<p>
5389Any syntax or runtime errors in a script won't abort 3D.lua.
5390The script will terminate and you'll get a warning message, hopefully
5391with enough information that lets you fix the error.
5392
5393<p><a name="shortcuts"></a><br>
5394<font size=+1><b>Creating your own keyboard shortcuts</b></font>
5395
5396<p>
5397It's possible to override any of the global functions in 3D.lua.
5398The following script shows how to override the HandleKey function
5399to create a keyboard shortcut for running a particular script.
5400Even more useful, you can get 3D.lua to run this script automatically
5401when it starts up by going to File > Set Startup Script and selecting
5402a .lua file containing this code:
5403
5404<dd><table border=0><pre>
5405-- a startup script for 3D.lua
5406local g = golly()
5407local gp = require "gplus"
5408local savedHandler = HandleKey
5409function HandleKey(event)
5410 local _, key, mods = gp.split(event)
5411 if key == "r" and mods == "alt" then
5412 RunScript(g.getdir("app").."My-scripts/3D/random-rule.lua")
5413 else
5414 -- pass the event to the original HandleKey function
5415 savedHandler(event)
5416 end
5417end</pre></table></dd>
5418
5419<p><a name="functions"></a><br>
5420<font size=+1><b>Script functions</b></font>
5421
5422<p>
5423Here is an alphabetical list of the various functions in 3D.lua you might
5424want to call from your own scripts:
5425<p>
5426<dd>
5427<table cellspacing=0 cellpadding=0>
5428<tr>
5429<td valign=top>
5430<a href="#CancelPaste"><b>CancelPaste</b></a><br>
5431<a href="#CancelSelection"><b>CancelSelection</b></a><br>
5432<a href="#CheckWindowSize"><b>CheckWindowSize</b></a><br>
5433<a href="#ClearOutside"><b>ClearOutside</b></a><br>
5434<a href="#ClearSelection"><b>ClearSelection</b></a><br>
5435<a href="#CopySelection"><b>CopySelection</b></a><br>
5436<a href="#CutSelection"><b>CutSelection</b></a><br>
5437<a href="#DeselectCell"><b>DeselectCell</b></a><br>
5438<a href="#DoPaste"><b>DoPaste</b></a><br>
5439<a href="#DrawMode"><b>DrawMode</b></a><br>
5440<a href="#FitGrid"><b>FitGrid</b></a><br>
5441<a href="#FlipPaste"><b>FlipPaste</b></a><br>
5442<a href="#FlipSelection"><b>FlipSelection</b></a><br>
5443<a href="#GetBarHeight"><b>GetBarHeight</b></a><br>
5444<a href="#GetBounds"><b>GetBounds</b></a>
5445</td>
5446<td valign=top width=30> </td>
5447<td valign=top>
5448<a href="#GetCell"><b>GetCell</b></a><br>
5449<a href="#GetCells"><b>GetCells</b></a><br>
5450<a href="#GetCellType"><b>GetCellType</b></a><br>
5451<a href="#GetGeneration"><b>GetGeneration</b></a><br>
5452<a href="#GetGridSize"><b>GetGridSize</b></a><br>
5453<a href="#GetPasteBounds"><b>GetPasteBounds</b></a><br>
5454<a href="#GetPercentage"><b>GetPercentage</b></a><br>
5455<a href="#GetPopulation"><b>GetPopulation</b></a><br>
5456<a href="#GetRule"><b>GetRule</b></a><br>
5457<a href="#GetSelectionBounds"><b>GetSelectionBounds</b></a><br>
5458<a href="#GetStepSize"><b>GetStepSize</b></a><br>
5459<a href="#HandleKey"><b>HandleKey</b></a><br>
5460<a href="#InitialView"><b>InitialView</b></a><br>
5461<a href="#MoveMode"><b>MoveMode</b></a><br>
5462<a href="#NewPattern"><b>NewPattern</b></a>
5463</td>
5464<td valign=top width=30> </td>
5465<td valign=top>
5466<a href="#OpenPattern"><b>OpenPattern</b></a><br>
5467<a href="#Paste"><b>Paste</b></a><br>
5468<a href="#PasteExists"><b>PasteExists</b></a><br>
5469<a href="#PutCells"><b>PutCells</b></a><br>
5470<a href="#RandomPattern"><b>RandomPattern</b></a><br>
5471<a href="#Reset"><b>Reset</b></a><br>
5472<a href="#RestoreState"><b>RestoreState</b></a><br>
5473<a href="#Rotate"><b>Rotate</b></a><br>
5474<a href="#RotatePaste"><b>RotatePaste</b></a><br>
5475<a href="#RotateSelection"><b>RotateSelection</b></a><br>
5476<a href="#RunScript"><b>RunScript</b></a><br>
5477<a href="#SavePattern"><b>SavePattern</b></a><br>
5478<a href="#SaveState"><b>SaveState</b></a><br>
5479<a href="#SelectAll"><b>SelectAll</b></a><br>
5480<a href="#SelectCell"><b>SelectCell</b></a>
5481</td>
5482<td valign=top width=30> </td>
5483<td valign=top>
5484<a href="#SelectedCell"><b>SelectedCell</b></a><br>
5485<a href="#SelectionExists"><b>SelectionExists</b></a><br>
5486<a href="#SelectMode"><b>SelectMode</b></a><br>
5487<a href="#SetCell"><b>SetCell</b></a><br>
5488<a href="#SetCellType"><b>SetCellType</b></a><br>
5489<a href="#SetGridSize"><b>SetGridSize</b></a><br>
5490<a href="#SetMessage"><b>SetMessage</b></a><br>
5491<a href="#SetRule"><b>SetRule</b></a><br>
5492<a href="#SetStepSize"><b>SetStepSize</b></a><br>
5493<a href="#Step"><b>Step</b></a><br>
5494<a href="#Update"><b>Update</b></a><br>
5495<a href="#ZoomIn"><b>ZoomIn</b></a><br>
5496<a href="#ZoomOut"><b>ZoomOut</b></a>
5497</td>
5498</tr>
5499</table>
5500</dd>
5501</p>
5502
5503<a name="CancelPaste"></a><p><dt><b>CancelPaste()</b></dt>
5504<dd>
5505Remove any existing paste pattern.
5506</dd>
5507
5508<a name="CancelSelection"></a><p><dt><b>CancelSelection()</b></dt>
5509<dd>
5510Deselect all selected cells.
5511</dd>
5512
5513<a name="CheckWindowSize"></a><p><dt><b>CheckWindowSize()</b></dt>
5514<dd>
5515If the Golly window size has changed then this function resizes the overlay.
5516Useful in scripts that allow user interaction.
5517</dd>
5518
5519<a name="ClearOutside"></a><p><dt><b>ClearOutside()</b></dt>
5520<dd>
5521Delete all live cells that are not selected.
5522</dd>
5523
5524<a name="ClearSelection"></a><p><dt><b>ClearSelection()</b></dt>
5525<dd>
5526Delete all live cells that are selected.
5527Note that the cells remain selected.
5528</dd>
5529
5530<a name="CopySelection"></a><p><dt><b>CopySelection()</b></dt>
5531<dd>
5532Return true if all the selected live cells can be saved in the clipboard
5533as an RLE3 pattern.
5534Return false if there are no selected live cells.
5535</dd>
5536
5537<a name="CutSelection"></a><p><dt><b>CutSelection()</b></dt>
5538<dd>
5539Return true if all the selected live cells can be saved in the clipboard
5540as an RLE3 pattern. If so then all the selected live cells are deleted.
5541Return false if there are no selected live cells.
5542</dd>
5543
5544<a name="DeselectCell"></a><p><dt><b>DeselectCell(<i>x, y, z</i>)</b></dt>
5545<dd>
5546Deselect the given cell.
5547The x,y,z coordinates are relative to the middle cell in the grid.
5548</dd>
5549
5550<a name="DoPaste"></a><p><dt><b>DoPaste(<i>x, y, z, mode</i>)</b></dt>
5551<dd>
5552If a paste pattern exists then move it to the given position
5553and paste it into the grid using the given mode ("or" or "xor").
5554The x,y,z coordinates are relative to the middle cell in the grid
5555and specify the desired position of the paste boundary's minimum corner.
5556</dd>
5557
5558<a name="DrawMode"></a><p><dt><b>DrawMode()</b></dt>
5559<dd>
5560Switch to the pencil cursor.
5561The next <a href="#Update">Update</a> call will display the active plane.
5562</dd>
5563
5564<a name="FitGrid"></a><p><dt><b>FitGrid()</b></dt>
5565<dd>
5566Zoom in or out so that the entire grid will be visible.
5567Call <a href="#Update">Update</a> to see the result.
5568</dd>
5569
5570<a name="FlipPaste"></a><p><dt><b>FlipPaste(<i>coordinates</i>)</b></dt>
5571<dd>
5572If the paste pattern exists then flip the given coordinates ("x", "y" or "z").
5573For example, if given "x" then the X coordinates of all cells in the paste pattern will be
5574reflected across the YZ plane running through the middle of the paste pattern.
5575</dd>
5576
5577<a name="FlipSelection"></a><p><dt><b>FlipSelection(<i>coordinates</i>)</b></dt>
5578<dd>
5579If a selection exists then flip the given coordinates ("x", "y" or "z").
5580For example, if given "x" then the X coordinates of all selected cells will be
5581reflected across the YZ plane running through the middle of the selection.
5582</dd>
5583
5584<a name="GetBarHeight"></a><p><dt><b>GetBarHeight()</b></dt>
5585<dd>
5586Return the combined height of the menu bar and tool bar.
5587The value will be 0 if the user has turned them off (by hitting the "T" key)
5588or switched to full screen mode.
5589Useful in scripts that allow user interaction.
5590</dd>
5591
5592<a name="GetBounds"></a><p><dt><b>GetBounds()</b></dt>
5593<dd>
5594Return {} if the pattern is empty, otherwise return the minimal bounding box
5595of all live cells as an array with 6 values: {minx, maxx, miny, maxy, minz, maxz}.
5596The boundary values are relative to the middle cell in the grid.
5597</dd>
5598
5599<a name="GetCell"></a><p><dt><b>GetCell(<i>x, y, z</i>)</b></dt>
5600<dd>
5601Return the state (0 or 1) of the given cell.
5602The x,y,z coordinates are relative to the middle cell in the grid.
5603</dd>
5604
5605<a name="GetCells"></a><p><dt><b>GetCells(<i>selected</i>)</b></dt>
5606<dd>
5607Return an array of live cell coordinates in the format
5608{ {x1,y1,z1}, {x2,y2,z2}, ... {xn,yn,zn} }.
5609All coordinates are relative to the middle cell in the grid.
5610If there are no live cells then {} is returned.
5611If selected is true then only the coordinates of selected live cells will be returned.
5612</dd>
5613
5614<a name="GetCellType"></a><p><dt><b>GetCellType()</b></dt>
5615<dd>
5616Return the current cell type: "cube", "sphere" or "point".
5617</dd>
5618
5619<a name="GetGeneration"></a><p><dt><b>GetGeneration()</b></dt>
5620<dd>
5621Return the generation count.
5622</dd>
5623
5624<a name="GetGridSize"></a><p><dt><b>GetGridSize()</b></dt>
5625<dd>
5626Return the current grid size (3 to 100).
5627</dd>
5628
5629<a name="GetPasteBounds"></a><p><dt><b>GetPasteBounds()</b></dt>
5630<dd>
5631Return {} if there is no paste pattern, otherwise return its minimal bounding box
5632as an array with 6 values: {minx, maxx, miny, maxy, minz, maxz}.
5633The boundary values are relative to the middle cell in the grid.
5634</dd>
5635
5636<a name="GetPercentage"></a><p><dt><b>GetPercentage()</b></dt>
5637<dd>
5638Return the percentage (0 to 100) given in the most recent File > Random Pattern dialog.
5639</dd>
5640
5641<a name="GetPopulation"></a><p><dt><b>GetPopulation()</b></dt>
5642<dd>
5643Return the number of live cells in the current pattern.
5644</dd>
5645
5646<a name="GetRule"></a><p><dt><b>GetRule()</b></dt>
5647<dd>
5648Return the current rule.
5649</dd>
5650
5651<a name="GetSelectionBounds"></a><p><dt><b>GetSelectionBounds()</b></dt>
5652<dd>
5653Return {} if there are no selected cells, otherwise return the minimal bounding box
5654of all selected cells (live or dead) as an array with 6 values: {minx, maxx, miny, maxy, minz, maxz}.
5655The boundary values are relative to the middle cell in the grid.
5656</dd>
5657
5658<a name="GetStepSize"></a><p><dt><b>GetStepSize()</b></dt>
5659<dd>
5660Return the current step size (1 to 100).
5661</dd>
5662
5663<a name="HandleKey"></a><p><dt><b>HandleKey(<i>event</i>)</b></dt>
5664<dd>
5665Process the given keyboard event.
5666Useful in scripts that allow user interaction.
5667</dd>
5668
5669<a name="InitialView"></a><p><dt><b>InitialView()</b></dt>
5670<dd>
5671Restore the initial view displayed by 3D.lua when it first starts up.
5672Call <a href="#Update">Update</a> to see the result.
5673</dd>
5674
5675<a name="MoveMode"></a><p><dt><b>MoveMode()</b></dt>
5676<dd>
5677Switch to the hand cursor.
5678</dd>
5679
5680<a name="NewPattern"></a><p><dt><b>NewPattern(<i>title</i>)</b></dt>
5681<dd>
5682Create a new, empty pattern.
5683All undo/redo history is deleted and the step size is reset to 1.
5684The given title string will appear in the title bar of the Golly window.
5685If not supplied it is set to "untitled".
5686</dd>
5687
5688<a name="OpenPattern"></a><p><dt><b>OpenPattern(<i>filepath</i>)</b></dt>
5689<dd>
5690Open the specified RLE3 pattern file. If the <i>filepath</i> is not supplied then
5691the user will be prompted to select a .rle3 file.
5692All undo/redo history is deleted and the step size is reset to 1.
5693</dd>
5694
5695<a name="Paste"></a><p><dt><b>Paste()</b></dt>
5696<dd>
5697Return true if the clipboard contains a valid, non-empty pattern (RLE3 or 2D) that fits
5698within the current grid. If the active plane exists and the clipboard pattern is
5699one cell thick (in any direction) then the paste pattern is located in the middle
5700of the active plane, otherwise it is located in the middle of the grid.
5701You can then call <a href="#FlipPaste">FlipPaste</a> or
5702<a href="#RotatePaste">RotatePaste</a> to modify the paste pattern.
5703Call <a href="#DoPaste">DoPaste</a> when you want to actually paste the pattern into the grid.
5704</dd>
5705
5706<a name="PasteExists"></a><p><dt><b>PasteExists()</b></dt>
5707<dd>
5708Return true if a paste pattern exists.
5709</dd>
5710
5711<a name="PutCells"></a><p><dt><b>PutCells(<i>cellarray, xoffset, yoffset, zoffset</i>)</b></dt>
5712<dd>
5713Use the given array of live cell coordinates and offsets to set cells in the grid.
5714The array must have the same format as the one returned by <a href="#GetCells">GetCells</a>.
5715Any cells outside the grid are silently clipped.
5716</dd>
5717
5718<a name="RandomPattern"></a><p><dt><b>RandomPattern(<i>percentage, fill, sphere</i>)</b></dt>
5719<dd>
5720Create a new, random pattern with the given percentage density (0 to 100) of live cells.
5721If fill is true then the grid is filled, otherwise there will be a gap around
5722the random object (this makes it easier to detect an explosive rule).
5723If sphere is true then a spherical object is created rather than a cube.
5724If no parameters are supplied then the user will be prompted for them.
5725All undo/redo history is deleted and the step size is reset to 1.
5726</dd>
5727
5728<a name="Reset"></a><p><dt><b>Reset()</b></dt>
5729<dd>
5730Restore the starting generation and step size.
5731</dd>
5732
5733<a name="RestoreState"></a><p><dt><b>RestoreState(<i>state</i>)</b></dt>
5734<dd>
5735Restore the state saved earlier by <a href="#SaveState">SaveState</a>.
5736</dd>
5737
5738<a name="Rotate"></a><p><dt><b>Rotate(<i>xdegrees, ydegrees, zdegrees</i>)</b></dt>
5739<dd>
5740Rotate the grid axes by the given amounts (integers from -359 to +359).
5741Call <a href="#Update">Update</a> to see the result.
5742</dd>
5743
5744<a name="RotatePaste"></a><p><dt><b>RotatePaste(<i>axis</i>)</b></dt>
5745<dd>
5746If the paste pattern exists then rotate it 90 degrees clockwise
5747about the given axis ("x", "y" or "z").
5748</dd>
5749
5750<a name="RotateSelection"></a><p><dt><b>RotateSelection(<i>axis</i>)</b></dt>
5751<dd>
5752If a selection exists then rotate it 90 degrees clockwise
5753about the given axis ("x", "y" or "z").
5754</dd>
5755
5756<a name="RunScript"></a><p><dt><b>RunScript(<i>filepath</i>)</b></dt>
5757<dd>
5758Run the specified .lua file, but only if the string "3D.lua" occurs
5759somewhere in a comment on the first line of the file.
5760If the <i>filepath</i> is not supplied then the user will be prompted
5761to select a .lua file.
5762</dd>
5763
5764<a name="SavePattern"></a><p><dt><b>SavePattern(<i>filepath</i>)</b></dt>
5765<dd>
5766Save the current pattern in a specified RLE3 file. If the <i>filepath</i>
5767is not supplied then the user will be prompted for its name and location.
5768</dd>
5769
5770<a name="SaveState"></a><p><dt><b>SaveState()</b></dt>
5771<dd>
5772Return an object representing the current state. The object can be given
5773later to <a href="#RestoreState">RestoreState</a> to restore the saved state.
5774The saved information includes the grid size, the active plane's orientation
5775and position, the cursor mode, the rule, the pattern and its generation count,
5776the step size, the selection, and the paste pattern.
5777</dd>
5778
5779<a name="SelectAll"></a><p><dt><b>SelectAll()</b></dt>
5780<dd>
5781Select all live cells. If there are no live cells then any existing selection
5782is removed.
5783</dd>
5784
5785<a name="SelectCell"></a><p><dt><b>SelectCell(<i>x, y, z</i>)</b></dt>
5786<dd>
5787Select the given cell.
5788The x,y,z coordinates are relative to the middle cell in the grid.
5789</dd>
5790
5791<a name="SelectedCell"></a><p><dt><b>SelectedCell(<i>x, y, z</i>)</b></dt>
5792<dd>
5793Return true if the given cell is selected (live or dead).
5794</dd>
5795
5796<a name="SelectionExists"></a><p><dt><b>SelectionExists()</b></dt>
5797<dd>
5798Return true if at least one cell is selected (live or dead).
5799</dd>
5800
5801<a name="SelectMode"></a><p><dt><b>SelectMode()</b></dt>
5802<dd>
5803Switch to the cross-hairs cursor.
5804The next <a href="#Update">Update</a> call will display the active plane.
5805</dd>
5806
5807<a name="SetCell"></a><p><dt><b>SetCell(<i>x, y, z, state</i>)</b></dt>
5808<dd>
5809Set the given cell to the given state (0 or 1).
5810If the state is not supplied then it defaults to 1.
5811The x,y,z coordinates are relative to the middle cell in the grid.
5812</dd>
5813
5814<a name="SetCellType"></a><p><dt><b>SetCellType(<i>string</i>)</b></dt>
5815<dd>
5816Set the cell type to "cube", "sphere" or "point".
5817</dd>
5818
5819<a name="SetGridSize"></a><p><dt><b>SetGridSize(<i>newsize</i>)</b></dt>
5820<dd>
5821Change the grid size to the new value (3 to 100).
5822If the <i>newsize</i> is not supplied then the user will be prompted for a value.
5823</dd>
5824
5825<a name="SetMessage"></a><p><dt><b>SetMessage(<i>msg</i>)</b></dt>
5826<dd>
5827The given string will be displayed by the next <a href="#Update">Update</a> call.
5828Call SetMessage(nil) to clear the message.
5829</dd>
5830
5831<a name="SetRule"></a><p><dt><b>SetRule(<i>rule</i>)</b></dt>
5832<dd>
5833Switch to the given rule. If <i>rule</i> is not supplied the default rule
5834is used (3D5..7/6).
5835</dd>
5836
5837<a name="SetStepSize"></a><p><dt><b>SetStepSize(<i>newsize</i>)</b></dt>
5838<dd>
5839Set the step size to the given value (1 to 100).
5840</dd>
5841
5842<a name="Step"></a><p><dt><b>Step(<i>n</i>)</b></dt>
5843<dd>
5844While the population is > 0 calculate the next <i>n</i> generations.
5845If <i>n</i> is not supplied it defaults to 1.
5846</dd>
5847
5848<a name="Update"></a><p><dt><b>Update()</b></dt>
5849<dd>
5850Update the display. Note that 3D.lua automatically updates the display
5851when a script finishes, so there's no need to call Update() at the
5852end of a script.
5853</dd>
5854
5855<a name="ZoomIn"></a><p><dt><b>ZoomIn()</b></dt>
5856<dd>
5857Zoom in by incrementing the cell size.
5858Useful in scripts that allow user interaction.
5859</dd>
5860
5861<a name="ZoomOut"></a><p><dt><b>ZoomOut()</b></dt>
5862<dd>
5863Zoom out by decrementing the cell size.
5864Useful in scripts that allow user interaction.
5865</dd>
5866
5867<p><a name="coords"></a><br>
5868<font size=+1><b>Cell coordinates</b></font>
5869
5870<p>
5871Many of the above script functions accept or return cell coordinates.
5872All coordinates are relative to the middle cell in the grid, so a call
5873like SetCell(0,0,0,1) will turn on the middle cell.
5874If N is the grid size then the minimum coordinate value is -floor(N/2)
5875and the maximum coordinate value is floor((N-1)/2).
5876
5877<p>
5878The following snippet creates a diagonal line of cells from the
5879grid corner with the minimum cell coordinates to the corner with
5880the maximum cell coordinates:
5881
5882<dd><table border=0><pre>
5883-- for 3D.lua
5884local N = GetGridSize()
5885for c = -(N//2), (N-1)//2 do
5886 SetCell(c, c, c, 1)
5887end</pre></table></dd>
5888
5889<p><a name="rules"></a><br>
5890<font size=+1><b>Supported rules</b></font>
5891
5892<p>
58933D.lua supports rules that use a number of different neighborhoods:
5894
5895<p><ul>
5896<li>
5897The Moore neighborhood consists of the 26 cells that form a cube around a central cell.
5898<li>
5899The Face neighborhood consists of the 6 cells adjacent to the faces of a cube
5900(this is the 3D version of the von Neumann neighborhood).
5901<li>
5902The Corner neighborhood consists of the 8 cells adjacent to the corners of a cube.
5903<li>
5904The Edge neighborhood consists of the 12 cells adjacent to the edges of a cube.
5905<li>
5906The Hexahedral neighborhood simulates 12 spheres packed around a central sphere
5907(also known as the face-centred cubic lattice, or the rhombic dodecahedral honeycomb).
5908Because it is simulating a hexahedral tessellation in a cubic grid, this neighborhood
5909is not orthogonally symmetric, so flipping or rotating a pattern can change the way it evolves.
5910<li>
5911The Busy Boxes neighborhood is rather complicated. Follow the links below if you
5912want to know all the gory details.
5913</ul>
5914
5915<p>
5916Use Control > Set Rule to change the current rule.
5917You can quickly restore 3D.lua's default rule (3D5..7/6) by simply deleting
5918the current rule and hitting OK.
5919
5920<p><a name="moore"></a><br>
5921<font size=+1><b>Moore neighborhood</b></font>
5922
5923<p>
5924Rules in this neighborhood are strings of the form "3DS,S,S,.../B,B,B,...".
5925The S values are the counts of neighboring live cells required
5926for a live cell to survive in the next generation.
5927The B values are the counts of neighboring live cells required for
5928birth; ie. a dead cell will become a live cell in the next generation.
5929Each cell has 26 neighbors so the S counts are from 0 to 26
5930and the B counts are from 1 to 26 (birth on 0 is not allowed).
5931Note that the Moore neighborhood is the combination of the Face+Corner+Edge neighborhoods.
5932
5933<p>
5934Contiguous counts can be specified as a range, so a rule like
59353D4,5,6,7,9/4,5,7 can be entered as 3D4..7,9/4,5,7 (this is the
5936canonical version).
5937
5938<p><a name="face"></a><br>
5939<font size=+1><b>Face neighborhood</b></font>
5940
5941<p>
5942Rules in this neighborhood use the same syntax as the Moore neighborhood
5943but with "F" appended. For example: 3D0..6/1,3F.
5944Each cell has 6 neighbors so the S counts are from 0 to 6 and the
5945B counts are from 1 to 6 (again, birth on 0 is not allowed).
5946
5947<p><a name="corner"></a><br>
5948<font size=+1><b>Corner neighborhood</b></font>
5949
5950<p>
5951Rules use the same syntax as the Moore neighborhood but with "C" appended.
5952Each cell has 8 neighbors so the S counts are from 0 to 8 and the
5953B counts are from 1 to 8.
5954
5955<p><a name="edge"></a><br>
5956<font size=+1><b>Edge neighborhood</b></font>
5957
5958<p>
5959Rules use the same syntax as the Moore neighborhood but with "E" appended.
5960Each cell has 12 neighbors so the S counts are from 0 to 12 and the
5961B counts are from 1 to 12.
5962
5963<p><a name="hex"></a><br>
5964<font size=+1><b>Hexahedral neighborhood</b></font>
5965
5966<p>
5967Rules use the same syntax as the Moore neighborhood but with "H" appended.
5968Each cell has 12 neighbors so the S counts are from 0 to 12 and the
5969B counts are from 1 to 12.
5970
5971<p><a name="busy"></a><br>
5972<font size=+1><b>Busy Boxes</b></font>
5973
5974<p>
5975Busy Boxes is a 3D reversible CA created by Ed Fredkin and Daniel B. Miller.
59763D.lua supports two rules: BusyBoxes and BusyBoxesW. The first rule uses a
5977"mirror" mode where gliders are reflected back when they reach a grid boundary.
5978The second rule is for "wrap" mode where gliders can cross a boundary and appear
5979in the opposite side of the grid.
5980
5981<p>
5982Each cell in the grid is either odd or even, depending on whether the sum of the
5983cell's x,y,z coordinates is odd or even.
59843D.lua uses cyan for odd cells and magenta for even cells (Fredkin and Miller use
5985blue and red but these colors are used by 3D.lua to display paste patterns
5986and the active plane).
5987
5988<p>
5989Each generation of a Busy Boxes pattern is in one of six phases, numbered 0 to 5.
5990In even phases, only even (magenta) cells can move.
5991In odd phases, only odd (cyan) cells can move.
5992In phases 0 and 3, movement can only occur in the XY plane.
5993In phases 1 and 4 movement can only occur in the YZ plane.
5994In phases 2 and 5, movement can only occur in the XZ plane.
5995For each diagonally opposite pair of cells in the current plane, if a live cell exists
5996at either of the pair's knight move positions, then the states of the two cells can be swapped.
5997However, this only happens if there is no other possible swap for either cell.
5998
5999<p>
6000For a pattern with a sparse or small population it's quite likely that no swaps
6001are possible in a particular phase. Or the only valid swaps might be between
6002two empty cells or two live cells. Either way, the pattern won't change.
6003At each generation, a live cell can only move into a diagonally opposite empty cell
6004(and only in the same orthogonal plane).
6005This also means that the initial population never changes.
6006For more details see the <a href="http://www.busyboxes.org/faq.html">Busy Boxes FAQ</a>
6007and the papers listed in the <a href="#refs">references</a>.
6008
6009<p><a name="rle3"></a><br>
6010<font size=+1><b>RLE3 file format</b></font>
6011
6012<p>
60133D.lua can read and write patterns as text files with a .rle3 extension.
6014The file format is known as RLE3 and is a simple extension of the well-known
6015RLE format used by Golly:
6016
6017<p>
6018<ul>
6019<li>
6020The first line must start with "3D" and be followed by a number of "keyword=value"
6021pairs separated by spaces. The valid keywords are:
6022
6023<p><table border=0 cellspacing=0 cellpadding=0>
6024<tr><td> version=<i>i</i> </td> <td> — specifies the file format version (currently 1)</td></tr>
6025<tr><td> size=<i>N</i> </td> <td> — specifies the grid dimensions (<i>N</i>x<i>N</i>x<i>N</i>)</td></tr>
6026<tr><td> pos=<i>x,y,z</i> </td> <td> — specifies the pattern's position within the grid</td></tr>
6027<tr><td> gen=<i>g</i> </td> <td> — specifies the generation number</td></tr>
6028<tr><td> </td></tr>
6029</table>
6030
6031<li>
6032If the pos and gen keywords are not present then their values are set to 0.
6033Any unknown keywords are simply ignored.
6034<li>
6035The first line can be followed by optional comment lines starting with "#".
6036<li>
6037Then comes a line specifying the pattern's size and the 3D rule string:
6038
6039<p><table border=0 cellspacing=0 cellpadding=0>
6040<tr><td> x=<i>width</i> y=<i>height</i> z=<i>depth</i> rule=<i>string</i></td></tr>
6041<tr><td> </td></tr>
6042</table>
6043
6044<li>
6045The remaining lines contain the pattern data in a run-length encoded format.
6046The only difference to the standard RLE format is the use of "/"
6047to move to the next plane (ie. increase the z coordinate).
6048<li>
6049Any empty lines (after the first line) are ignored.
6050</ul>
6051
6052<p>
6053The following is a small example of the RLE3 file format.
6054You can either save it in a .rle3 file, or copy it to the clipboard
6055and type shift-O (after returning to the 3D.lua window):
6056
6057<dd><table border=0><pre>
60583D version=1 size=40 pos=19,18,18
6059# A 10c/10 orthogonal spaceship.
6060# Found by Andrew Trevorrow in April, 2018.
6061x=2 y=4 z=4 rule=3D4,7/5,8
6062$bo$bo/bo$bo$bo$oo/oo$bo$bo$bo/$bo$bo!</pre></table></dd>
6063
6064<p><a name="refs"></a><br>
6065<font size=+1><b>Credits and references</b></font>
6066
6067<p>
60683D.lua was inspired by the work of Carter Bays and his colleagues:
6069
6070<p>
6071Candidates for the Game of Life in Three Dimensions<br>
6072<a href="http://www.complex-systems.com/pdf/01-3-1.pdf">http://www.complex-systems.com/pdf/01-3-1.pdf</a>
6073
6074<p>
6075Patterns for Simple Cellular Automata in a Universe of Dense-Packed Spheres<br>
6076<a href="http://www.complex-systems.com/pdf/01-5-1.pdf">http://www.complex-systems.com/pdf/01-5-1.pdf</a>
6077
6078<p>
6079Classification of Semitotalistic Cellular Automata in Three Dimensions<br>
6080<a href="http://www.complex-systems.com/pdf/02-2-6.pdf">http://www.complex-systems.com/pdf/02-2-6.pdf</a>
6081
6082<p>
6083A Note on the Discovery of a New Game of Three-dimensional Life<br>
6084<a href="http://www.complex-systems.com/pdf/02-3-1.pdf">http://www.complex-systems.com/pdf/02-3-1.pdf</a>
6085
6086<p>
6087The Discovery of a New Glider for the Game of Three-Dimensional Life<br>
6088<a href="http://www.complex-systems.com/pdf/04-6-2.pdf">http://www.complex-systems.com/pdf/04-6-2.pdf</a>
6089
6090<p>
6091Further Notes on the Game of Three-Dimensional Life<br>
6092<a href="http://www.complex-systems.com/pdf/08-1-4.pdf">http://www.complex-systems.com/pdf/08-1-4.pdf</a>
6093
6094<p>
6095A Note About the Discovery of Many New Rules for the Game of Three-Dimensional Life<br>
6096<a href="http://wpmedia.wolfram.com/uploads/sites/13/2018/02/16-4-7.pdf">http://wpmedia.wolfram.com/uploads/sites/13/2018/02/16-4-7.pdf</a>
6097
6098<p>
6099References for Busy Boxes:
6100
6101<p>
6102Website: <a href="http://www.busyboxes.org">http://www.busyboxes.org</a>
6103
6104<p>
6105Two State, Reversible, Universal Cellular Automata In Three Dimensions<br>
6106<a href="https://arxiv.org/ftp/nlin/papers/0501/0501022.pdf">https://arxiv.org/ftp/nlin/papers/0501/0501022.pdf</a>
6107
6108<p>
6109Circular Motion of Strings in Cellular Automata, and Other Surprises<br>
6110<a href="https://arxiv.org/abs/1206.2060">https://arxiv.org/abs/1206.2060</a>
6111
6112</body></html>
6113]]
6114
6115 if g.os() == "Mac" then
6116 htmldata = htmldata:gsub(" enter ", " return ")
6117 htmldata = htmldata:gsub(" alt", " option")
6118 htmldata = htmldata:gsub("ctrl", "cmd")
6119 end
6120
6121 local htmlfile = g.getdir("temp").."3D.html"
6122 local f = io.open(htmlfile,"w")
6123 if not f then
6124 g.warn("Failed to create 3D.html!", false)
6125 return
6126 end
6127 f:write(htmldata)
6128 f:close()
6129
6130 g.open(htmlfile)
6131end
6132
6133--------------------------------------------------------------------------------
6134
6135function SetCursor(cursor)
6136 if currcursor ~= cursor then
6137 RememberCurrentState()
6138 currcursor = cursor
6139 if not arrow_cursor then ov("cursor "..currcursor) end
6140 CheckIfGenerating()
6141 end
6142end
6143
6144--------------------------------------------------------------------------------
6145
6146function DrawMode()
6147 -- called when drawbox is clicked
6148 SetCursor(drawcursor)
6149 Refresh()
6150end
6151
6152--------------------------------------------------------------------------------
6153
6154function SelectMode()
6155 -- called when selectbox is clicked
6156 SetCursor(selectcursor)
6157 Refresh()
6158end
6159
6160--------------------------------------------------------------------------------
6161
6162function MoveMode()
6163 -- called when movebox is clicked
6164 SetCursor(movecursor)
6165 Refresh()
6166end
6167
6168--------------------------------------------------------------------------------
6169
6170function CycleCursor()
6171 -- cycle to next cursor mode
6172 if currcursor == drawcursor then
6173 SelectMode()
6174 elseif currcursor == selectcursor then
6175 MoveMode()
6176 else -- currcursor == movecursor
6177 DrawMode()
6178 end
6179end
6180
6181
6182--------------------------------------------------------------------------------
6183
6184function SetCellTypeOnly(newtype)
6185 celltype = newtype
6186 ovt{"setcelltype3d", newtype}
6187end
6188
6189--------------------------------------------------------------------------------
6190
6191function CycleCellType()
6192 if celltype == "cube" then
6193 celltype = "sphere"
6194 elseif celltype == "sphere" then
6195 celltype = "point"
6196 else -- celltype == "point"
6197 celltype = "cube"
6198 end
6199 SetCellTypeOnly(celltype)
6200 ViewChanged(false)
6201 RefreshIfNotGenerating()
6202end
6203
6204--------------------------------------------------------------------------------
6205
6206function SetCellType(newtype)
6207 SetCellTypeOnly(newtype)
6208 ViewChanged(false)
6209 RefreshIfNotGenerating()
6210end
6211
6212--------------------------------------------------------------------------------
6213
6214function ToggleAxes()
6215 showaxes = not showaxes
6216 RefreshIfNotGenerating()
6217end
6218
6219--------------------------------------------------------------------------------
6220
6221function ToggleLines()
6222 showlines = not showlines
6223 RefreshIfNotGenerating()
6224end
6225
6226--------------------------------------------------------------------------------
6227
6228function ToggleDepthShading()
6229 depthshading = not depthshading
6230 InitDepthShading()
6231 ViewChanged(false)
6232 RefreshIfNotGenerating()
6233end
6234
6235--------------------------------------------------------------------------------
6236
6237function UpdateHistory()
6238 ovt{"sethistory3d", showhistory, fadehistory}
6239 ViewChanged(false)
6240end
6241
6242--------------------------------------------------------------------------------
6243
6244function ToggleShowHistory()
6245 if showhistory == HISTORYOFF then
6246 showhistory = DEFAULTHISTORY
6247 else
6248 showhistory = HISTORYOFF
6249 end
6250 UpdateHistory()
6251 RefreshIfNotGenerating()
6252end
6253
6254--------------------------------------------------------------------------------
6255
6256function ToggleFadeHistory()
6257 fadehistory = not fadehistory
6258 UpdateHistory()
6259 RefreshIfNotGenerating()
6260end
6261
6262--------------------------------------------------------------------------------
6263
6264function ToggleToolBar()
6265 if toolbarht > 0 then
6266 toolbarht = 0
6267 midy = int(ovht/2)
6268 -- hide all the controls
6269 mbar.hide()
6270 ssbutton.hide()
6271 s1button.hide()
6272 resetbutton.hide()
6273 fitbutton.hide()
6274 undobutton.hide()
6275 redobutton.hide()
6276 drawbox.hide()
6277 selectbox.hide()
6278 movebox.hide()
6279 stepslider.hide()
6280 exitbutton.hide()
6281 helpbutton.hide()
6282 else
6283 toolbarht = mbarht+buttonht+gap*2
6284 midy = int(ovht/2 + toolbarht/2)
6285 end
6286 Refresh()
6287end
6288
6289--------------------------------------------------------------------------------
6290
6291function ExitScript()
6292 if dirty and scriptlevel == 0 then
6293 local answer = g.savechanges("Save your changes?",
6294 "If you don't save, the changes will be lost.")
6295 if answer == "yes" then
6296 SavePattern()
6297 if dirty then
6298 -- error occurred or user hit Cancel in g.savedialog
6299 return
6300 end
6301 elseif answer == "no" then
6302 g.exit()
6303 else -- answer == "cancel"
6304 return
6305 end
6306 end
6307 g.exit()
6308end
6309
6310--------------------------------------------------------------------------------
6311
6312--[[ no longer used, but might come in handy
6313
6314local function PointInPolygon(x, y, vertices)
6315 -- return true if the given mouse position is inside the given polygon
6316 -- (see https://stackoverflow.com/questions/31730923/check-if-point-lies-in-polygon-lua)
6317 local inside = false
6318 local n = #vertices
6319 local j = n
6320 for i = 1, n do
6321 local pix = projectedx[ vertices[i] ]
6322 local piy = projectedy[ vertices[i] ]
6323 local pjx = projectedx[ vertices[j] ]
6324 local pjy = projectedy[ vertices[j] ]
6325 if (piy < y and pjy >= y) or
6326 (pjy < y and piy >= y) then
6327 if pix + (y - piy) / (pjy - piy) * (pjx - pix) < x then
6328 inside = not inside
6329 end
6330 end
6331 j = i
6332 end
6333 return inside
6334end
6335
6336--]]
6337
6338--------------------------------------------------------------------------------
6339
6340local function PointInTriangle(x, y, A, B, C)
6341 -- return true if x,y is inside the given triangle
6342 -- (see https://stackoverflow.com/questions/2049582/how-to-determine-if-a-point-is-in-a-2d-triangle)
6343 local ax, ay = projectedx[A], projectedy[A]
6344 local bx, by = projectedx[B], projectedy[B]
6345 local cx, cy = projectedx[C], projectedy[C]
6346 local as_x = x - ax
6347 local as_y = y - ay
6348 local s_ab = (bx-ax)*as_y - (by-ay)*as_x >= 0
6349 if (cx-ax)*as_y - (cy-ay)*as_x > 0 == s_ab then return false end
6350 if (cx-bx)*(y-by) - (cy-by)*(x-bx) > 0 ~= s_ab then return false end
6351 return true
6352end
6353
6354--------------------------------------------------------------------------------
6355
6356local function PointInFace(x, y, P, Q, R, S)
6357 -- return true if x,y is inside the given face (a parallelogram)
6358 return PointInTriangle(x, y, P, Q, R) or PointInTriangle(x, y, R, S, P)
6359end
6360
6361--------------------------------------------------------------------------------
6362
6363local function IntersectionPoint(x1,y1, x2,y2, x3,y3, x4,y4)
6364 -- return the intersection point of 2 line segments
6365 -- (see http://paulbourke.net/geometry/pointlineplane/pdb.c)
6366 local denom = (y4-y3) * (x2-x1) - (x4-x3) * (y2-y1)
6367 local numera = (x4-x3) * (y1-y3) - (y4-y3) * (x1-x3)
6368 local numerb = (x2-x1) * (y1-y3) - (y2-y1) * (x1-x3)
6369
6370 -- check if the lines are coincident
6371 if abs(numera) < 0.0001 and abs(numerb) < 0.0001 and abs(denom) < 0.0001 then
6372 return (x1 + x2) / 2, (y1 + y2) / 2
6373 end
6374
6375 local mua = numera / denom
6376 return x1 + mua * (x2 - x1), y1 + mua * (y2 - y1)
6377end
6378
6379--------------------------------------------------------------------------------
6380
6381local function FindActiveCell(x, y, face)
6382 -- return cell coordinates of the active cell containing x,y
6383 -- which is somewhere inside the given face
6384 local mid = N//2
6385 local shift = N*CELLSIZE
6386 local hcells = N
6387 local vcells = N
6388 local xoffset = 0
6389 local yoffset = 0
6390 local zoffset = 0
6391 local Pv, Qv, Sv
6392 local Px, Py, Qx, Qy, Sx, Sy
6393 local Ix, Iy, Jx, Jy
6394 local x1, y1, x2, y2
6395 local A, B, a, b
6396 local cx, cy, cz
6397
6398 local function CalculateAaBb()
6399 -- to find which cell contains x,y we need to calculate lengths A,a,B,b:
6400 -- (in this diagram hcells is 5 and vcells is 3, but for the given face
6401 -- the values are either 1 or N because the active plane is 1 cell thick)
6402 --
6403 -- S ___________________________________
6404 -- / / / / / /
6405 -- / / / / / /
6406 -- /______/______/______/______/______/
6407 -- J /------/------/------/-*x,y / /
6408 -- / / / / / / / /
6409 -- / /______/______/______/_/____/______/
6410 -- B // / / / / / /
6411 -- / b/ / / / / / /
6412 -- / //______/______/______/_/____/______/
6413 -- P --a--- I Q
6414 -- ---------A------------
6415 --
6416 Px, Py = projectedx[Pv], projectedy[Pv]
6417 Qx, Qy = projectedx[Qv], projectedy[Qv]
6418 Sx, Sy = projectedx[Sv], projectedy[Sv]
6419
6420 -- find intersection point of PQ and line containing x,y parallel to PS
6421 -- (we must ensure x1,y1 and x2,y2 are outside the face's edges)
6422 x1 = x - (Sx - Px) * shift
6423 y1 = y - (Sy - Py) * shift
6424 x2 = x + (Sx - Px) * shift
6425 y2 = y + (Sy - Py) * shift
6426 Ix, Iy = IntersectionPoint(Px, Py, Qx, Qy, x1, y1, x2, y2)
6427
6428 -- find intersection point of PS and line containing x,y parallel to PQ
6429 -- (we must ensure x1,y1 and x2,y2 are outside the face's edges)
6430 x1 = x - (Qx - Px) * shift
6431 y1 = y - (Qy - Py) * shift
6432 x2 = x + (Qx - Px) * shift
6433 y2 = y + (Qy - Py) * shift
6434 Jx, Jy = IntersectionPoint(Px, Py, Sx, Sy, x1, y1, x2, y2)
6435
6436 A = sqrt( (Ix-Px)^2 + (Iy-Py)^2 )
6437 B = sqrt( (Jx-Px)^2 + (Jy-Py)^2 )
6438 a = sqrt( (Qx-Px)^2 + (Qy-Py)^2 ) / hcells
6439 b = sqrt( (Sx-Px)^2 + (Sy-Py)^2 ) / vcells
6440 end
6441
6442 if activeplane == "XY" then
6443 -- F and B faces have N*N cells, the other faces (TULR) have N cells;
6444 -- we only need to use 3 vertices in each face, but they have to be
6445 -- chosen carefully so that the vectors PQ and PS span the parallelogram
6446 if face == "F" then
6447 Pv, Qv, Sv = 1, 7, 3 -- front vertices of active plane
6448 elseif face == "B" then
6449 Pv, Qv, Sv = 2, 8, 4 -- back vertices
6450 elseif face == "T" then
6451 Pv, Qv, Sv = 3, 5, 4 -- top vertices
6452 vcells = 1
6453 yoffset = N-1
6454 elseif face == "U" then
6455 Pv, Qv, Sv = 1, 7, 2 -- underneath (ie. bottom) vertices
6456 vcells = 1
6457 elseif face == "L" then
6458 Pv, Qv, Sv = 1, 2, 3 -- left vertices
6459 hcells = 1
6460 elseif face == "R" then
6461 Pv, Qv, Sv = 7, 8, 5 -- right vertices
6462 hcells = 1
6463 xoffset = N-1
6464 end
6465 CalculateAaBb()
6466 cx = floor(A / a) + xoffset; if cx >= N then cx = N-1 end
6467 cy = floor(B / b) + yoffset; if cy >= N then cy = N-1 end
6468 cx = cx - mid
6469 cy = cy - mid
6470 cz = activepos
6471
6472 elseif activeplane == "YZ" then
6473 -- L and R faces have N*N cells, the other faces (TUFB) have N cells
6474 if face == "L" then
6475 Pv, Qv, Sv = 2, 1, 4 -- left vertices
6476 elseif face == "R" then
6477 Pv, Qv, Sv = 8, 7, 6 -- right vertices
6478 elseif face == "T" then
6479 Pv, Qv, Sv = 6, 5, 4 -- top vertices
6480 vcells = 1
6481 yoffset = N-1
6482 elseif face == "U" then
6483 Pv, Qv, Sv = 8, 7, 2 -- underneath (ie. bottom) vertices
6484 vcells = 1
6485 elseif face == "F" then
6486 Pv, Qv, Sv = 1, 7, 3 -- front vertices
6487 hcells = 1
6488 zoffset = N-1
6489 elseif face == "B" then
6490 Pv, Qv, Sv = 2, 8, 4 -- back vertices
6491 hcells = 1
6492 end
6493 CalculateAaBb()
6494 cy = floor(B / b) + yoffset; if cy >= N then cy = N-1 end
6495 cz = floor(A / a) + zoffset; if cz >= N then cz = N-1 end
6496 cy = cy - mid
6497 cz = cz - mid
6498 cx = activepos
6499
6500 else -- activeplane == "XZ"
6501 -- T and U faces have N*N cells, the other faces (FBLR) have N cells
6502 if face == "T" then
6503 Pv, Qv, Sv = 4, 6, 3 -- top vertices
6504 elseif face == "U" then
6505 Pv, Qv, Sv = 2, 8, 1 -- underneath (ie. bottom) vertices
6506 elseif face == "F" then
6507 Pv, Qv, Sv = 1, 7, 3 -- front vertices
6508 vcells = 1
6509 zoffset = N-1
6510 elseif face == "B" then
6511 Pv, Qv, Sv = 4, 6, 2 -- back vertices
6512 vcells = 1
6513 elseif face == "L" then
6514 Pv, Qv, Sv = 4, 2, 3 -- left vertices
6515 hcells = 1
6516 elseif face == "R" then
6517 Pv, Qv, Sv = 6, 8, 5 -- right vertices
6518 hcells = 1
6519 xoffset = N-1
6520 end
6521 CalculateAaBb()
6522 cx = floor(A / a) + xoffset; if cx >= N then cx = N-1 end
6523 cz = floor(B / b) + zoffset; if cz >= N then cz = N-1 end
6524 cx = cx - mid
6525 cz = cz - mid
6526 cy = activepos
6527 end
6528
6529 -- return user coordinates (displayed later by Refresh)
6530 return cx, cy, cz
6531end
6532
6533--------------------------------------------------------------------------------
6534
6535function FindFace(mousex, mousey, box)
6536 -- create given box's rotated and projected vertices
6537 for i = 1, 8 do
6538 rotx[i], roty[i], rotz[i] = TransformPoint(box[i])
6539 projectedx[i] = round( rotx[i] ) + midx
6540 projectedy[i] = round( roty[i] ) + midy
6541 end
6542
6543 -- find which face of given box contains mousex,mousey (if any);
6544 -- note that up to 3 faces (all parallelograms) are visible
6545 local face = ""
6546 if rotz[1] < rotz[2] then
6547 -- front face is visible
6548 if PointInFace(mousex, mousey, 1,3,5,7) then face = "F" end
6549 else
6550 -- back face is visible
6551 if PointInFace(mousex, mousey, 8,6,4,2) then face = "B" end
6552 end
6553 if #face == 0 then
6554 -- not in front/back face, so try right and left
6555 if rotz[5] < rotz[3] then
6556 -- right face is visible
6557 if PointInFace(mousex, mousey, 5,6,8,7) then face = "R" end
6558 else
6559 -- left face is visible
6560 if PointInFace(mousex, mousey, 1,2,4,3) then face = "L" end
6561 end
6562 end
6563 if #face == 0 then
6564 -- not in front/back/right/left so try top and bottom
6565 if rotz[5] < rotz[7] then
6566 -- top face is visible
6567 if PointInFace(mousex, mousey, 5,3,4,6) then face = "T" end
6568 else
6569 -- bottom face is visible (use U for underneath; B is for back)
6570 if PointInFace(mousex, mousey, 1,7,8,2) then face = "U" end
6571 end
6572 end
6573 return face -- empty/F/B/R/L/T/U
6574end
6575
6576--------------------------------------------------------------------------------
6577
6578local function InsideActiveCell(mousex, mousey)
6579 -- if the given mouse position is inside the active plane then
6580 -- return a string containing the cell coords in the format "x,y,z"
6581 -- otherwise return an empty string
6582
6583 -- create a box enclosing all active cells using same vertex order as CreateCube
6584 local x = minactivex * CELLSIZE - MIDGRID
6585 local y = minactivey * CELLSIZE - MIDGRID
6586 local z = minactivez * CELLSIZE - MIDGRID
6587 local xlen = (maxactivex - minactivex + 1) * CELLSIZE
6588 local ylen = (maxactivey - minactivey + 1) * CELLSIZE
6589 local zlen = (maxactivez - minactivez + 1) * CELLSIZE
6590 local activebox = {
6591 {x , y , z+zlen}, -- v1
6592 {x , y , z }, -- v2
6593 {x , y+ylen, z+zlen}, -- v3
6594 {x , y+ylen, z }, -- v4
6595 {x+xlen, y+ylen, z+zlen}, -- v5
6596 {x+xlen, y+ylen, z }, -- v6
6597 {x+xlen, y , z+zlen}, -- v7
6598 {x+xlen, y , z } -- v8
6599 }
6600
6601 -- test if mousex,mousey is inside a visible face of activebox
6602 local face = FindFace(mousex, mousey, activebox)
6603
6604 if #face > 0 then
6605 -- determine which active cell contains mousex,mousey
6606 local cx, cy, cz = FindActiveCell(mousex, mousey, face)
6607 return cx..","..cy..","..cz
6608 else
6609 return ""
6610 end
6611end
6612
6613--------------------------------------------------------------------------------
6614
6615function StartDrawing(mousex, mousey)
6616 local oldcell = activecell
6617 activecell = InsideActiveCell(mousex, mousey)
6618 if #activecell > 0 then
6619 RememberCurrentState()
6620 -- toggle the state of the clicked cell
6621 local x, y, z = split(activecell, ",")
6622 local mid = N//2
6623 x = tonumber(x) + mid
6624 y = tonumber(y) + mid
6625 z = tonumber(z) + mid
6626 local pos = x + N * (y + N * z)
6627 if grid1[pos] then
6628 -- death
6629 grid1[pos] = nil
6630 popcount = popcount - 1
6631 dirty = true
6632 update_grid = true
6633 minimal_live_bounds = false
6634 drawstate = 0
6635 else
6636 -- birth
6637 SetLiveCell(x, y, z) -- sets dirty = true
6638 drawstate = 1
6639 end
6640 Refresh()
6641 prevactive = activecell
6642 return true
6643 else
6644 if activecell ~= oldcell then Refresh() end
6645 return false
6646 end
6647end
6648
6649--------------------------------------------------------------------------------
6650
6651function StartSelecting(mousex, mousey)
6652 local oldcell = activecell
6653 activecell = InsideActiveCell(mousex, mousey)
6654 if #activecell > 0 then
6655 RememberCurrentState()
6656 -- toggle the selection state of the clicked cell
6657 local x, y, z = split(activecell, ",")
6658 local mid = N//2
6659 x = tonumber(x) + mid
6660 y = tonumber(y) + mid
6661 z = tonumber(z) + mid
6662 local pos = x + N * (y + N * z)
6663 if selected[pos] then
6664 -- deselect
6665 selected[pos] = nil
6666 selcount = selcount - 1
6667 minimal_sel_bounds = false
6668 selstate = false
6669 else
6670 selected[pos] = true
6671 selcount = selcount + 1
6672 UpdateSelectionBoundary(x, y, z)
6673 selstate = true
6674 end
6675 Refresh()
6676 prevactive = activecell
6677 return true
6678 else
6679 if activecell ~= oldcell then Refresh() end
6680 return false
6681 end
6682end
6683
6684--------------------------------------------------------------------------------
6685
6686function SetLine(x1, y1, z1, x2, y2, z2, setfunction, state)
6687 -- draw/erase/select/deselect a line of cells from x1,y1,z1 to x2,y2,z2
6688 -- using a 3D version of Bresenham's algorithm
6689 -- (note that x1,y1,z1 has already been set and x2,y2,z2 is a different cell)
6690 local dx = x2 - x1
6691 local dy = y2 - y1
6692 local dz = z2 - z1
6693 local xinc = 1; if dx < 0 then xinc = -1 end
6694 local yinc = 1; if dy < 0 then yinc = -1 end
6695 local zinc = 1; if dz < 0 then zinc = -1 end
6696 local ax = abs(dx)
6697 local ay = abs(dy)
6698 local az = abs(dz)
6699 local dx2 = ax * 2
6700 local dy2 = ay * 2
6701 local dz2 = az * 2
6702
6703 if ax >= ay and ax >= az then
6704 local e1 = dy2 - ax
6705 local e2 = dz2 - ax
6706 while x1 ~= x2 do
6707 setfunction(x1, y1, z1, state)
6708 if e1 > 0 then
6709 y1 = y1 + yinc
6710 e1 = e1 - dx2
6711 end
6712 if e2 > 0 then
6713 z1 = z1 + zinc
6714 e2 = e2 - dx2
6715 end
6716 e1 = e1 + dy2
6717 e2 = e2 + dz2
6718 x1 = x1 + xinc
6719 end
6720 elseif ay >= ax and ay >= az then
6721 local e1 = dx2 - ay
6722 local e2 = dz2 - ay
6723 while y1 ~= y2 do
6724 setfunction(x1, y1, z1, state)
6725 if e1 > 0 then
6726 x1 = x1 + xinc
6727 e1 = e1 - dy2
6728 end
6729 if e2 > 0 then
6730 z1 = z1 + zinc
6731 e2 = e2 - dy2
6732 end
6733 e1 = e1 + dx2
6734 e2 = e2 + dz2
6735 y1 = y1 + yinc
6736 end
6737 else
6738 local e1 = dy2 - az
6739 local e2 = dx2 - az
6740 while z1 ~= z2 do
6741 setfunction(x1, y1, z1, state)
6742 if e1 > 0 then
6743 y1 = y1 + yinc
6744 e1 = e1 - dz2
6745 end
6746 if e2 > 0 then
6747 x1 = x1 + xinc
6748 e2 = e2 - dz2
6749 end
6750 e1 = e1 + dy2
6751 e2 = e2 + dx2
6752 z1 = z1 + zinc
6753 end
6754 end
6755 setfunction(x1, y1, z1, state)
6756end
6757
6758--------------------------------------------------------------------------------
6759
6760function DrawCells(mousex, mousey)
6761 -- draw/erase cells in active plane
6762 local oldcell = activecell
6763 activecell = InsideActiveCell(mousex, mousey)
6764 if #activecell > 0 and activecell ~= prevactive then
6765 -- mouse has moved to a different cell
6766 local mid = N//2
6767 local x, y, z = split(activecell, ",")
6768 x = tonumber(x) + mid
6769 y = tonumber(y) + mid
6770 z = tonumber(z) + mid
6771 -- draw/erase a line of cells from prevactive to activecell
6772 local prevx, prevy, prevz = split(prevactive, ",")
6773 prevx = tonumber(prevx) + mid
6774 prevy = tonumber(prevy) + mid
6775 prevz = tonumber(prevz) + mid
6776 SetLine(prevx, prevy, prevz, x, y, z, SetCellState, drawstate)
6777 Refresh()
6778 prevactive = activecell
6779 else
6780 if activecell ~= oldcell then Refresh() end
6781 end
6782end
6783
6784--------------------------------------------------------------------------------
6785
6786function SelectCells(mousex, mousey)
6787 -- select/deselect cells in active plane
6788 local oldcell = activecell
6789 activecell = InsideActiveCell(mousex, mousey)
6790 if #activecell > 0 and activecell ~= prevactive then
6791 -- mouse has moved to a different cell
6792 local x, y, z = split(activecell, ",")
6793 local mid = N//2
6794 x = tonumber(x) + mid
6795 y = tonumber(y) + mid
6796 z = tonumber(z) + mid
6797 -- select/deselect a line of cells from prevactive to activecell
6798 local prevx, prevy, prevz = split(prevactive, ",")
6799 prevx = tonumber(prevx) + mid
6800 prevy = tonumber(prevy) + mid
6801 prevz = tonumber(prevz) + mid
6802 SetLine(prevx, prevy, prevz, x, y, z, SetSelection, selstate)
6803 Refresh()
6804 prevactive = activecell
6805 else
6806 if activecell ~= oldcell then Refresh() end
6807 end
6808end
6809
6810--------------------------------------------------------------------------------
6811
6812function StartDraggingPaste(mousex, mousey)
6813 -- test if mouse click is in a paste cell
6814 local NN = N*N
6815 for k,_ in pairs(pastepatt) do
6816 local px, py = GetMidPoint(k%N, (k//N)%N, k//NN)
6817 if abs(px - mousex) < HALFCELL and
6818 abs(py - mousey) < HALFCELL then
6819 -- create a box enclosing all paste cells using same vertex order as CreateCube
6820 local x = minpastex * CELLSIZE - MIDGRID
6821 local y = minpastey * CELLSIZE - MIDGRID
6822 local z = minpastez * CELLSIZE - MIDGRID
6823 local xlen = (maxpastex - minpastex + 1) * CELLSIZE
6824 local ylen = (maxpastey - minpastey + 1) * CELLSIZE
6825 local zlen = (maxpastez - minpastez + 1) * CELLSIZE
6826 local pastebox = {
6827 {x , y , z+zlen}, -- v1
6828 {x , y , z }, -- v2
6829 {x , y+ylen, z+zlen}, -- v3
6830 {x , y+ylen, z }, -- v4
6831 {x+xlen, y+ylen, z+zlen}, -- v5
6832 {x+xlen, y+ylen, z }, -- v6
6833 {x+xlen, y , z+zlen}, -- v7
6834 {x+xlen, y , z } -- v8
6835 }
6836 -- return the face containing mousex,mousey
6837 return FindFace(mousex, mousey, pastebox)
6838 end
6839 end
6840 return ""
6841end
6842
6843--------------------------------------------------------------------------------
6844
6845function MovePastePattern(deltax, deltay, deltaz)
6846 -- move the paste pattern by the given amounts, if possible
6847 if minpastex + deltax < 0 then deltax = -minpastex end
6848 if minpastey + deltay < 0 then deltay = -minpastey end
6849 if minpastez + deltaz < 0 then deltaz = -minpastez end
6850 if maxpastex + deltax >= N then deltax = N-1 - maxpastex end
6851 if maxpastey + deltay >= N then deltay = N-1 - maxpastey end
6852 if maxpastez + deltaz >= N then deltaz = N-1 - maxpastez end
6853
6854 if deltax == 0 and deltay == 0 and deltaz == 0 then return end
6855
6856 minpastex = minpastex + deltax
6857 minpastey = minpastey + deltay
6858 minpastez = minpastez + deltaz
6859 maxpastex = maxpastex + deltax
6860 maxpastey = maxpastey + deltay
6861 maxpastez = maxpastez + deltaz
6862
6863 local pcells = GetPasteCells()
6864 local mid = N//2
6865 pastepatt = {}
6866 for _, xyz in ipairs(pcells) do
6867 local x = xyz[1] + mid + deltax
6868 local y = xyz[2] + mid + deltay
6869 local z = xyz[3] + mid + deltaz
6870 pastepatt[ x + N * (y + N * z) ] = true
6871 end
6872
6873 Refresh()
6874end
6875
6876--------------------------------------------------------------------------------
6877
6878function DragPaste(mousex, mousey, prevx, prevy, face)
6879 -- create a large temporary active plane parallel to given face
6880 local oldN = N
6881 local oldplane = activeplane
6882 local oldpos = activepos
6883 SetTemporaryGridSize(N*3)
6884 if face == "F" or face == "B" then
6885 -- mouse in front/back face
6886 SetActivePlane("XY", 0)
6887 elseif face == "T" or face == "U" then
6888 -- mouse in top/bottom face
6889 SetActivePlane("XZ", 0)
6890 else
6891 -- mouse in left/right face
6892 SetActivePlane("YZ", 0)
6893 end
6894
6895 -- find the cell locations of mousex,mousey and prevx,prevy in the temporary plane
6896 local oldcell = InsideActiveCell(prevx, prevy)
6897 local newcell = InsideActiveCell(mousex, mousey)
6898
6899 -- restore the original active plane
6900 SetTemporaryGridSize(oldN)
6901 SetActivePlane(oldplane, oldpos)
6902
6903 -- check if mouse stayed in same cell, or moved outside temporary plane
6904 if oldcell == newcell or #oldcell == 0 or #newcell == 0 then
6905 return
6906 end
6907
6908 -- calculate how many cells the mouse has moved
6909 local oldx, oldy, oldz = split(oldcell,",")
6910 local newx, newy, newz = split(newcell,",")
6911 local deltax = tonumber(newx) - tonumber(oldx)
6912 local deltay = tonumber(newy) - tonumber(oldy)
6913 local deltaz = tonumber(newz) - tonumber(oldz)
6914
6915 MovePastePattern(deltax, deltay, deltaz)
6916end
6917
6918--------------------------------------------------------------------------------
6919
6920local save_cells -- for restoring live cells under a moving selection
6921local selxyz -- store x,y,z positions of selected cells
6922local livexyz -- store x,y,z positions of selected live cells
6923
6924function StartDraggingSelection(mousex, mousey)
6925 -- test if mouse click is in a selected cell
6926 MinimizeSelectionBoundary()
6927 local NN = N*N
6928 for k,_ in pairs(selected) do
6929 local px, py = GetMidPoint(k%N, (k//N)%N, k//NN)
6930 if abs(px - mousex) < HALFCELL and
6931 abs(py - mousey) < HALFCELL then
6932 -- create a box enclosing all selected cells using same vertex order as CreateCube
6933 local x = minselx * CELLSIZE - MIDGRID
6934 local y = minsely * CELLSIZE - MIDGRID
6935 local z = minselz * CELLSIZE - MIDGRID
6936 local xlen = (maxselx - minselx + 1) * CELLSIZE
6937 local ylen = (maxsely - minsely + 1) * CELLSIZE
6938 local zlen = (maxselz - minselz + 1) * CELLSIZE
6939 local selbox = {
6940 {x , y , z+zlen}, -- v1
6941 {x , y , z }, -- v2
6942 {x , y+ylen, z+zlen}, -- v3
6943 {x , y+ylen, z }, -- v4
6944 {x+xlen, y+ylen, z+zlen}, -- v5
6945 {x+xlen, y+ylen, z }, -- v6
6946 {x+xlen, y , z+zlen}, -- v7
6947 {x+xlen, y , z } -- v8
6948 }
6949 local face = FindFace(mousex, mousey, selbox)
6950 if #face > 0 then
6951 -- initialize save_cells, selxyz and livexyz for use in MoveSelection
6952 save_cells = {}
6953 selxyz = {}
6954 livexyz = {}
6955 for k1,_ in pairs(selected) do
6956 -- selected[k1] is a selected cell
6957 local x1 = k1 % N
6958 local y1 = (k1 // N) % N
6959 local z1 = k1 // NN
6960 selxyz[#selxyz+1] = {x1, y1, z1}
6961 if grid1[k1] then
6962 -- selected cell is a live cell
6963 livexyz[#livexyz+1] = {x1, y1, z1}
6964 end
6965 end
6966 RememberCurrentState()
6967 end
6968 return face
6969 end
6970 end
6971 return ""
6972end
6973
6974--------------------------------------------------------------------------------
6975
6976function MoveSelection(deltax, deltay, deltaz)
6977 -- move all selected cells by the given amounts, if possible
6978 if minselx + deltax < 0 then deltax = -minselx end
6979 if minsely + deltay < 0 then deltay = -minsely end
6980 if minselz + deltaz < 0 then deltaz = -minselz end
6981 if maxselx + deltax >= N then deltax = N-1 - maxselx end
6982 if maxsely + deltay >= N then deltay = N-1 - maxsely end
6983 if maxselz + deltaz >= N then deltaz = N-1 - maxselz end
6984
6985 if deltax == 0 and deltay == 0 and deltaz == 0 then return end
6986
6987 -- RememberCurrentState was called in StartDraggingSelection
6988
6989 minselx = minselx + deltax
6990 minsely = minsely + deltay
6991 minselz = minselz + deltaz
6992 maxselx = maxselx + deltax
6993 maxsely = maxsely + deltay
6994 maxselz = maxselz + deltaz
6995
6996 -- kill all live cells in the current selection
6997 local savepop = popcount
6998 local NN = N*N
6999 for k,_ in pairs(selected) do
7000 if grid1[k] then
7001 grid1[k] = nil
7002 popcount = popcount - 1
7003 dirty = true
7004 update_grid = true
7005 minimal_live_bounds = false
7006 end
7007 end
7008
7009 if popcount == 0 and selcount == savepop then
7010 -- all live cells (and only those cells) were selected
7011 -- so we can handle this common case much faster by simply
7012 -- putting the selected cells in their new positions
7013 selected = {}
7014 for i, xyz in ipairs(livexyz) do
7015 local x = xyz[1] + deltax
7016 local y = xyz[2] + deltay
7017 local z = xyz[3] + deltaz
7018 local k = x + N * (y + N * z)
7019 selected[k] = true
7020 grid1[k] = 1
7021 livexyz[i] = {x, y, z}
7022 end
7023 popcount = savepop
7024
7025 -- new live boundary is same as new selection boundary
7026 -- (and minimal because StartDraggingSelection called MinimizeSelectionBoundary)
7027 minx = minselx
7028 miny = minsely
7029 minz = minselz
7030 maxx = maxselx
7031 maxy = maxsely
7032 maxz = maxselz
7033 minimal_live_bounds = true
7034 else
7035 -- avoid modifying any live cells under the moving selection
7036 -- by restoring the live cells in save_cells (if any)
7037 for k,_ in pairs(save_cells) do
7038 if not grid1[k] then
7039 grid1[k] = 1
7040 popcount = popcount + 1
7041 dirty = true
7042 update_grid = true
7043 -- boundary might expand
7044 local x = k % N
7045 local y = (k // N) % N
7046 local z = k // NN
7047 if x < minx then minx = x end
7048 if y < miny then miny = y end
7049 if z < minz then minz = z end
7050 if x > maxx then maxx = x end
7051 if y > maxy then maxy = y end
7052 if z > maxz then maxz = z end
7053 end
7054 end
7055
7056 -- move the selected cells to their new positions
7057 -- and save any live cells in the new selection in save_cells
7058 save_cells = {}
7059 selected = {}
7060 for i, xyz in ipairs(selxyz) do
7061 local x = xyz[1] + deltax
7062 local y = xyz[2] + deltay
7063 local z = xyz[3] + deltaz
7064 local k = x + N * (y + N * z)
7065 selected[k] = true
7066 selxyz[i] = {x, y, z}
7067 if grid1[k] then save_cells[k] = true end
7068 end
7069
7070 -- put live cells saved in livexyz into their new positions
7071 for i, xyz in ipairs(livexyz) do
7072 local x = xyz[1] + deltax
7073 local y = xyz[2] + deltay
7074 local z = xyz[3] + deltaz
7075 local k = x + N * (y + N * z)
7076 if not grid1[k] then
7077 grid1[k] = 1
7078 popcount = popcount + 1
7079 dirty = true
7080 update_grid = true
7081 -- boundary might expand
7082 if x < minx then minx = x end
7083 if y < miny then miny = y end
7084 if z < minz then minz = z end
7085 if x > maxx then maxx = x end
7086 if y > maxy then maxy = y end
7087 if z > maxz then maxz = z end
7088 end
7089 livexyz[i] = {x, y, z}
7090 end
7091 end
7092
7093 Refresh()
7094end
7095
7096--------------------------------------------------------------------------------
7097
7098function DragSelection(mousex, mousey, prevx, prevy, face)
7099 -- create a large temporary active plane parallel to given face
7100 local oldN = N
7101 local oldplane = activeplane
7102 local oldpos = activepos
7103 SetTemporaryGridSize(N*3)
7104 if face == "F" or face == "B" then
7105 -- mouse in front/back face
7106 SetActivePlane("XY", 0)
7107 elseif face == "T" or face == "U" then
7108 -- mouse in top/bottom face
7109 SetActivePlane("XZ", 0)
7110 else
7111 -- mouse in left/right face
7112 SetActivePlane("YZ", 0)
7113 end
7114
7115 -- find the cell locations of mousex,mousey and prevx,prevy in the temporary plane
7116 local oldcell = InsideActiveCell(prevx, prevy)
7117 local newcell = InsideActiveCell(mousex, mousey)
7118
7119 -- restore the original active plane
7120 SetTemporaryGridSize(oldN)
7121 SetActivePlane(oldplane, oldpos)
7122
7123 -- check if mouse stayed in same cell, or moved outside temporary plane
7124 if oldcell == newcell or #oldcell == 0 or #newcell == 0 then
7125 return
7126 end
7127
7128 -- calculate how many cells the mouse has moved
7129 local oldx, oldy, oldz = split(oldcell,",")
7130 local newx, newy, newz = split(newcell,",")
7131 local deltax = tonumber(newx) - tonumber(oldx)
7132 local deltay = tonumber(newy) - tonumber(oldy)
7133 local deltaz = tonumber(newz) - tonumber(oldz)
7134
7135 MoveSelection(deltax, deltay, deltaz)
7136end
7137
7138--------------------------------------------------------------------------------
7139
7140function StopDraggingSelection()
7141 save_cells = {}
7142 selxyz = {}
7143 livexyz = {}
7144end
7145
7146--------------------------------------------------------------------------------
7147
7148function StartDraggingPlane(mousex, mousey)
7149 local oldcell = activecell
7150 activecell = InsideActiveCell(mousex, mousey)
7151 if activecell ~= oldcell then Refresh() end
7152 if #activecell > 0 then
7153 RememberCurrentState()
7154 return true
7155 else
7156 return false
7157 end
7158end
7159
7160--------------------------------------------------------------------------------
7161
7162function DragActivePlane(mousex, mousey, prevx, prevy)
7163 -- create a box enclosing all active cells using same vertex order as CreateCube
7164 local x = minactivex * CELLSIZE - MIDGRID
7165 local y = minactivey * CELLSIZE - MIDGRID
7166 local z = minactivez * CELLSIZE - MIDGRID
7167 local xlen = (maxactivex - minactivex + 1) * CELLSIZE
7168 local ylen = (maxactivey - minactivey + 1) * CELLSIZE
7169 local zlen = (maxactivez - minactivez + 1) * CELLSIZE
7170 local activebox = {
7171 {x , y , z+zlen}, -- v1
7172 {x , y , z }, -- v2
7173 {x , y+ylen, z+zlen}, -- v3
7174 {x , y+ylen, z }, -- v4
7175 {x+xlen, y+ylen, z+zlen}, -- v5
7176 {x+xlen, y+ylen, z }, -- v6
7177 {x+xlen, y , z+zlen}, -- v7
7178 {x+xlen, y , z } -- v8
7179 }
7180
7181 -- create activebox's rotated vertices
7182 for i = 1, 8 do
7183 rotx[i], roty[i], rotz[i] = TransformPoint(activebox[i])
7184 end
7185
7186 -- create a temporary active plane perpendicular to the nearest thin face
7187 -- and 3 times larger (to ensure all of the real active plane is enclosed)
7188 local oldN = N
7189 local oldplane = activeplane
7190 local oldpos = activepos
7191 SetTemporaryGridSize(N*3)
7192 if activeplane == "XY" then
7193 if (rotz[5] <= rotz[1] and rotz[5] <= rotz[3] and rotz[7] <= rotz[3]) or
7194 (rotz[4] <= rotz[8] and rotz[4] <= rotz[6] and rotz[2] <= rotz[6]) then
7195 -- right/left face is nearest
7196 SetActivePlane("YZ", 0)
7197 else
7198 -- top/bottom face is nearest
7199 SetActivePlane("XZ", 0)
7200 end
7201 elseif activeplane == "YZ" then
7202 if (rotz[5] <= rotz[8] and rotz[5] <= rotz[6] and rotz[7] <= rotz[6]) or
7203 (rotz[4] <= rotz[1] and rotz[4] <= rotz[3] and rotz[2] <= rotz[3]) then
7204 -- front/back face is nearest
7205 SetActivePlane("XY", 0)
7206 else
7207 -- top/bottom face is nearest
7208 SetActivePlane("XZ", 0)
7209 end
7210 else -- activeplane == "XZ"
7211 if (rotz[5] <= rotz[4] and rotz[5] <= rotz[6] and rotz[3] <= rotz[6]) or
7212 (rotz[4] <= rotz[5] and rotz[4] <= rotz[3] and rotz[6] <= rotz[3]) then
7213 -- front/back face is nearest
7214 SetActivePlane("XY", 0)
7215 else
7216 -- left/right face is nearest
7217 SetActivePlane("YZ", 0)
7218 end
7219 end
7220
7221 -- find the cell locations of mousex,mousey and prevx,prevy in the temporary plane
7222 local oldcell = InsideActiveCell(prevx, prevy)
7223 local newcell = InsideActiveCell(mousex, mousey)
7224
7225 -- restore the original active plane
7226 SetTemporaryGridSize(oldN)
7227 SetActivePlane(oldplane, oldpos)
7228
7229 -- check if mouse stayed in same cell, or moved outside temporary plane
7230 if oldcell == newcell or #oldcell == 0 or #newcell == 0 then
7231 return
7232 end
7233
7234 -- calculate how many cells the mouse has moved
7235 local oldx, oldy, oldz = split(oldcell,",")
7236 local newx, newy, newz = split(newcell,",")
7237 local deltax = tonumber(newx) - tonumber(oldx)
7238 local deltay = tonumber(newy) - tonumber(oldy)
7239 local deltaz = tonumber(newz) - tonumber(oldz)
7240
7241 -- move the active plane by the appropriate delta but don't call Refresh yet
7242 if activeplane == "XY" then
7243 MoveActivePlane(activepos + deltaz, false)
7244 elseif activeplane == "YZ" then
7245 MoveActivePlane(activepos + deltax, false)
7246 else -- activeplane == "XZ"
7247 MoveActivePlane(activepos + deltay, false)
7248 end
7249
7250 activecell = InsideActiveCell(mousex, mousey)
7251 Refresh()
7252end
7253
7254--------------------------------------------------------------------------------
7255
7256function MiddlePaste()
7257 -- move paste pattern to middle of grid
7258 if pastecount > 0 then
7259 -- calculate the delta amounts needed to move paste pattern to middle of grid
7260 local deltax = (N - (maxpastex - minpastex)) // 2 - minpastex
7261 local deltay = (N - (maxpastey - minpastey)) // 2 - minpastey
7262 local deltaz = (N - (maxpastez - minpastez)) // 2 - minpastez
7263 if deltax == 0 and deltay == 0 and deltaz == 0 then return end
7264
7265 RememberCurrentState()
7266 local pcells = {}
7267 local NN = N*N
7268 for k,_ in pairs(pastepatt) do
7269 pcells[#pcells+1] = {k % N, (k // N) % N, k // NN}
7270 end
7271 pastepatt = {}
7272 for _,xyz in ipairs(pcells) do
7273 local x = xyz[1] + deltax
7274 local y = xyz[2] + deltay
7275 local z = xyz[3] + deltaz
7276 pastepatt[x + N * (y + N * z)] = true
7277 end
7278
7279 -- update the paste boundary
7280 minpastex = minpastex + deltax
7281 minpastey = minpastey + deltay
7282 minpastez = minpastez + deltaz
7283 maxpastex = maxpastex + deltax
7284 maxpastey = maxpastey + deltay
7285 maxpastez = maxpastez + deltaz
7286
7287 CheckIfGenerating()
7288 Refresh()
7289 end
7290end
7291
7292--------------------------------------------------------------------------------
7293
7294function MiddleSelection()
7295 -- move selection to middle of grid
7296 if selcount > 0 then
7297 MinimizeSelectionBoundary()
7298 -- calculate the delta amounts needed to move selection to middle of grid
7299 local deltax = (N - (maxselx - minselx)) // 2 - minselx
7300 local deltay = (N - (maxsely - minsely)) // 2 - minsely
7301 local deltaz = (N - (maxselz - minselz)) // 2 - minselz
7302 if deltax == 0 and deltay == 0 and deltaz == 0 then return end
7303
7304 RememberCurrentState()
7305 -- only set dirty = true if live cells are selected
7306 local selcells = {}
7307 local livecells = {} -- for live cells in selection
7308 local NN = N*N
7309 for k,_ in pairs(selected) do
7310 local x = k % N
7311 local y = (k // N) % N
7312 local z = k // NN
7313 selcells[#selcells+1] = {x, y, z}
7314 if grid1[k] then
7315 grid1[k] = nil
7316 popcount = popcount - 1
7317 dirty = true
7318 update_grid = true
7319 minimal_live_bounds = false
7320 livecells[#livecells+1] = {x, y, z}
7321 end
7322 end
7323 selected = {}
7324 for _,xyz in ipairs(selcells) do
7325 local x = xyz[1] + deltax
7326 local y = xyz[2] + deltay
7327 local z = xyz[3] + deltaz
7328 selected[x + N * (y + N * z)] = true
7329 end
7330 -- move live cells that were selected
7331 for _,xyz in ipairs(livecells) do
7332 local x = xyz[1] + deltax
7333 local y = xyz[2] + deltay
7334 local z = xyz[3] + deltaz
7335 local k = x + N * (y + N * z)
7336 if not grid1[k] then
7337 grid1[k] = 1
7338 popcount = popcount + 1
7339 -- dirty set to true above
7340 -- boundary might expand
7341 if x < minx then minx = x end
7342 if y < miny then miny = y end
7343 if z < minz then minz = z end
7344 if x > maxx then maxx = x end
7345 if y > maxy then maxy = y end
7346 if z > maxz then maxz = z end
7347 end
7348 end
7349
7350 -- update the selection boundary
7351 minselx = minselx + deltax
7352 minsely = minsely + deltay
7353 minselz = minselz + deltaz
7354 maxselx = maxselx + deltax
7355 maxsely = maxsely + deltay
7356 maxselz = maxselz + deltaz
7357 -- MinimizeSelectionBoundary set minimal_sel_bounds to true
7358
7359 CheckIfGenerating()
7360 Refresh()
7361 end
7362end
7363
7364--------------------------------------------------------------------------------
7365
7366function MiddlePattern()
7367 -- move pattern to middle of grid
7368 if popcount > 0 then
7369 MinimizeLiveBoundary()
7370 -- calculate the delta amounts needed to move pattern to middle of grid
7371 local deltax = (N - (maxx - minx)) // 2 - minx
7372 local deltay = (N - (maxy - miny)) // 2 - miny
7373 local deltaz = (N - (maxz - minz)) // 2 - minz
7374 if deltax == 0 and deltay == 0 and deltaz == 0 then return end
7375
7376 RememberCurrentState()
7377 dirty = true
7378 update_grid = true
7379 local livecells = {}
7380 local NN = N*N
7381 for k,_ in pairs(grid1) do
7382 livecells[#livecells+1] = {k % N, (k // N) % N, k // NN}
7383 end
7384 grid1 = {}
7385 for _,xyz in ipairs(livecells) do
7386 local x = xyz[1] + deltax
7387 local y = xyz[2] + deltay
7388 local z = xyz[3] + deltaz
7389 grid1[x + N * (y + N * z)] = 1
7390 end
7391
7392 -- update the live cell boundary
7393 minx = minx + deltax
7394 miny = miny + deltay
7395 minz = minz + deltaz
7396 maxx = maxx + deltax
7397 maxy = maxy + deltay
7398 maxz = maxz + deltaz
7399 -- MinimizeLiveBoundary set minimal_live_bounds to true
7400
7401 CheckIfGenerating()
7402 Refresh()
7403 end
7404end
7405
7406--------------------------------------------------------------------------------
7407
7408-- use this flag in EraseLiveCells and SelectLiveCells to call
7409-- RememberCurrentState just before the first change (if any)
7410local firstchange = false
7411
7412function EraseLiveCells(mousex, mousey, firstcall)
7413 -- erase all live cells whose projected mid points are close to mousex,mousey
7414 if popcount > 0 then
7415 if firstcall then firstchange = false end
7416 local changes = 0
7417 local NN = N*N
7418 for k,_ in pairs(grid1) do
7419 -- grid1[k] is a live cell
7420 local x = k % N
7421 local y = (k // N) % N
7422 local z = k // NN
7423 local px, py = GetMidPoint(x, y, z)
7424 if abs(px - mousex) < HALFCELL and
7425 abs(py - mousey) < HALFCELL then
7426 if not firstchange then
7427 RememberCurrentState()
7428 firstchange = true
7429 end
7430 grid1[k] = nil
7431 popcount = popcount - 1
7432 dirty = true
7433 update_grid = true
7434 minimal_live_bounds = false
7435 changes = changes + 1
7436 end
7437 end
7438 if changes > 0 then Refresh() end
7439 end
7440end
7441
7442--------------------------------------------------------------------------------
7443
7444function SelectLiveCells(mousex, mousey, firstcall)
7445 -- select all live cells whose projected mid points are close to mousex,mousey
7446 if popcount > 0 then
7447 if firstcall then firstchange = false end
7448 local changes = 0
7449 local NN = N*N
7450 for k,_ in pairs(grid1) do
7451 -- grid1[k] is a live cell
7452 local x = k % N
7453 local y = (k // N) % N
7454 local z = k // NN
7455 local px, py = GetMidPoint(x, y, z)
7456 if abs(px - mousex) < HALFCELL and
7457 abs(py - mousey) < HALFCELL then
7458 if not selected[k] then
7459 if not firstchange then
7460 RememberCurrentState()
7461 firstchange = true
7462 end
7463 selected[k] = true
7464 selcount = selcount + 1
7465 UpdateSelectionBoundary(x, y, z)
7466 changes = changes + 1
7467 end
7468 end
7469 end
7470 if changes > 0 then Refresh() end
7471 end
7472end
7473
7474--------------------------------------------------------------------------------
7475
7476function CreateOverlay()
7477 -- overlay covers entire viewport (more if viewport is too small)
7478 viewwd, viewht = g.getview(g.getlayer())
7479 ovwd, ovht = viewwd, viewht
7480 if ovwd < minwd then ovwd = minwd end
7481 if ovht < minht then ovht = minht end
7482 midx = int(ovwd/2)
7483 midy = int(ovht/2 + toolbarht/2)
7484 ov("create "..ovwd.." "..ovht)
7485 ov("cursor "..currcursor)
7486
7487 ov("font 11 default-bold") -- font for info text
7488
7489 -- set parameters for menu bar and tool bar buttons
7490 op.buttonht = buttonht
7491 op.textgap = 8 -- gap between edge of button and its label
7492 op.textfont = "font 10 default-bold" -- font for button labels
7493 op.menufont = "font 11 default-bold" -- font for menu and item labels
7494 op.textshadowx = 2
7495 op.textshadowy = 2
7496 if g.os() == "Linux" then
7497 op.textfont = "font 10 default"
7498 op.menufont = "font 11 default"
7499 end
7500end
7501
7502--------------------------------------------------------------------------------
7503
7504function CreateMenuBar()
7505 -- create the menu bar and add some menus
7506 -- (note that changes to the order of menus or their items will require
7507 -- changes to DrawMenuBar and EnableControls)
7508 mbar = op.menubar()
7509 mbar.addmenu("File")
7510 mbar.addmenu("Edit")
7511 mbar.addmenu("Control")
7512 mbar.addmenu("View")
7513
7514 -- add items to File menu
7515 mbar.additem(1, "New Pattern", NewPattern)
7516 mbar.additem(1, "Random Pattern...", RandomPattern)
7517 mbar.additem(1, "Open Pattern...", OpenPattern)
7518 mbar.additem(1, "Open Clipboard", OpenClipboard)
7519 mbar.additem(1, "Save Pattern...", SavePattern)
7520 mbar.additem(1, "---", nil)
7521 mbar.additem(1, "Run Script...", RunScript)
7522 mbar.additem(1, "Run Clipboard", RunClipboard)
7523 mbar.additem(1, "Set Startup Script...", SetStartupScript)
7524 mbar.additem(1, "---", nil)
7525 mbar.additem(1, "Exit 3D.lua", ExitScript)
7526
7527 -- add items to Edit menu
7528 mbar.additem(2, "Undo", Undo)
7529 mbar.additem(2, "Redo", Redo)
7530 mbar.additem(2, "---", nil)
7531 mbar.additem(2, "Cut", CutSelection)
7532 mbar.additem(2, "Copy", CopySelection)
7533 mbar.additem(2, "Paste", Paste)
7534 mbar.additem(2, "Cancel Paste", CancelPaste)
7535 mbar.additem(2, "Clear", ClearSelection)
7536 mbar.additem(2, "Clear Outside", ClearOutside)
7537 mbar.additem(2, "---", nil)
7538 mbar.additem(2, "Select All", SelectAll)
7539 mbar.additem(2, "Cancel Selection", CancelSelection)
7540 mbar.additem(2, "---", nil)
7541 mbar.additem(2, "Middle Pattern", MiddlePattern)
7542 mbar.additem(2, "Middle Selection", MiddleSelection)
7543 mbar.additem(2, "Middle Paste", MiddlePaste)
7544
7545 -- add items to Control menu
7546 mbar.additem(3, "Start Generating", StartStop)
7547 mbar.additem(3, "Next Generation", Step1)
7548 mbar.additem(3, "Next Step", NextStep)
7549 mbar.additem(3, "Reset", Reset)
7550 mbar.additem(3, "---", nil)
7551 mbar.additem(3, "Set Rule...", ChangeRule)
7552
7553 -- add items to View menu
7554 mbar.additem(4, "Initial View", InitialView)
7555 mbar.additem(4, "Fit Grid", FitGrid)
7556 mbar.additem(4, "Set Grid Size...", SetGridSize)
7557 mbar.additem(4, "---", nil)
7558 mbar.additem(4, "Cubes", SetCellType, {"cube"})
7559 mbar.additem(4, "Spheres", SetCellType, {"sphere"})
7560 mbar.additem(4, "Points", SetCellType, {"point"})
7561 mbar.additem(4, "---", nil)
7562 mbar.additem(4, "Show Axes", ToggleAxes)
7563 mbar.additem(4, "Show Lattice Lines", ToggleLines)
7564 mbar.additem(4, "Use Depth Shading", ToggleDepthShading)
7565 mbar.additem(4, "Show History", ToggleShowHistory)
7566 mbar.additem(4, "Fade History", ToggleFadeHistory)
7567 mbar.additem(4, "---", nil)
7568 mbar.additem(4, "Help", ShowHelp)
7569end
7570
7571--------------------------------------------------------------------------------
7572
7573function CreateToolBar()
7574 -- create tool bar buttons
7575 ssbutton = op.button("Start", StartStop)
7576 s1button = op.button("+1", Step1)
7577 resetbutton = op.button("Reset", Reset)
7578 fitbutton = op.button("Fit", FitGrid)
7579 undobutton = op.button("Undo", Undo)
7580 redobutton = op.button("Redo", Redo)
7581 helpbutton = op.button("?", ShowHelp)
7582 exitbutton = op.button("X", ExitScript)
7583
7584 -- create radio buttons and slider (don't shadow text)
7585 op.textshadowx = 0
7586 op.textshadowy = 0
7587
7588 drawbox = op.radiobutton("Draw", op.black, DrawMode)
7589 selectbox = op.radiobutton("Select", op.black, SelectMode)
7590 movebox = op.radiobutton("Move", op.black, MoveMode)
7591
7592 -- create a slider for adjusting stepsize
7593 stepslider = op.slider("", op.black, 100, 1, 100, StepChange)
7594end
7595
7596--------------------------------------------------------------------------------
7597
7598function CreatePopUpMenus()
7599 -- text in pop-up menus is shadowed
7600 op.textshadowx = 2
7601 op.textshadowy = 2
7602
7603 -- create a pop-up menu for paste actions
7604 pastemenu = op.popupmenu()
7605 pastemenu.additem("Paste OR", PasteOR)
7606 pastemenu.additem("Paste XOR", PasteXOR)
7607 pastemenu.additem("---", nil)
7608 pastemenu.additem("Flip X Coords", FlipPasteX)
7609 pastemenu.additem("Flip Y Coords", FlipPasteY)
7610 pastemenu.additem("Flip Z Coords", FlipPasteZ)
7611 pastemenu.additem("---", nil)
7612 pastemenu.additem("Rotate X Axis", RotatePasteX)
7613 pastemenu.additem("Rotate Y Axis", RotatePasteY)
7614 pastemenu.additem("Rotate Z Axis", RotatePasteZ)
7615 pastemenu.additem("---", nil)
7616 pastemenu.additem("Cancel Paste", CancelPaste)
7617
7618 -- create a pop-up menu for selection actions
7619 selmenu = op.popupmenu()
7620 selmenu.additem("Cut", CutSelection)
7621 selmenu.additem("Copy", CopySelection)
7622 selmenu.additem("Clear", ClearSelection)
7623 selmenu.additem("Clear Outside", ClearOutside)
7624 selmenu.additem("---", nil)
7625 selmenu.additem("Flip X Coords", FlipSelectionX)
7626 selmenu.additem("Flip Y Coords", FlipSelectionY)
7627 selmenu.additem("Flip Z Coords", FlipSelectionZ)
7628 selmenu.additem("---", nil)
7629 selmenu.additem("Rotate X Axis", RotateSelectionX)
7630 selmenu.additem("Rotate Y Axis", RotateSelectionY)
7631 selmenu.additem("Rotate Z Axis", RotateSelectionZ)
7632 selmenu.additem("---", nil)
7633 selmenu.additem("Cancel Selection", CancelSelection)
7634end
7635
7636--------------------------------------------------------------------------------
7637
7638local showtoolbar = false -- restore tool bar?
7639
7640function CheckWindowSize()
7641 -- if viewport size has changed then resize the overlay
7642 local newwd, newht = g.getview(g.getlayer())
7643 if newwd ~= viewwd or newht ~= viewht then
7644 viewwd, viewht = newwd, newht
7645 ovwd, ovht = viewwd, viewht
7646 if ovwd < minwd then ovwd = minwd end
7647 if ovht < minht then ovht = minht end
7648 local fullscreen = g.getoption("fullscreen")
7649 if fullscreen == 1 and toolbarht > 0 then
7650 -- hide tool bar but restore it when we exit full screen mode
7651 toolbarht = 0
7652 showtoolbar = true
7653 elseif fullscreen == 0 and showtoolbar then
7654 if toolbarht == 0 then
7655 -- restore tool bar
7656 toolbarht = mbarht+buttonht+gap*2
7657 end
7658 showtoolbar = false
7659 end
7660 midx = int(ovwd/2)
7661 midy = int(ovht/2 + toolbarht/2)
7662 ov("resize "..ovwd.." "..ovht)
7663 Refresh()
7664 end
7665end
7666
7667--------------------------------------------------------------------------------
7668
7669function CheckCursor(xy)
7670 local editing = currcursor ~= movecursor
7671 if #xy > 0 then
7672 -- update cursor if mouse moves in/out of tool bar
7673 local x, y = split(xy)
7674 x = tonumber(x)
7675 y = tonumber(y)
7676 if y < toolbarht then
7677 if not arrow_cursor then
7678 -- mouse moved inside tool bar
7679 ov("cursor arrow")
7680 arrow_cursor = true
7681 if #activecell > 0 and editing then
7682 activecell = ""
7683 Refresh()
7684 end
7685 end
7686 else
7687 if arrow_cursor then
7688 -- mouse moved outside tool bar
7689 ov("cursor "..currcursor)
7690 arrow_cursor = false
7691 end
7692 if editing then
7693 local oldcell = activecell
7694 activecell = InsideActiveCell(x, y)
7695 if activecell ~= oldcell then Refresh() end
7696 end
7697 end
7698 elseif #activecell > 0 and editing then
7699 activecell = ""
7700 Refresh()
7701 end
7702end
7703
7704--------------------------------------------------------------------------------
7705
7706function ViewChanged(rotate)
7707 -- cube needs recreating on rotate or depth shade toggle
7708 lastHistorySize = -1
7709 lastCubeSize = -1
7710 lastBusyCubeSize["E"] = -1
7711 lastBusyCubeSize["O"] = -1
7712 if not rotate then
7713 -- sphere only needs recreating on depth shade toggle
7714 lastSphereSize = -1
7715 lastBusySphereSize["E"] = -1
7716 lastBusySphereSize["O"] = -1
7717 end
7718end
7719
7720--------------------------------------------------------------------------------
7721
7722function Rotate(xangle, yangle, zangle, display)
7723 if display == nil then display = true end
7724 local x = xangle * DEGTORAD
7725 local y = yangle * DEGTORAD
7726 local z = zangle * DEGTORAD
7727 local cosrx = cos(x)
7728 local sinrx = sin(x)
7729 local cosry = cos(y)
7730 local sinry = sin(y)
7731 local cosrz = cos(z)
7732 local sinrz = sin(z)
7733
7734 -- calculate transformation matrix for rotation
7735 -- (note that rotation is about fixed *screen* axes)
7736 local a = cosry*cosrz
7737 local b = cosry*sinrz
7738 local c = -sinry
7739 local d = sinrx*sinry*cosrz - cosrx*sinrz
7740 local e = sinrx*sinry*sinrz + cosrx*cosrz
7741 local f = sinrx*cosry
7742 local g = cosrx*sinry*cosrz + sinrx*sinrz
7743 local h = cosrx*sinry*sinrz - sinrx*cosrz
7744 local i = cosrx*cosry
7745
7746 -- rotate global matrix by new matrix
7747 local anew = a*xixo + b*yixo + c*zixo
7748 local bnew = a*xiyo + b*yiyo + c*ziyo
7749 local cnew = a*xizo + b*yizo + c*zizo
7750 local dnew = d*xixo + e*yixo + f*zixo
7751 local enew = d*xiyo + e*yiyo + f*ziyo
7752 local fnew = d*xizo + e*yizo + f*zizo
7753 local gnew = g*xixo + h*yixo + i*zixo
7754 local hnew = g*xiyo + h*yiyo + i*ziyo
7755 local inew = g*xizo + h*yizo + i*zizo
7756
7757 -- check if the view changed
7758 if (xixo ~= anew) or (xiyo ~= bnew) or (xizo ~= cnew) or
7759 (yixo ~= dnew) or (yiyo ~= enew) or (yizo ~= fnew) or
7760 (zixo ~= gnew) or (ziyo ~= hnew) or (zizo ~= inew) then
7761 ViewChanged(true)
7762 end
7763
7764 -- update the transformation matrix
7765 xixo = anew
7766 xiyo = bnew
7767 xizo = cnew
7768 yixo = dnew
7769 yiyo = enew
7770 yizo = fnew
7771 zixo = gnew
7772 ziyo = hnew
7773 zizo = inew
7774 ovt{"settrans3d", xixo, xiyo, xizo, yixo, yiyo, yizo, zixo, ziyo, zizo}
7775
7776 if display then Refresh() end
7777end
7778
7779--------------------------------------------------------------------------------
7780
7781function InitialView(display)
7782 if display == nil then display = true end
7783
7784 -- initialize the transformation matrix
7785 xixo = 1.0; yixo = 0.0; zixo = 0.0
7786 xiyo = 0.0; yiyo = 1.0; ziyo = 0.0
7787 xizo = 0.0; yizo = 0.0; zizo = 1.0
7788 ovt{"settrans3d", xixo, xiyo, xizo, yixo, yiyo, yizo, zixo, ziyo, zizo}
7789
7790 -- rotate to a nice view but don't call Refresh
7791 Rotate(160, 20, 0, false)
7792 -- user can hit the up arrow 4 times and the right arrow 4 times
7793 -- to see an untilted XY plane parallel with the screen
7794
7795 FitGrid(display) -- calls Refresh if display is true
7796end
7797
7798--------------------------------------------------------------------------------
7799
7800function InitDepthShading()
7801 -- initialize each depth shading layer
7802 local extradepth = round(depthlayers * sqrt(3))
7803 mindepth = -extradepth // 2
7804 maxdepth = depthlayers + extradepth // 2
7805 ovt{"setdepthshading3d", depthshading, depthlayers, mindepth, maxdepth}
7806end
7807
7808--------------------------------------------------------------------------------
7809
7810function Initialize()
7811 CreateOverlay()
7812 CreateMenuBar()
7813 CreateToolBar()
7814 CreatePopUpMenus()
7815 CreateAxes()
7816 InitDepthShading()
7817 UpdateHistory()
7818
7819 if #rulestring == 0 then
7820 -- first call must initialize rulestring, survivals, births and NextGeneration
7821 ParseRule(DEFAULT_RULE)
7822 end
7823
7824 if N == 0 then
7825 -- set grid size to default
7826 SetGridSizeOnly(DEFAULTN)
7827 else
7828 SetGridSizeOnly(N)
7829 end
7830
7831 -- create reference cube (never displayed)
7832 refcube = CreateCube(0,0,0)
7833
7834 ClearCells()
7835 if rulestring == DEFAULT_RULE then
7836 -- initial pattern is the Life-like glider in rule 3D5,6,7/6
7837 local mid = N//2
7838 SetLiveCell(mid, mid+1, mid)
7839 SetLiveCell(mid+1, mid, mid)
7840 SetLiveCell(mid-1, mid-1, mid)
7841 SetLiveCell(mid, mid-1, mid)
7842 SetLiveCell(mid+1, mid-1, mid)
7843 SetLiveCell(mid, mid+1, mid-1)
7844 SetLiveCell(mid+1, mid, mid-1)
7845 SetLiveCell(mid-1, mid-1, mid-1)
7846 SetLiveCell(mid, mid-1, mid-1)
7847 SetLiveCell(mid+1, mid-1, mid-1)
7848 dirty = false
7849 end
7850
7851 SetActivePlane()
7852 InitialView(false) -- don't call Refresh now (we'll do it below)
7853
7854 -- run the user's startup script if it exists
7855 local f = io.open(startup, "r")
7856 if f then
7857 f:close()
7858 RunScript(startup)
7859 ClearUndoRedo() -- don't want to undo startup script
7860 end
7861
7862 -- note that startup script might have changed BACK_COLOR etc
7863 ov("textoption background "..BACK_COLOR:sub(6))
7864 ssbutton.customcolor = START_COLOR
7865 ssbutton.darkcustomcolor = SELSTART_COLOR
7866
7867 ovt{"setpattern3d", grid1, false}
7868 update_grid = false
7869
7870 Refresh()
7871end
7872
7873--------------------------------------------------------------------------------
7874
7875function HandleKey(event)
7876 local CMDCTRL = "cmd"
7877 if g.os() ~= "Mac" then CMDCTRL = "ctrl" end
7878 local _, key, mods = split(event)
7879 if key == "return" and mods == "none" then StartStop()
7880 elseif key == "space" and mods == "none" then Step1()
7881 elseif key == "tab" and mods == "none" then NextStep()
7882 elseif key == "down" and mods == "none" then Rotate(-5, 0, 0)
7883 elseif key == "up" and mods == "none" then Rotate( 5, 0, 0)
7884 elseif key == "left" and mods == "none" then Rotate( 0, -5, 0)
7885 elseif key == "right" and mods == "none" then Rotate( 0, 5, 0)
7886 elseif key == "down" and mods == "alt" then Rotate( 0, 0, -5)
7887 elseif key == "right" and mods == "alt" then Rotate( 0, 0, -5)
7888 elseif key == "up" and mods == "alt" then Rotate( 0, 0, 5)
7889 elseif key == "left" and mods == "alt" then Rotate( 0, 0, 5)
7890 elseif key == "delete" and mods == "none" then ClearSelection()
7891 elseif key == "delete" and mods == "shift" then ClearOutside()
7892 elseif key == "=" and mods == "none" then Faster()
7893 elseif key == "-" and mods == "none" then Slower()
7894 elseif key == "1" and mods == "none" then SetStepSizeTo1()
7895 elseif key == "5" and mods == "none" then RandomPattern()
7896 elseif key == "n" and mods == CMDCTRL then NewPattern()
7897 elseif key == "o" and mods == CMDCTRL then OpenPattern()
7898 elseif key == "s" and mods == CMDCTRL then SavePattern()
7899 elseif key == "o" and mods == "shift" then OpenClipboard()
7900 elseif key == "r" and mods == "shift" then RunClipboard()
7901 elseif key == "r" and mods == CMDCTRL then Reset()
7902 elseif key == "r" and mods == "none" then ChangeRule()
7903 elseif key == "g" and (mods == "none" or mods == CMDCTRL) then SetGridSize()
7904 elseif key == "a" and (mods == "none" or mods == CMDCTRL) then SelectAll()
7905 elseif key == "k" and (mods == "none" or mods == CMDCTRL) then CancelSelection()
7906 elseif key == "z" and (mods == "none" or mods == CMDCTRL) then Undo()
7907 elseif key == "z" and (mods == "shift" or mods == CMDCTRL.."shift") then Redo()
7908 elseif key == "x" and mods == CMDCTRL then CutSelection()
7909 elseif key == "c" and mods == CMDCTRL then CopySelection()
7910 elseif key == "v" and (mods == "none" or mods == CMDCTRL) then Paste()
7911 elseif key == "v" and mods == "alt" then CancelPaste()
7912 elseif key == "b" and mods == CMDCTRL then PasteOR()
7913 elseif key == "b" and mods == CMDCTRL.."shift" then PasteXOR()
7914 elseif key == "b" and mods == "none" then Rotate(0, 180, 0)
7915 elseif key == "[" and mods == "none" then ZoomOut()
7916 elseif key == "]" and mods == "none" then ZoomIn()
7917 elseif key == "{" and mods == "none" then ZoomHalf()
7918 elseif key == "}" and mods == "none" then ZoomDouble()
7919 elseif key == "i" and mods == "none" then InitialView()
7920 elseif key == "f" and mods == "none" then FitGrid()
7921 elseif key == "p" and mods == "none" then CycleCellType()
7922 elseif key == "l" and mods == "none" then ToggleLines()
7923 elseif key == "l" and mods == "shift" then ToggleAxes()
7924 elseif key == "d" and mods == "alt" then ToggleDepthShading()
7925 elseif key == "t" and mods == "none" then ToggleToolBar()
7926 elseif key == "," and mods == "none" then MoveActivePlane(activepos+1, true)
7927 elseif key == "." and mods == "none" then MoveActivePlane(activepos-1, true)
7928 elseif key == "a" and mods == "shift" then CycleActivePlane()
7929 elseif key == "c" and mods == "none" then CycleCursor()
7930 elseif key == "d" and mods == "none" then DrawMode()
7931 elseif key == "s" and mods == "none" then SelectMode()
7932 elseif key == "m" and mods == "none" then MoveMode()
7933 elseif key == "m" and mods == "shift" then MiddlePattern()
7934 elseif key == "h" and mods == "none" then ShowHelp()
7935 elseif key == "y" and mods == "none" then ToggleShowHistory()
7936 elseif key == "y" and mods == "shift" then ToggleFadeHistory()
7937 elseif key == "q" then ExitScript()
7938 else
7939 -- could be a keyboard shortcut (eg. for full screen)
7940 g.doevent(event)
7941 end
7942end
7943
7944--------------------------------------------------------------------------------
7945
7946function MouseDown(x, y, mods, mouseinfo)
7947 -- mouse button has been pressed
7948 mouseinfo.mousedown = true
7949 mouseinfo.prevx = x
7950 mouseinfo.prevy = y
7951 if pastecount > 0 then
7952 -- paste pattern can be dragged using any cursor
7953 mouseinfo.dragface = StartDraggingPaste(x, y)
7954 mouseinfo.drag_paste = #mouseinfo.dragface > 0
7955 end
7956 if mouseinfo.drag_paste then
7957 -- ignore currcursor
7958 RememberCurrentState()
7959 elseif currcursor == drawcursor then
7960 if mods == "none" then
7961 mouseinfo.drawing = StartDrawing(x, y)
7962 elseif mods == "shift" then
7963 mouseinfo.drag_active = StartDraggingPlane(x, y)
7964 end
7965 elseif currcursor == selectcursor then
7966 if mods == "none" then
7967 mouseinfo.selecting = StartSelecting(x, y)
7968 elseif mods == "shift" then
7969 mouseinfo.drag_active = StartDraggingPlane(x, y)
7970 end
7971 else
7972 -- currcursor == movecursor
7973 if mods == "none" then
7974 if selcount > 0 then
7975 mouseinfo.dragface = StartDraggingSelection(x, y)
7976 mouseinfo.drag_selection = #mouseinfo.dragface > 0
7977 end
7978 elseif mods == "alt" then
7979 mouseinfo.hand_erase = true
7980 EraseLiveCells(x, y, true)
7981 elseif mods == "shift" then
7982 mouseinfo.hand_select = true
7983 SelectLiveCells(x, y, true)
7984 end
7985 end
7986end
7987
7988--------------------------------------------------------------------------------
7989
7990function MouseUp(mouseinfo)
7991 -- mouse button has been released
7992 mouseinfo.mousedown = false
7993 if mouseinfo.drawing then
7994 mouseinfo.drawing = false
7995 CheckIfGenerating()
7996 elseif mouseinfo.selecting then
7997 mouseinfo.selecting = false
7998 CheckIfGenerating()
7999 elseif mouseinfo.drag_paste then
8000 mouseinfo.drag_paste = false
8001 CheckIfGenerating()
8002 elseif mouseinfo.drag_selection then
8003 mouseinfo.drag_selection = false
8004 StopDraggingSelection()
8005 CheckIfGenerating()
8006 elseif mouseinfo.drag_active then
8007 mouseinfo.drag_active = false
8008 CheckIfGenerating()
8009 elseif mouseinfo.hand_erase then
8010 mouseinfo.hand_erase = false
8011 CheckIfGenerating()
8012 elseif mouseinfo.hand_select then
8013 mouseinfo.hand_select = false
8014 CheckIfGenerating()
8015 end
8016end
8017
8018--------------------------------------------------------------------------------
8019
8020function CheckMousePosition(mousepos, mouseinfo)
8021 if #mousepos > 0 then
8022 local x, y = split(mousepos)
8023 x = tonumber(x)
8024 y = tonumber(y)
8025 if x ~= mouseinfo.prevx or y ~= mouseinfo.prevy then
8026 -- mouse has moved
8027 if mouseinfo.drawing then
8028 DrawCells(x, y)
8029 elseif mouseinfo.selecting then
8030 SelectCells(x, y)
8031 elseif mouseinfo.drag_paste then
8032 DragPaste(x, y, mouseinfo.prevx, mouseinfo.prevy, mouseinfo.dragface)
8033 elseif mouseinfo.drag_selection then
8034 DragSelection(x, y, mouseinfo.prevx, mouseinfo.prevy, mouseinfo.dragface)
8035 elseif mouseinfo.drag_active then
8036 DragActivePlane(x, y, mouseinfo.prevx, mouseinfo.prevy)
8037 elseif mouseinfo.hand_erase then
8038 EraseLiveCells(x, y, false)
8039 elseif mouseinfo.hand_select then
8040 SelectLiveCells(x, y, false)
8041 else
8042 -- rotate the view
8043 local deltax = x - mouseinfo.prevx
8044 local deltay = y - mouseinfo.prevy
8045 Rotate(round(-deltay/2.0), round(deltax/2.0), 0)
8046 end
8047 mouseinfo.prevx = x
8048 mouseinfo.prevy = y
8049 end
8050 elseif #activecell > 0 and currcursor ~= movecursor then
8051 activecell = ""
8052 Refresh()
8053 end
8054end
8055
8056--------------------------------------------------------------------------------
8057
8058function EventLoop()
8059 -- best to call Initialize here so any error is caught by xpcall
8060 Initialize()
8061
8062 local mouseinfo = {
8063 mousedown = false, -- mouse button is down?
8064 drawing = false, -- draw/erase cells with pencil cursor?
8065 selecting = false, -- (de)select cells with cross-hairs cursor?
8066 drag_paste = false, -- drag paste pattern with any cursor?
8067 drag_selection = false, -- drag selected cells with hand cursor?
8068 drag_active = false, -- drag active plane with pencil/cross-hairs?
8069 hand_erase = false, -- erase live cells with hand cursor?
8070 hand_select = false, -- select live cells with hand cursor?
8071 dragface = "", -- which paste/selection face is being dragged
8072 prevx = nil, prevy = nil -- previous mouse position
8073 }
8074
8075 while true do
8076 local event = g.getevent()
8077 if #event == 0 then
8078 if not mouseinfo.mousedown then
8079 if not generating then
8080 g.sleep(5) -- don't hog the CPU when idle
8081 end
8082 CheckWindowSize() -- may need to resize the overlay
8083 end
8084 else
8085 if message and (event:find("^key") or event:find("^oclick") or event:find("^file")) then
8086 message = nil
8087 Refresh() -- remove the most recent message
8088 end
8089 event = op.process(event)
8090 if #event == 0 then
8091 -- op.process handled the given event
8092 elseif event:find("^key") then
8093 -- don't do key action if mouse button is down (can clobber undo history)
8094 if not mouseinfo.mousedown then
8095 HandleKey(event)
8096 end
8097 elseif event:find("^oclick") then
8098 local _, x, y, button, mods = split(event)
8099 x = tonumber(x)
8100 y = tonumber(y)
8101 if y > toolbarht then
8102 if (button == "right" and (mods == "none" or mods == "ctrl")) or
8103 (button == "left" and mods == "ctrl") then
8104 if pastecount > 0 then
8105 ChoosePasteAction(x, y)
8106 elseif selcount > 0 then
8107 ChooseSelectionAction(x, y)
8108 end
8109 elseif button == "left" then
8110 MouseDown(x, y, mods, mouseinfo)
8111 end
8112 end
8113 elseif event:find("^mup") then
8114 MouseUp(mouseinfo)
8115 elseif event:find("^ozoomout") then
8116 if not arrow_cursor then ZoomOutPower() end
8117 elseif event:find("^ozoomin") then
8118 if not arrow_cursor then ZoomInPower() end
8119 elseif event:find("^file") then
8120 OpenFile(event:sub(6))
8121 end
8122 end
8123
8124 local mousepos = ov("xy")
8125 if mouseinfo.mousedown then
8126 CheckMousePosition(mousepos, mouseinfo)
8127 else
8128 CheckCursor(mousepos)
8129 if generating then NextGeneration() end
8130 end
8131 end
8132end
8133
8134--------------------------------------------------------------------------------
8135
8136ReadSettings()
8137oldstate = SaveGollyState()
8138
8139status, err = xpcall(EventLoop, gp.trace)
8140if err then g.continue(err) end
8141-- the following code is always executed
8142
8143-- ensure the following code *completes*, even if user quits Golly
8144g.check(false)
8145
8146RestoreGollyState(oldstate)
8147WriteSettings()
8148