diff --git a/.gitignore b/.gitignore index 5e1422c..4cc98f5 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ /test/tmp/ /test/version_tmp/ /tmp/ +/.idea/ # Used by dotenv library to load environment variables. # .env diff --git a/README.md b/README.md index 79df5df..391d149 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ You can add a digging designation to the end of any of the drawing commands - [dujihrx]. The default is 'd'. -For example, `digshape star 5 2 j` will dig a 5 pointed star out of downstairs with the mark as the center and the cursor as a vertex on the star +For example, `digshape star 5 2 j` will dig a 5 pointed star out of 'downstair' designations with the mark as the center and the cursor as a vertex on the star. ## In-game help digshape ? @@ -12,15 +12,15 @@ For example, `digshape star 5 2 j` will dig a 5 pointed star out of downstairs w *The origin is used for most drawing operations. It persists between operations. It is Step 1 in any of the following operations.* -## draw a line +## Draw a line digshape line *draws a straight line from cursor to mark* -## draw an ellipse +## Draw an ellipse digshape ellipse [filled] *Step 2: draw an ellipse contained within the bounding box formed of the mark and current cursor.* -### draw an ellipse using 3 pt +### Draw an ellipse using 3 pt digshape major *Step 2: set the end of the major axis from mark to cursor.* @@ -28,11 +28,11 @@ For example, `digshape star 5 2 j` will dig a 5 pointed star out of downstairs w *Step 3: draw the ellipse using new cursor as the semiminor axis length (midpoint of major axis->cursor)* *Note: ellipse3p cannot yet be filled by an argument, use flood* -## draw a circle with arbitrary diameter +## Draw a circle with arbitrary diameter digshape circle2p *Step 2: mark and cursor form the diameter of the circle (at any tilt).* -## draw a 3 pt bezier curve +## Draw a 3 pt bezier curve digshape major *Step 2: set the endpoint of the curve* @@ -44,47 +44,49 @@ Step 1: place start of curve at cursor: `digshape origin`\ Step 2: move cursor to end of curve, then: `digshape major`\ Step 3: move cursor to the side of the line to be the control point for the curve, then: `digshape bez`, or `digshape bez 9` for a curve that gets closer to the cursor. -## draw a polygon with cursor as vertex +## Draw a polygon with cursor as vertex digshape polygon *Step 2: draw a polygon with sides using the mark as center and cursor as a vertex* _eg:_ `digshape polygon 5 h`: draws a pentagram of channel designations, with the mark as the center, and the cursor as one of the verticies. \ _eg:_ `digshape polygon 6`: draws a hexagon of dig designations, with the mark as the center, and the cursor as one of the verticies. -## draw a polygon with cursor as apothem +## Draw a polygon with cursor as apothem digshape polygon apothem *Step 2: Draw a polygon with n sides, with the mark as center, and cursor as a midpoint of one of the sides [apothem, like a radius]* ## Draw a star polygon In [Schläfli symbol notation](https://en.wikipedia.org/wiki/Schl%C3%A4fli_symbol) -*mark as center and cursor as a vertex* +*Origin as center and cursor as a vertex* + digshape star [skip=2] -## draw a point with n-fold symmetry +## Draw a point with n-fold symmetry digshape star *Step 2: draw a point at the cursor, and at n points around the origin at the same radius, as though they were verticies of a star without drawing the connecting lines. (eg for 5fold: "digshape star 5 5"). It is helpful to bind this to a keycombo, so that it can be used to draw.* -## Draw an Archimedean spiral with specified number of "coils", each point separated by "chord" tiles +## Draw an Archimedean spiral digshape spiral +*Draws a spiral with specified number of "coils", each point separated by "chord" tiles* -## flood fill an area +## Flood fill an area digshape flood [max coverage=10000] *Fill an area with a dig designation. Will only fill tiles that match the designation under the cursor. Ignores/does not require the origin to be set.* *Note: Larger max coverages can take time to fill. A great way to fill in the above shapes.* -## undo last digshape command +## Undo last digshape command digshape undo -*restores designations for the last digshape command. Will not record manual or other commands designations, but won't loose it's record.* +*Restores designations for the last digshape command. Will not record manual or other commands designations, but won't loose it's record.* -## move all markers to the current z level +## Move all markers to the current z level digshape resetz -*moves the markers (origin, major) to the current z level* - - digshape star [skip=2] +*Moves the markers (origin, major) to the current z level* +## Swap origin and cursor markers + digshape swap +*Swaps the cursor and the origin marker, handy for walking a spline* + -## Draw an Archimedean spiral with specified number of "coils", each point separated by "chord" tiles - digshape spiral # Contributors diff --git a/digshape.rb b/digshape.rb index 919ba50..306e952 100644 --- a/digshape.rb +++ b/digshape.rb @@ -1,32 +1,37 @@ -# dig shapes +# designate geometric shapes + =begin digshape ======= +digshape allows the creation of a number of repetative geometric designations. The script can be called manually, or through gui/digshape. Commands that do not require a set origin: To dig a 3x3 sparse up-down stairway: - digshape downstair + digshape downstair (differentStart? [default: true]) To undo the previous command (restoring designation): digshape undo + To flood fill with a designation, overwriting ONLY the designation under the cursor: + digshape flood + Commands that require an origin to be set: To set the origin for drawing: digshape origin - To draw to the target point: + To draw to the cursor: digshape line To draw an ellipse using the origin and target as a bounding box: digshape ellipse (filled? [default: false]) - To draw an ellipse using the origin and target as a major axis, and the cursor as the length of the semiminor axis: - digshape major (to set the major axis endpoint) + To draw an ellipse using the origin and target as a major axis, and the cursor as the length of the semiminor axis (aka width [dist from cursor to line]): + digshape major (to set the major axis endpoint [the length]) digshape ellipse3p - To draw a circle using an arbitrary diameter: + To draw a circle using an arbitrary diameter (gives slightly different results to digcircle): digshape circle2p To draw a 3 pt bezier curve, with an arbitrary float for weighting the sharpness: @@ -35,92 +40,370 @@ To draw a polygon using the origin as the center and the cursor as the radius|apothem (radius) digshape polygon [radius|apothem] [digMode] - - To flood fill with a designation, overwriting ONLY the designation under the cursor: - digshape flood - - To move all of the markers to the current z level: - digshape resetz - To draw an Archimedean spiral (coils - number of coils, chord - distance between points): + To draw a star using the origin as the center and the cursor as the radius|apothem (radius) + digshape star [skip] [digMode] + + To draw an Archimedean spiral (coils - number of coils, chord - distance between points) centered on the origin and having a radius to the cursor: digshape spiral + To move all of the markers to the current z level: + digshape resetz + + To swap the origin and cursor: + digshape swap All commands accept a digging designation mode as a single character argument [dujihrx], otherwise will default to 'd' +=end + + + + + + +=begin +============ SCRIPT DESCRIPTION ============ +Digshape operates directly on the current mapstate. An undo buffer is maintained for the last-run command, and for the markers. + +This file is broken into segments. +=GUI interfacing + code required to allow headless operation through gui/digshape. + +=Utility functions + generic helper functions for script + +=Data structures + structure to hold coordinates, and to interpret the designations. + +=Cursor functions + code to interact with the map (df(hack)). Get, set, undo. + +=Shape functions + functions to draw geometries. + +=Script control + print help, functions to parse arguments, untap, upkeep. + +=Commands + user-callable interactions with digshape. Each command is responsible for getting arguments it needs, calling the functions to make it's geometry, and plotting them to the map (frequently a side-effect of the geometry function). + +============ KNOWN BUGS ============ + BUG: "digshape polygon 3 r" uses both 'radius' and digmode:'r' + BUG: "digshape polygon 2 apothem" does not work, but higher numbers do. + + +============ TODO ============ + TODO: mark origin should not change the digging designation, ellipse cleanup should restore not clear it. + TODO: replace text dig designations with dfhack enums + TODO: convert digshape to lua script + TODO: just always use the current Z level. (ensure undo) + TODO: rename control points to A,B,C,... for more generality and faster discussion. Maybe origin+ABCD...? [Origin, Cursor, A,B,C,D,...] + TODO: add marker mode/toggle marker designation, smooth, engrave, carveFortification + + +============ FEATURE IDEAS ============ + IDEA: Pixel fonts (size) + IDEA: Gradient fill. (2pt box, 1pt midpoint, arg: direction[NSEW,diags,star,pit,rings,etc]) + IDEA: 3d shapes (eg platonic solids) + + IDEA: circle3p (given any 3p) (((Ali Sheikhpour (https://math.stackexchange.com/users/707123/ali-sheikhpour), Get the equation of a circle when given 3 points, URL (version: 2021-01-26): https://math.stackexchange.com/q/4000949))) + IDEA: arc 3p (as circle3p but only draw inside bbox) + IDEA: default digmode is whatever is selected currently in the active df:designationMode screen + IDEA: add diagonal adjacency to floodfill as an option +=end + + + + + + -TODO: mark origin should not change the digging designation, ellipse cleanup should restore not clear it. + + +=begin +======================== DIGSHAPE GUI interfacing +really nasty hack so that lua can use digshape. + +GUI allows for 'preview' which returns the points digshape would dig, but does not modify the map. Printing and errors are redirected. =end -def markOrigin(ox, oy, oz) - t = df.map_tile_at(ox, oy, oz) - if t then - s = t.shape_basic - #TODO: preseve designation: - #$originTile = t.designation # a global to store the original origin state - #puts "origin: #{$originTile}" - t.dig(:Default) if s == :Wall +$isLuaMode = $script_args[0] == "lua" +$isPreviewOnly = false + +if $isLuaMode then +# puts "-->> Scriptargs:" +# puts $script_args +# puts "<<--" + + $script_args.delete_at(0) + $isPreviewOnly = $script_args[0] == "preview" + if $isPreviewOnly then + $script_args.delete_at(0) + if $script_args[0] == "argreference" then + puts "ref:ARGREFTABLE" + $script_args.delete_at(0) + #throw :script_finished + end + end + + $output + +end + +def writeLuaPos(name, digPos) # pos:::: + if $isLuaMode then + puts "pos:#{name}:#{digPos.to_s}" end end -def unDig() - #Exicute one level of undo. - #z level is presumed to be the current. - i=$digBufferX.length - while i >= 0 do - x=$digBufferX.pop - y=$digBufferY.pop - d=$digBufferD.pop - - digAt(x,y,df.cursor.z, enum2dig(d), buffer=false) - i = i-1 - end - #clear buffer for next dig. - clearDigBuffer() + + + + +=begin +======================== UTILITY FUNCTIONS +=end + + +def stdout(msg) + # print a message to the console + if $isLuaMode == false then + puts msg + else + puts "msg:"+msg + end +end + +def stderr(msg) + # print an error message to the console + if $isLuaMode == false then + puts " Error: "+msg + else + puts "err:"+msg + end +end + +def scriptError(msg) + # call this when you reach corner cases / the fault perhaps isn't the user. Script terminates. + stderr(msg) + raise "oopsie! script errored! ;)" +end + +def userSucks(msg) + # call this when we don't like the user's input. Script may continue. + stderr(msg) + throw :script_finished +end + + + + + + + + + +=begin +======================== DATA STRUCTURES +=end + + +DigPos = Struct.new(:x, :y, :z) do + #Holds a 3d coordinate + def to_s + return "(#{x},#{y},#{z})" + end + def clone + return DigPos.new(x,y,z) + end end +#Control points: + #$origin + #$major + #$cursor def clearDigBuffer() - #clear buffer for next dig, or initialize it's existance on first run. + #$digBuffer* is a set of global arrays containing the 3d coordinates and their prior dig designations. + + #clear buffer for next dig, or initialize if empty. $digBufferX=[] $digBufferY=[] + $digBufferZ=[] $digBufferD=[] end -def digAt(x, y, z, digMode = 'd', buffer=true) - t = df.map_tile_at(x, y, z) - - #store the current tile's designation in a undo buffer - if buffer then - $digBufferX.push(x) - $digBufferY.push(y) - $digBufferD.push(t.designation.dig) +def getDigMode(digMode = 'd') +#TODO just integrate this into getDigModeArgument. + if ['d', 'u', 'j', 'i', 'h', 'r', 'x'].include? digMode then + return digMode + end + return 'd' +end + + +def digMode2enum(digMode) + #this function turns a digmode into the appropriate enum for easier comparison on tile reading (eg floodfill.) + case digMode #from https://github.com/DFHack/scripts/blob/master/digfort.rb + when 'd'; return :Default + when 'u'; return :UpStair + when 'j'; return :DownStair + when 'i'; return :UpDownStair + when 'h'; return :Channel + when 'r'; return :Ramp + when 'x'; return :No + else + scriptError("Unknown digMode, `"+digMode+"', digMode must be any of 'd', 'u', 'j', 'i', 'h', 'r', or 'x', which correspond to the designation keys") + end +end + + +def enum2digMode(digEnum) + #this function turns a designation enum into the appropriate digtype character + case digEnum #from https://github.com/DFHack/scripts/blob/master/digfort.rb + when :Default; return 'd' + when :UpStair; return 'u' + when :DownStair; return 'j' + when :UpDownStair; return 'i' + when :Channel; return 'h' + when :Ramp; return 'r' + when :No; return 'x' + else + scriptError("Unknown digEnum `#{digEnum.to_s}'") end +end + + + + +=begin +======================== CURSOR FUNCTIONS +=end + + +def cursorAsDigPos() + #returns current cursor position as a DigPos + return DigPos.new(df.cursor.x, df.cursor.y, df.cursor.z) +end + + + + +def setOrigin(x, y, z) + # sets the origin and marks if it iff we are in console. cleans up last mark too. + #TODO: make 'controlpoints' an array indexed by name, so that all can be operated on in the same way. + $origin = DigPos.new(x,y,z) + # the rest is just really complicated logic to mark the origin if the user is using the console version + # it also has to play well with lua + + if $oldOrigin then + oldTile = df.map_tile_at($oldOrigin.x, $oldOrigin.y, $oldOrigin.z) + oldTile.dig($oldOriginDesignation) if oldTile.shape_basic == $oldOriginShape && oldTile.designation.dig == digMode2enum('d') + end + if not $isLuaMode then + $oldOrigin = $origin.clone() + newOriginTile = df.map_tile_at($origin.x, $origin.y, $origin.z) + $oldOriginDesignation = newOriginTile.designation.dig + $oldOriginShape = newOriginTile.shape_basic + + digAt($origin.x, $origin.y, $origin.z, 'd', buffer: false) + else + $oldOrigin = nil # don't undo our origins if we are in lua mode + end +end + +def setMajor(x, y, z) + # Assigns the control point 'major' to the current cursor location + $major = DigPos.new(x,y,z) +end + + + +def undo() + #Execute one level of undo. + #z level is presumed to be the current. + #BUG: Does not keep track of Z levels + #TODO: have multiple levels of undo / redo + i=$digBufferX.length + newBufferX = [] + newBufferY = [] + newBufferZ = [] # redundant, i.e. always the same most of the time, but needed so that we use it as a pointer for digAt. Also supports digging multi dimenisional shapes + newBufferD = [] + while i > 0 do + x=$digBufferX.pop + y=$digBufferY.pop + z=$digBufferZ.pop + d=$digBufferD.pop + digAt(x,y,z, enum2digMode(d), buffer: true, bufferX: newBufferX, bufferY: newBufferY, bufferZ: newBufferZ, bufferD: newBufferD) + i = i-1 + end + + #clear buffer for next dig + $digBufferX = newBufferX + $digBufferY = newBufferY + $digBufferZ = newBufferZ + $digBufferD = newBufferD +end + + +def isDigPermitted(digMode, tileShape) + # can we dig on this tile? + if not tileShape then return false end + + case digMode + when 'd', 'u', 'i', 'r'; return tileShape == :Wall + when 'j', 'h'; return tileShape == :Wall || tileShape == :Floor + when 'x'; return true + else + scriptError("Unknown digMode: `"+digMode+"'") + end +end + +def digAt(x, y, z, digMode = 'd', buffer: true, bufferX: $digBufferX, bufferY: $digBufferY, bufferZ: $digBufferZ, bufferD: $digBufferD) + #Commit designation@coords to the map, opt save current value there to the buffer for undo. + + #puts "DIGAT: "+x.to_s+","+y.to_s + tile = df.map_tile_at(x, y, z) # check if the tile returned is valid, ignore if its not (out of bounds, air, etc) - if t then - s = t.shape_basic - - case digMode #from https://github.com/DFHack/scripts/blob/master/digfort.rb - when 'd'; t.dig(:Default) if s == :Wall - when 'u'; t.dig(:UpStair) if s == :Wall - when 'j'; t.dig(:DownStair) if s == :Wall or s == :Floor - when 'i'; t.dig(:UpDownStair) if s == :Wall - when 'h'; t.dig(:Channel) if s == :Wall or s == :Floor - when 'r'; t.dig(:Ramp) if s == :Wall - when 'x'; t.dig(:No) + if tile then + tileShape = tile.shape_basic + + if isDigPermitted(digMode, tileShape) then + if $isPreviewOnly then + puts "dig:"+digMode+":"+x.to_s+":"+y.to_s+":"+z.to_s else - puts " Error: Unknown digtype" - throw :script_finished + if buffer then # store the current tile's designation in a undo buffer + bufferX.push(x) + bufferY.push(y) + bufferZ.push(z) + bufferD.push(tile.designation.dig) + + end + tile.dig(digMode2enum(digMode)) + end end end end -# https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm + + + + + + +=begin +======================== SHAPES FUNCTIONS +=end + + def drawLineLow(x0, y0, z0, x1, y1, z1, digMode = 'd') + # Helper function for drawLine. + # Uses: https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm dx = x1 - x0 dy = y1 - y0 yi = 1 @@ -145,6 +428,8 @@ def drawLineLow(x0, y0, z0, x1, y1, z1, digMode = 'd') end def drawLineHigh(x0, y0, z0, x1, y1, z1, digMode = 'd') + # Helper function for drawLine. + # Uses: https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm dx = x1 - x0 dy = y1 - y0 xi = 1 @@ -168,6 +453,8 @@ def drawLineHigh(x0, y0, z0, x1, y1, z1, digMode = 'd') end def drawLine(x0, y0, z0, x1, y1, z1, digMode = 'd') + # Draw a straight, line between two points. + # Uses: https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm if (y1 - y0).abs < (x1 - x0).abs then if x0 > x1 then drawLineLow(x1, y1, z1, x0, y0, z0, digMode) @@ -183,13 +470,15 @@ def drawLine(x0, y0, z0, x1, y1, z1, digMode = 'd') end end - def plotQuadRationalBezierSeg(x0, y0, z0, x1, y1, z1, x2, y2, z2, w, digMode = 'd') - #/* plot a limited rational Bezier segment, squared weight */ - #http://members.chello.at/easyfilter/bresenham.pdf listing 12 + # Helper function for plotQuadRationalBezier, draws one portion of the curve. + +=begin + /* plot a limited rational Bezier segment, squared weight */ + Source: http://members.chello.at/easyfilter/bresenham.pdf listing 12 #p0:origin, p1:weight, p2:termination #w is the weighting. "For w =1 the curve is a parabola, for w < 1 the curve is an ellipse, for w = 0 the curve is a straight line and for w>1 the curve is a hyperbola. The weights are normally assumed to be all positive." - +=end x0 = x0.floor #start with integer locations. Original code stores in int, so this is implicit. x1 = x1.floor x2 = x2.floor @@ -300,11 +589,10 @@ def plotQuadRationalBezierSeg(x0, y0, z0, x1, y1, z1, x2, y2, z2, w, digMode = ' drawLine(x0,y0,z0, x2,y2,z0, digMode) end - - def plotQuadRationalBezier(x0, y0, z0, x1, y1, z1, x2, y2, z2, w=1.5, digMode = 'd') - #http://members.chello.at/easyfilter/bresenham.pdf listing 11 - ## plot any quadratic rational Bezier curve */ + #Draw a bezier between origin[p0] and control point[p2], pulled out towards cursor[p1] (by a weighting of 'w') + + # Source: http://members.chello.at/easyfilter/bresenham.pdf listing 11: /* plot any quadratic rational Bezier curve */ x = x0 - 2 * x1 + x2 y = y0 - 2 * y1 + y2 @@ -390,20 +678,21 @@ def plotQuadRationalBezier(x0, y0, z0, x1, y1, z1, x2, y2, z2, w=1.5, digMode plotQuadRationalBezierSeg(x0, y0, z0, x1, y1, z0, x2, y2, z0, w * w, digMode) end - def plotRotatedEllipse(x, y, z, a, b, angle, digMode='d') - ## plot ellipse rotated by angle (radian) */ - #taken from: http://members.chello.at/easyfilter/bresenham.pdf listing 13. Explicitly released without copyright - #Note: most of this function deals with the ellipse at the origin. Translation to coordinates is at final call. + # Helper function for drawEllipse(). Draw an ellipse(center, major len, minor len) rotated by angle (radian) + +=begin + Source: http://members.chello.at/easyfilter/bresenham.pdf listing 13. Explicitly released without copyright + Note: most of this function deals with the ellipse at the origin. Translation to coordinates is at final call. - #x,y is the coodinates of the center - #a is __SEMI__major length - #b is __SEMI__minor length - #angle (radians), prob measured CCW from east + x,y is the coodinates of the center + a is __SEMI__major length + b is __SEMI__minor length + angle (radians), prob measured CCW from east - #A far more readable paper on plotting rotated ellipses (no pseudocode): http://www.crbond.com/papers/ell_alg.pdf - #Another paper on rasterizing 2d primitives: https://cs.brown.edu/research/pubs/theses/masters/1989/dasilva.pdf - + A far more readable paper on plotting rotated ellipses (no pseudocode): http://www.crbond.com/papers/ell_alg.pdf + Another paper on rasterizing 2d primitives: https://cs.brown.edu/research/pubs/theses/masters/1989/dasilva.pdf +=end angle = -angle #deal with -y axis. xd = a * a @@ -425,8 +714,6 @@ def plotRotatedEllipse(x, y, z, a, b, angle, digMode='d') plotRotatedEllipseRect(x - a, y - b, z, x + a, y + b, (4 * zd * Math.cos(angle)), digMode) end - - def plotRotatedEllipseRect(x0, y0, z0, x1, y1, zd, digMode='d') #http://members.chello.at/easyfilter/bresenham.pdf listing 13 #/* rectangle enclosing the ellipse, integer rotation angle */ @@ -441,7 +728,7 @@ def plotRotatedEllipseRect(x0, y0, z0, x1, y1, zd, digMode='d') if (zd == 0) #Special case: no rotation. Use standard method. /* looks nicer */ #this should never be reached, as we call this from the regular ellipse function. - puts "zd=0 degenerate case" + stdout("zd=0 degenerate case") drawEllipse(x0,y0,z0, x1,y1,z0) return end @@ -449,12 +736,11 @@ def plotRotatedEllipseRect(x0, y0, z0, x1, y1, zd, digMode='d') ## squared weight of P1 */ if (w != 0.0) then w = (w - zd) / (w + w) - end + end if not(w <= 1.0 && w >= 0.0) then #/* limit angle to |zd|<=xd*yd */ - puts " Error: Limit angle to |zd|<=xd*yd" - throw :script_finished - end + scriptError("Limit angle to |zd|<=xd*yd") + end ## snap xe,ye to int */ xd = (xd * w + 0.5).floor @@ -467,8 +753,10 @@ def plotRotatedEllipseRect(x0, y0, z0, x1, y1, zd, digMode='d') plotQuadRationalBezierSeg(x1, y1-yd, z0, x1,y0, z0, x0+xd,y0, z0, w, digMode) end - def drawEllipse(x0, y0, z0, x1, y1, z1, x2=nil, y2=nil, z2=nil, filled = false, digMode = 'd', mode = 'bbox') + #Draw an ellipse, using the current control points, in the method specified by mode. + +=begin # A Fast Bresenham Type Algorithm For Drawing Ellipses http://homepage.smc.edu/kennedy_john/belipse.pdf (https://www.dropbox.com/s/3q89g566u115g3q/belipse.pdf?dl=0) # also adapted from https://github.com/teichgraf/WriteableBitmapEx/blob/master/Source/WriteableBitmapEx/WriteableBitmapShapeExtensions.cs used under the MIT license @@ -476,6 +764,13 @@ def drawEllipse(x0, y0, z0, x1, y1, z1, x2=nil, y2=nil, z2=nil, filled = false, #p1 [xyz]: termination of major axis; OR the other corner of the bbox #p2 [xyz]: the extent (not a point nessisarily on the minor axis..) of the minor _radius_; aka, a point on the bounding box long side that will be used to determine the length of the short side. + #mode ['bbox']: + -'diameter': make a circle given 2p as the diameter + -'axis': make an ellipse along the line [origin, major], with the cursor setting the width. width is the distance from the cursor to the line. + -'bbox': generate an ellipse to fit entirely within the bounding box of [origin, cursor] + -IDEA: '5p': given 5p draw an ellipse that fits. +=end + xl = [x0, x1].min # find left edge xr = [x0, x1].max # find right edge yb = [y0, y1].min # find lower edge @@ -646,13 +941,22 @@ def drawEllipse(x0, y0, z0, x1, y1, z1, x2=nil, y2=nil, z2=nil, filled = false, end end -def digKeupoStair(x, y, z, depth) +def digKeupoStair(x, y, z, depth,differentStart=true) + #Dig an X of updown stairs (corners and center of a 3x3) centered on cursor, down a number of zlevels. iz = z + startingDesignation='' + digAt(x, y, iz, 'j') digAt(x - 1, y + 1, iz, 'j') digAt(x - 1, y - 1, iz, 'j') digAt(x + 1, y + 1, iz, 'j') digAt(x + 1, y - 1, iz, 'j') + if (differentStart) then + startingDesignation='j' + iz=iz-1 + else + startingDesignation='i' + end while iz >= z - (depth - 1) do digAt(x, y, iz, 'i') digAt(x - 1, y + 1, iz, 'i') @@ -663,15 +967,9 @@ def digKeupoStair(x, y, z, depth) end end -def getDigMode(digMode = 'd') - if ['d', 'u', 'j', 'i', 'h', 'r', 'x'].include? digMode then - return digMode - end - return 'd' -end - def drawPolygon(x0, y0, z0, x1, y1, z1, n = 3, apothem=false, digMode = 'd') - # if you dig a 2-gon (aka a line) it always passes through the origin so it's still convienient / useful + #Draw a polygon centered on origin with cursor at (apothem==T: midpoint of a side, ==F: vertex) + # if you dig a 2-gon (aka a line) it always passes through the origin so it's still convienient / useful. In apothem==T this makes the origin the midpoint of the drawn line. xOffset = x1 - x0 yOffset = y1 - y0 @@ -702,6 +1000,7 @@ def drawPolygon(x0, y0, z0, x1, y1, z1, n = 3, apothem=false, digMode = 'd') end def drawStar(x0, y0, z0, x1, y1, z1, n = 5, skip = 2, digMode = 'd') + #Draw a star centered at origin, with cursor at a vertex. xOffset = x1 - x0 yOffset = y1 - y0 @@ -725,59 +1024,60 @@ def drawStar(x0, y0, z0, x1, y1, z1, n = 5, skip = 2, digMode = 'd') end end -def dig2enum(digMode) - #this function turns a digmode into the appropriate enum for easier comparison on tile reading (eg floodfill.) - case digMode #from https://github.com/DFHack/scripts/blob/master/digfort.rb - when 'd'; return :Default - when 'u'; return :UpStair - when 'j'; return :DownStair - when 'i'; return :UpDownStair - when 'h'; return :Channel - when 'r'; return :Ramp - when 'x'; return :No - else - puts " Error: Unknown digtype" - throw :script_finished - end -end +def drawSpiral(x0, y0, z0, x1, y1, z1, coils, chord = 10,rotation=0, digMode = 'd') + # Draw a spiral centered at the origin, to a radius of the cursor. It makes turns, and if is >1, is made of points rather than a line.remaining + # Source: https://stackoverflow.com/questions/13894715/draw-equidistant-points-on-a-spiral -def enum2dig(digEnum) - #this function turns a designation enum into the appropriate digtype character - case digEnum #from https://github.com/DFHack/scripts/blob/master/digfort.rb - when :Default; return 'd' - when :UpStair; return 'u' - when :DownStair; return 'j' - when :UpDownStair; return 'i' - when :Channel; return 'h' - when :Ramp; return 'r' - when :No; return 'x' - else - puts " Error: Unknown digEnum" - throw :script_finished - end -end -def digPermitted(digMode, tileShape_basic) - #can we dig on this tile? - - if not tileShape_basic then return false end - - case digMode - when 'd'; if tileShape_basic == :Wall then return true else return false end - when 'u'; if tileShape_basic == :Wall then return true else return false end - when 'j'; if tileShape_basic == :Wall || tileShape_basic == :Floor then return true else return false end - when 'i'; if tileShape_basic == :Wall then return true else return false end - when 'h'; if tileShape_basic == :Wall || tileShape_basic == :Floor then return true else return false end - when 'r'; if tileShape_basic == :Wall then return true else return false end - when 'x'; return true - else - puts " Error: Unknown digtype" - throw :script_finished + + + # value of theta corresponding to end of last coil + thetaMax = coils * 2 * Math::PI + + xOffset = x1 - x0 + yOffset = y1 - y0 + + # ('0'=no rotation, '1'=360 degrees, '180/360'=180 degrees, <0=track cursor) + rotation=Math.atan2(yOffset,xOffset)+rotation*0.01745 #convert to radians + + + + radius = Math.sqrt(xOffset ** 2 + yOffset ** 2) + if (radius > 0.0) then + # How far to step away from center for each side. + awayStep = radius / thetaMax + + digAt(x0, y0, z0, digMode) + + # For every side, step around and away from center. + # start at the angle corresponding to a distance of chord + # away from centre. + theta = chord / awayStep + + while (theta.abs <= thetaMax.abs) + # How far away from center + away = awayStep * theta + + # How far around the center. + around = theta + rotation + + # Convert 'around' and 'away' to X and Y. + x = x0 + (Math.cos(around) * away).round + y = y0 + (Math.sin(around) * away).round + + digAt(x, y, z0, digMode) + + # to a first approximation, the points are on a circle + # so the angle between them is chord/radius + theta += chord / away end end +end def floodfill(x,y,z,targetDig, digMode, maxCounter= 10000) + #Flood fills out from the cursor until different designation reached (eg if on 'd' fill only 'd', if on empty, fill only empty). Rooks move adjacency only. + #targetDig: what designation type can we overwrite? #digMode: what designation are we placing? #maxCounter: a limit to help with performance. @@ -787,14 +1087,16 @@ def floodfill(x,y,z,targetDig, digMode, maxCounter= 10000) if not t then #ignore impossible tiles (eg air.) + stdout("Tile does not exist") throw :script_finished return end - digNum = dig2enum(digMode) #Stash this, we'll use it many times. + digNum = digMode2enum(digMode) #Stash this, we'll use it many times. if t.designation.dig == digNum then #don't dig tiles that are already dug + stdout("Tile is already dug") throw :script_finished return end @@ -802,53 +1104,113 @@ def floodfill(x,y,z,targetDig, digMode, maxCounter= 10000) #scan for next tile to dig. xStack = [x] yStack = [y] - + + checkedTiles=Hash.new #these keep us from rechecking tiles and such. + designatedTiles=Hash.new + + loopcounter=1 + loop do +# puts "Loop#: "+ loopcounter.to_s + loopcounter=loopcounter+1 + if xStack.length <=0 then + # puts "Stack empty." + break + end + x= xw = xe = xStack.pop() y = yStack.pop() #always push/pop x&y together. + +# puts "Checking: "+x.to_s+","+y.to_s + if checkedTiles[x.to_s+","+y.to_s] !=nil then + #puts x.to_s+","+y.to_s+" already checked" + next + else + #puts x.to_s+","+y.to_s+" added to checked list" + checkedTiles[x.to_s+","+y.to_s]=true + end #search W for bounds - loop do - xi = xw - 1 #move xw cursor west until it hits a match - t=df.map_tile_at(xi,y,z) - - if !t || xi == 0 || t.designation.dig != targetDig || !digPermitted(digMode,t.shape_basic) then - break + if checkedTiles[(x-1).to_s+","+y.to_s] == nil then + #don't scan west if we've already tried that from the tile to the left. + + loop do + xi = xw - 1 #move xw cursor west until it hits a match + t=df.map_tile_at(xi,y,z) + + if !t || xi == 0 || t.designation.dig != targetDig || !isDigPermitted(digMode,t.shape_basic) then + break + end + xw = xi end - xw = xi - end - - #search E for bounds - loop do - xi = xe + 1 #move xe cursor east until it hits a match - t=df.map_tile_at(xi,y,z) - - if !t || xi == 0 || t.designation.dig != targetDig || !digPermitted(digMode,t.shape_basic) then - break + + #search E for bounds + if checkedTiles[(x+1).to_s+","+y.to_s] == nil then + #don't scan east if we've already tried that from the tile to the left. + loop do + xi = xe + 1 #move xe cursor east until it hits a match + t=df.map_tile_at(xi,y,z) + + if !t || xi == 0 || t.designation.dig != targetDig || !isDigPermitted(digMode,t.shape_basic) then + break + end + xe = xi + end + + else + # puts "don't scan west: "+(x-1).to_s+","+y.to_s+" already checked." end - xe = xi + else + #puts "don't scan west: "+(x-1).to_s+","+y.to_s+" already checked." end #scan W..E filling, and checking N/S for xi in xw..xe do - digAt(xi, y, z, digMode) +# puts "scan: "+xi.to_s+","+y.to_s+" = "+designatedTiles[xi.to_s+","+y.to_s].to_s + if designatedTiles[xi.to_s+","+y.to_s] == nil then + counter = counter -1 #only decrease counter based on tiles dug, not tiles checked. + digAt(xi, y, z, digMode) + designatedTiles[xi.to_s+","+y.to_s]=true +# puts "Dig: "+xi.to_s+","+y.to_s + else +# puts "Nodig: "+xi.to_s+","+y.to_s+" ="+designatedTiles[xi.to_s+","+y.to_s].to_s+"/" + end - counter = counter -1 - if counter <=0 then - puts " Max coverage of #{maxCounter} tiles reached. Use multiple floods, or add a number for max coverage as 'digshape flood [max coverage] [dig type]'." + + if counter <=0 then + puts "Loop#: "+ loopcounter.to_s + stdout(" Max coverage of #{maxCounter} tiles reached. Use multiple floods, or add a number for max coverage as 'digshape flood [max coverage] [dig type]'.") + #stdout(" Automatically cancelling flood") + #undo() return end - + if loopcounter >=100000 then + puts "Loop#: "+ loopcounter.to_s + stdout(" Runaway flood algorithm, abort!") + undo() + stderr(" Automatically cancelling flood.") + return + end + + + #check N/S - t = df.map_tile_at(xi,y+1,z) - if t && t.designation.dig == targetDig && digPermitted(digMode,t.shape_basic) then - xStack.push(xi) - yStack.push(y+1) - end - t = df.map_tile_at(xi,y-1,z) - if t && t.designation.dig == targetDig && digPermitted(digMode,t.shape_basic) then - xStack.push(xi) - yStack.push(y-1) + if checkedTiles[(x).to_s+","+(y+1).to_s] == nil then + #don't recheck South + t = df.map_tile_at(xi,y+1,z) + if t && t.designation.dig == targetDig && isDigPermitted(digMode,t.shape_basic) then + #puts "push"+xi.to_s+" "+(y+1).to_s+" as row down" + xStack.push(xi) + yStack.push(y+1) + end + end + if checkedTiles[(x).to_s+","+(y-1).to_s] == nil then + #don't recheck north + t = df.map_tile_at(xi,y-1,z) + if t && t.designation.dig == targetDig && isDigPermitted(digMode,t.shape_basic) then + xStack.push(xi) + yStack.push(y-1) + end end end @@ -856,334 +1218,429 @@ def floodfill(x,y,z,targetDig, digMode, maxCounter= 10000) break end end + #puts "Loop#: "+ loopcounter.to_s end -# based on an algorithm in this stackoverflow question -# https://stackoverflow.com/questions/13894715/draw-equidistant-points-on-a-spiral -def drawSpiral(x0, y0, z0, x1, y1, z1, coils, chord = 10, digMode = 'd') - # ('0'=no rotation, '1'=360 degrees, '180/360'=180 degrees) - rotation = 0 - # value of theta corresponding to end of last coil - thetaMax = coils * 2 * Math::PI - xOffset = x1 - x0 - yOffset = y1 - y0 - radius = Math.sqrt(xOffset ** 2 + yOffset ** 2) - if (radius > 0.0) then - # How far to step away from center for each side. - awayStep = radius / thetaMax - digAt(x0, y0, z0, digMode) - # For every side, step around and away from center. - # start at the angle corresponding to a distance of chord - # away from centre. - theta = chord / awayStep +=begin +======================== SCRIPT CONTROL +script execution start +=end - while (theta.abs <= thetaMax.abs) - # How far away from center - away = awayStep * theta +if not $script_args[0] or $script_args[0]=="help" or $script_args[0]=="?" then - # How far around the center. - around = theta + rotation + stdout " To set origin: digshape origin" + stdout " To draw line after origin is set: digshape line" + stdout " To draw ellipse after origin is set (as bounding box): digshape ellipse " + stdout "..To draw an ellipse after origin is set (by major and minor axis): digshape major (must be horizontal or vertical), then digshape ellipse3p" + stdout " To draw a 3 point bezier curve after origin and major are set: digshape bez " + stdout "..To draw a circle after origin is set, select any point as a diameter: digshape circle2p " + stdout " To draw a polygon after origin is set (as center) with the cursor as a vertex: digshape polygon <# sides>" + stdout " To draw a polygon after origin is set (as center) with the cursor as a midpoint of a segment(apothem): digshape polygon <# sides> apothem" + stdout " To draw a star after origin is set (as center) with the cursor as a vertex : digshape star <# points> " + stdout "To draw an Archimedean spiral (coils - number of coils, chord - distance between points): digshape spiral " + stdout " To draw downstair: digshape downstair depth [differentStart?=true]" + stdout " " + stdout " To flood fill with a designation, overwriting ONLY the designation under the cursor (warning: slow on areas bigger than 10k tiles..): digshape flood [maxArea=10000]" + stdout " To undo the previous command (restoring designation): digshape undo" + stdout " To move all markers to the current z level (without displaying them): digshape resetz" + stdout " To swap the origin and cursor markers: digshape swap" + stdout " All commands accept a one letter digging designation [dujihrx] at the end, or will default to 'd'" + throw :script_finished +end - # Convert 'around' and 'away' to X and Y. - x = x0 + (Math.cos(around) * away).round - y = y0 + (Math.sin(around) * away).round +command = $script_args[0] +$script_args.delete_at(0) - digAt(x, y, z0, digMode) +if df.cursor.x == -30000 then + userSucks("Cursor must be on map") +end - # to a first approximation, the points are on a circle - # so the angle between them is chord/radius - theta += chord / away - end +if not (command == 'undo' or command=='u') and not $isPreviewOnly then + clearDigBuffer() #clear the dig buffer so we can undo the following command. Or initialize it's first run +end + +def requireOriginZLevel(msg: "Origin and target must be on the same z-level (use command 'digshape resetz' or 'digshape setz [Z-level, default=Cursor Z]' to fix)") + #Ensure cursor is on same z as origin (TODO: and control points). + if df.cursor.z != $origin.z then + userSucks(msg) end + #writeLuaPos("origin",$origin) # visualize them for the user end +def requireMajor(msg: "Set a point for the end of the major axis with the cursor and 'digshape major'") + #Ensure control point: 'major' has been set and is valid. + if $major == nil then + userSucks(msg) + end + requireOriginZLevel() + #writeLuaPos("origin", $origin) # visualize them for the user + #writeLuaPos("major", $major) # visualize them for the user +end -# script execution start +def getDigModeArgument(args) + #get next[LAST] script argument IFF it is a digmode designation, or set default if not present. + argument = args[0] + digMode = getDigMode(argument) + if not ['d', 'u', 'j', 'i', 'h', 'r', 'x'].include? digMode then + digMode='d' + else + args.delete_at(0) + end + return digMode +end -if not $script_args[0] or $script_args[0]=="help" or $script_args[0]=="?" then - puts " To draw downstair: digshape downstair depth" - puts " To set origin: digshape origin" - puts " To draw line after origin is set: digshape line" - puts " To draw ellipse after origin is set (as bounding box): digshape ellipse " - puts "..To draw an ellipse after origin is set (by major and minor axis): digshape major (must be horizontal or vertical), then digshape ellipse3p" - puts " To draw a 3 point bezier curve after origin and major are set: digshape bez " - puts "..To draw a circle after origin is set, select any point as a diameter: digshape circle2p " - puts " To draw a polygon after origin is set (as center) with the cursor as a vertex: digshape polygon <# sides>" - puts " To draw a polygon after origin is set (as center) with the cursor as a midpoint of a segment(apothem): digshape polygon <# sides> apothem" - puts " To draw a star after origin is set (as center) with the cursor as a vertex : digshape star <# points> " - puts "To draw an Archimedean spiral (coils - number of coils, chord - distance between points): - digshape spiral " - puts " To flood fill with a designation, overwriting ONLY the designation under the cursor (warning: slow on areas bigger than 10k tiles..): digshape flood [maxArea=10000]" - puts " To undo the previous command (restoring designation): digshape undo" - puts " To move all markers to the current z level (without displaying them): digshape resetz" - puts " All commands accept a one letter digging designation [dujihrx] at the end, or will default to 'd'" - throw :script_finished + +def getFilledArgument(args, default: false) + #get next script argument IFF it is fill. + # this doesn't *expect* and argument and so only consumes an argument when something matches + argument = args[0] + + case argument + when 'filled', 'f', 'true', 'yes', 'y'; filled = true + when 'hollow', 'h', 'false', 'no', 'n'; filled = false + else + return default # doesn't consume if nothing matches + end + args.delete_at(0); + return filled end -command = $script_args[0] -argument1 = $script_args[1] -argument2 = $script_args[2] -argument3 = $script_args[3] +def getFloatArgument(args, default: nil, type: "(unnamed number)", positive: true) + #get next script argument, which must be a float. + num = args[0] + result = nil + defaultMessage = "" -if df.cursor.x == -30000 then - puts " Error: cursor must be on map" - throw :script_finished + if default != nil then + defaultMessage = "Use `-' for the default value (#{default})" + end + + if not num then + userSucks("Must supply #{type} parameter (number).#{defaultMessage}") + end + args.delete_at(0) + + case num + when 'default','-'; + userSucks("No default value for #{type} parameter!") if default == nil + result = default + else + result = Float(num) rescue userSucks("Malformed number for "+type+" parameter, got `"+num+"'.#{defaultMessage}") + end + + if positive && result<0 then + userSucks("Expected positive number for #{type} parameter, got `#{num}'.#{defaultMessage}") + end + + return result +end + +def getIntegerArgument(args, default: nil, type: "(unnamed integer)", positive: true) + #get next script argument, which must be an integer. + num = args[0] + result = nil + defaultMessage = "" + + if default != nil then + defaultMessage = "Use `-' for the default value (#{default})" + end + + if not num then + userSucks("Must supply #{type} parameter (integer).#{defaultMessage}") + end + args.delete_at(0) + + case num + when 'default','-'; + userSucks("No default value for #{type} parameter!") if default == nil + result = default + else + result = Integer(num) rescue userSucks("Malformed integer for "+type+" parameter, got `"+num+"'.#{defaultMessage}") + end + + if positive && result<0 then + userSucks("Expected positive integer for #{type} parameter, got `#{num}'.#{defaultMessage}") + end + + return result +end + +def getBooleanArgument(args, default: nil, type: "(unnamed integer)") + #get next script argument, which must be an integer. + num = args[0] + result = nil + defaultMessage = "" + + if default != nil then + defaultMessage = "Use `-' for the default value (#{default})" + end + + if not num then + userSucks("Must supply #{type} parameter (boolean).#{defaultMessage}") + end + args.delete_at(0) + + case num + when 'default','-'; + userSucks("No default value for #{type} parameter!") if default == nil + result = default + when 'true', true; + result=true + when 'false',false; + return false + else + result = num==true rescue userSucks("Malformed boolean for "+type+" parameter, got `"+num+"'.#{defaultMessage}") + end + return result end -if command=="o" or command=="set" then #alias - command="origin" -elsif command=="keupo" or command=="stairs" or command=="downstairs" then - command="downstair" +def makeDefaultPosMap(oldPos) + return { + '~' => cursorAsDigPos(), # cursor positon + '-' => oldPos.clone() # old value of this (i.e. leave it unchanged) + } end -if command != 'undo' then - clearDigBuffer() #clear the dig buffer so we can undo the following command. Or initialize it's first run +def getPosComponentArgument(args, defaultMap, symbol) + if defaultMap[args[0]] then # it is either ~ or - + value = defaultMap[args[0]][symbol] + args.delete_at(0) + return value + else # it is an integer + return getIntegerArgument(args, type: "#{symbol.to_s} coordinate", positive: true) + end +end + +def getPositionArgument(args, oldPos, default: nil) # X Y Z, ~ ~ ~, - - -, or any mix # returns nil if no default! + if args[0] && args[1] && args[2] then + defaultMap = makeDefaultPosMap(oldPos) + return DigPos.new(getPosComponentArgument(args, defaultMap, :x), + getPosComponentArgument(args, defaultMap, :y), + getPosComponentArgument(args, defaultMap, :z)) + else + return default + end end +def noMoreArguments(args) + if args[0] then + userSucks("Did not expect more arguments #{args}") + end +end + + + + + +=begin +======================== DIGSHAPE COMMANDS +=end + + +#def registerCommand(name, aliases, usage) +# return "TODO:" +#end + + case command - when 'origin' - $originx = df.cursor.x - $originy = df.cursor.y - $originz = df.cursor.z - - markOrigin($originx, $originy, $originz) - when 'resetz' - $originz = $majorz = df.cursor.z - when 'line' - dig = getDigMode(argument1) - - if df.cursor.z == $originz then - drawLine($originx, $originy, $originz, df.cursor.x, df.cursor.y, df.cursor.z, dig) - else - puts " Error: origin and target must be on the same z level" - throw :script_finished - end - when 'ellipse' #digshape ellipse [filled] [digmode] - filled = false - case argument1 - when 'filled'; filled = true - when 'hollow'; filled = false - when 'true'; filled = true - when 'false'; filled = false - when 't'; filled = true - when 'f'; filled = false - when 'y'; filled = true - when 'n'; filled = false - end + when 'origin', 'o', 'set' + # $usage = createUsage(name: 'origin', aliases: ['o', 'set']) # TODO ADD USAGES TO EACH COMMANDS, make scriptError/userSucks print them out + # Even better, to refator all these commands with associated data into classes / anonoymous functions so we can eventually do digshape help + newOrigin = getPositionArgument($script_args, $origin, default: cursorAsDigPos()) + noMoreArguments($script_args) - dig = getDigMode(argument1) #check argument 1 for dig instructions - if argument2 then # if argument 2 is present, look at that for dig instructions - dig = getDigMode(argument2) - end + setOrigin(newOrigin.x, newOrigin.y, newOrigin.z) # need to refactor setOrigin - if df.cursor.z == $originz then - drawEllipse($originx, $originy, $originz, df.cursor.x, df.cursor.y, df.cursor.z, x2=nil, y2=nil, z2=nil, filled=filled, digMode=dig, mode = 'bbox') + #writeLuaPos("origin", $origin) - # remove origin designation - digAt($originx, $originy, $originz, 'x') - else - puts " Error: origin and target must be on the same z level" - throw :script_finished - end - when 'circle2p' #digshape circle2p [filled] [digmode] - filled = false - case argument1 - when 'filled'; filled = true - when 'hollow'; filled = false - when 'true'; filled = true - when 'false'; filled = false - when 't'; filled = true - when 'f'; filled = false - when 'y'; filled = true - when 'n'; filled = false - end + when 'major', 'm', 'a' #used to mark the end point of the major diameter + newMajor = getPositionArgument($script_args, $major, default: cursorAsDigPos()) + noMoreArguments($script_args) + + requireOriginZLevel() + + setMajor(newMajor.x, newMajor.y, newMajor.z) # need to refactor setMajor - dig = getDigMode(argument1) #check argument 1 for dig instructions - if argument2 then # if argument 2 is present, look at that for dig instructions - dig = getDigMode(argument2) - end + #writeLuaPos("major", $major) - if df.cursor.z == $originz then - drawEllipse($originx, $originy, $originz, df.cursor.x, df.cursor.y, df.cursor.z, x2=nil, y2=nil, z2=nil, filled=filled, digMode = dig, mode = 'diameter') - else - puts " Error: origin and target must be on the same z level" - throw :script_finished + stdout("Now move the cursor to the minor axis radius (extent) and call ellipse3p") + + when 'resetz', 'setz' + z = df.cursor.z # default + +# if args[0] then +# z = getPosComponentArgument(args, makeDefaultPosMap(origin), :z) # only really need the z-component from origin for default +# end +# noMoreArguments($script_args) + + setOrigin($origin.x, $origin.y, z) + if $major then + setMajor($major.x, $major.y, z) end - when 'major' #digshape major - #used to mark the end point of the major diameter - #$major = df.cursor - $majorx = df.cursor.x - $majory = df.cursor.y - $majorz = df.cursor.z - if df.cursor.z == $originz then - markOrigin($majorx, $majory, $majorz) - puts " Now move the cursor to the minor axis radius (extent) and call ellipse3p" + when 'swap' + stdout("origin: #{$origin != nil ? $origin.to_s : ''}") + stdout("major : #{$major != nil ? $major.to_s : ''}") + stdout("cursor: #{cursorAsDigPos().to_s}") + stdout("---swap---") + + temp=$origin + $origin=cursorAsDigPos() + df.cursor.x=temp.x + df.cursor.y=temp.y + df.cursor.z=temp.z + + stdout("origin: #{$origin != nil ? $origin.to_s : ''}") + stdout("major : #{$major != nil ? $major.to_s : ''}") + stdout("cursor: #{cursorAsDigPos().to_s}") + + + when 'ls', 'status' + if $isLuaMode then + writeLuaPos("origin", $origin) + writeLuaPos("major", $major) + if $digBufferX.length >0 then + puts "Undo Buffer exists" + end + throw :script_finished else - puts " Error: origin and target must be on the same z level" - throw :script_finished + stdout("origin: #{$origin != nil ? $origin.to_s : ''}") + stdout("major : #{$major != nil ? $major.to_s : ''}") + stdout("cursor: #{cursorAsDigPos().to_s}") end - when 'ellipse3p' #digshape ellipse3p [filled] [digmode] - if df.cursor.z == $originz then - if $majorx == nil then - puts " Error: Set a point for the end of the major axis with the cursor and 'digshape major'" - throw :script_finished - end - - filled = false - case argument1 - when 'filled'; filled = true - when 'hollow'; filled = false - when 'true'; filled = true - when 'false'; filled = false - when 't'; filled = true - when 'f'; filled = false - when 'y'; filled = true - when 'n'; filled = false - end - - if filled then - puts " Filled not yet supported for 3p ellipses." - filled = false - end + + when 'line', 'l' + digMode = getDigModeArgument($script_args) + noMoreArguments($script_args) - dig = getDigMode(argument1) #check argument 1 for dig instructions - if argument2 then # if argument 2 is present, look at that for dig instructions - dig = getDigMode(argument2) - end + requireOriginZLevel() - drawEllipse($originx, $originy, $originz, $majorx, $majory, $majorz, df.cursor.x, df.cursor.y, df.cursor.z, filled = filled, digMode = dig, mode = 'axis') - else - puts " Error: all control points must be on the same z level" - throw :script_finished + drawLine($origin.x, $origin.y, $origin.z, df.cursor.x, df.cursor.y, df.cursor.z, digMode) + + when 'ellipse', 'e' #digshape ellipse [filled] [digmode] + + filled = getFilledArgument($script_args) + digMode = getDigModeArgument($script_args) + noMoreArguments($script_args) + requireOriginZLevel() + + drawEllipse($origin.x, $origin.y, $origin.z, df.cursor.x, df.cursor.y, df.cursor.z, x2=nil, y2=nil, z2=nil, filled=filled, digMode=digMode, mode='bbox') # fixme: default arguments should be colon not equals + + when 'circle2p', 'circle', 'c','c2' #digshape circle2p [filled] [digmode] + filled = getFilledArgument($script_args) + digMode = getDigModeArgument($script_args) + noMoreArguments($script_args) + + requireOriginZLevel() + + drawEllipse($origin.x, $origin.y, $origin.z, df.cursor.x, df.cursor.y, df.cursor.z, x2=nil, y2=nil, z2=nil, filled=filled, digMode=digMode, mode='diameter') # fixme: default arguments should be colon not equals + + when 'ellipse3p', 'e3p','e3' #digshape ellipse3p [filled] [digmode] + filled = getFilledArgument($script_args) + digMode = getDigModeArgument($script_args) + noMoreArguments($script_args) + + requireOriginZLevel(msg:"All control points must be on the same z level") + requireMajor() + + if filled then + stdout("Filled not yet supported for 3p ellipses.") + filled = false end - when 'bez' #digshape bez + + drawEllipse($origin.x, $origin.y, $origin.z, $major.x, $major.y, $major.z, df.cursor.x, df.cursor.y, df.cursor.z, filled=filled, digMode=digMode, mode='axis') # fixme: default arguments should be colon not equals + + when 'bezier', 'bez', 'b', 'curve' #digshape bezier [weight] digmode] #use origin and major as endpoints, cursor as curve shaper - if df.cursor.z == $originz then - if $majorx == nil then - puts " Error: Set an endpoint for the curve with the cursor and 'digshape major'" - throw :script_finished - end - - weight = argument1.to_f - if weight = 0 then - weight = 1.5 - end - - dig = getDigMode(argument1) #check argument 1 for dig instructions - if argument2 then # if argument 2 is present, look at that for dig instructions - dig = getDigMode(argument2) - end - - plotQuadRationalBezier($originx, $originy, $originz, df.cursor.x, df.cursor.y, df.cursor.z, $majorx, $majory, $majorz, weight, dig) - else - puts " Error: all control points must be on the same z level" - throw :script_finished - end - when 'polygon' - if not argument1 then - puts " Must supply a polygon n-sides parameter" - throw :script_finished - else - n = argument1.to_i - dig = getDigMode(argument2) - if argument3 then - dig = getDigMode(argument3) - end - apothem=false; - case argument2 - when 'apothem'; apothem=true - when 'radius'; apothem=false - when 'a'; apothem=true - when 'r'; apothem=false - when 't'; apothem=true - when 'f'; apothem=false - when 'y'; apothem=true - when 'n'; apothem=false - end - if df.cursor.z == $originz then - drawPolygon($originx, $originy, $originz, df.cursor.x, df.cursor.y, df.cursor.z, n, apothem, dig) - else - puts " Error: origin and target must be on the same z level" - throw :script_finished - end - end - when 'star' # star N [SKIP=2] [DIGMODE] - if not argument1 then - puts " Must supply a star n-sides parameter" - throw :script_finished - else - dig = getDigMode(argument2) - if argument3 then - dig = getDigMode(argument3) - end - n = argument1.to_i - skip = Integer(argument2) rescue 2 + weight = getFloatArgument($script_args, default: 1.5, type: "bezier weight") + digMode = getDigModeArgument($script_args) #check argument 1 for dig instructions + noMoreArguments($script_args) + + requireOriginZLevel(msg:"All control points must be on the same z level") + requireMajor() + + plotQuadRationalBezier($origin.x, $origin.y, $origin.z, df.cursor.x, df.cursor.y, df.cursor.z, $major.x, $major.y, $major.z, weight, digMode) - if df.cursor.z == $originz then - drawStar($originx, $originy, $originz, df.cursor.x, df.cursor.y, df.cursor.z, n, skip, dig) - else - puts " Error: origin and target must be on the same z level" - throw :script_finished - end + when 'polygon', 'p' #digshape polygon [sides] [apothem/radius] [digMode] # apothem is default + sides = getIntegerArgument($script_args, type: "polygon n-sides") + + apothem = false # custom argument parse + case $script_args[0] + when 'apothem', 'a', 't', 'true', 'y', 'yes'; apothem=true; $script_args.delete_at(0) + when 'radius', 'r', 'f', 'false', 'n', 'no'; apothem=false; $script_args.delete_at(0) end - when 'downstair' - if not argument1 then - puts " Must supply a depth parameter" - throw :script_finished - else - depth = argument1.to_i - if depth <= 0 then - puts " Depth must be an integer greater than zero" - throw :script_finished - else - digKeupoStair(df.cursor.x, df.cursor.y, df.cursor.z, depth) - end + + digMode = getDigModeArgument($script_args) + noMoreArguments($script_args) + + requireOriginZLevel() + + drawPolygon($origin.x, $origin.y, $origin.z, df.cursor.x, df.cursor.y, df.cursor.z, sides, apothem, digMode) + + when 'star', 's' #digshape star N [skip=2] [digMode] + n = getIntegerArgument($script_args, type: "star n-sides") + skip = getIntegerArgument($script_args, default: 2, type: "skip") + digMode = getDigModeArgument($script_args) + noMoreArguments($script_args) + + requireOriginZLevel() + + drawStar($origin.x, $origin.y, $origin.z, df.cursor.x, df.cursor.y, df.cursor.z, n, skip, digMode) + + when 'spiral' + coils=getIntegerArgument($script_args, default: 2, type: "number of coils") + chord=getIntegerArgument($script_args, default: 1, type: "distance between points") + rotate = getIntegerArgument($script_args, default: 0, type: "rotate", positive: false) + digMode = getDigModeArgument($script_args) + noMoreArguments($script_args) + + requireOriginZLevel() + + drawSpiral($origin.x, $origin.y, $origin.z, df.cursor.x, df.cursor.y, df.cursor.x, coils, chord, rotate, digMode) + # todo: spiral with negative turns should twist other way. + + when 'keupo', 'stairs', 'downstairs', 'downstair' #digshape keupo depth + depth = getIntegerArgument($script_args, type: "depth") + start = getBooleanArgument($script_args, type: "different Start?", default: true) + noMoreArguments($script_args) + + if depth <= 0 then + userSucks("Depth must be an integer greater than zero") end - when 'flood' - maxArea = argument1.to_i - if maxArea == 0 then maxArea = 10000 end - dig = getDigMode(argument1) #check argument 1 for dig instructions - if argument2 then # if argument 2 is present, look at that for dig instructions - dig = getDigMode(argument2) - end + digKeupoStair(df.cursor.x, df.cursor.y, df.cursor.z, depth,start) + + when 'flood', 'f' + stdout("cursor: #{cursorAsDigPos().to_s}") + maxArea = getIntegerArgument($script_args, default: 10000, type: "maximum flood area") + digMode = getDigModeArgument($script_args) + + noMoreArguments($script_args) - t=df.map_tile_at(df.cursor.x, df.cursor.y, df.cursor.z) - targetDig = t.designation.dig #we will only fill the designation type under the cursor. + tile = df.map_tile_at(df.cursor.x, df.cursor.y, df.cursor.z) + targetDig = tile.designation.dig #we will only fill the designation type under the cursor. - if not t.designation.dig==:No then - if dig2enum(dig) == t.designation.dig then - puts " Error: floodfill must be centered on an undesignated/matching tile." - throw :script_finished + if targetDig != :No then + if targetDig == digMode2enum(digMode) then + userSucks("Floodfill must be centered on an undesignated/matching tile.") end end - floodfill(df.cursor.x, df.cursor.y, df.cursor.z, targetDig, dig, maxArea) - throw :script_finished - when 'undo' - unDig() - when 'spiral' - if not argument1 then - puts " Must supply a coils parameter" - throw :script_finished - else - coils = argument1.to_i - chord = 2 - if argument2 then - chord = argument2.to_i - end + floodfill(df.cursor.x, df.cursor.y, df.cursor.z, targetDig, digMode, maxArea) - dig = getDigMode(argument3) - - drawSpiral($originx, $originy, $originz, df.cursor.x, df.cursor.y, df.cursor.x, coils, chord, dig) - end + when 'undo', 'u' + undo() + else - puts " Error: Invalid command" - throw :script_finished + userSucks("Invalid command") end + +#Report back to lua our status. +writeLuaPos("origin", $origin) +writeLuaPos("major", $major) +writeLuaPos("cursor", cursorAsDigPos()) #lua doesn't need this, but it is handy for dev for it to be printed at the same time. \ No newline at end of file diff --git a/gui/digshape.lua b/gui/digshape.lua new file mode 100644 index 0000000..41bc36e --- /dev/null +++ b/gui/digshape.lua @@ -0,0 +1,1518 @@ +--gui front-end for digshape.rb, a geometric designations generating tool + +--[====[ +=========== +gui front-end for digshape.rb, a geometric designations generating tool + +--todo: mousemode. mouse: click1 set origin; click1 again: set cursor, commmit +]====] + +verbose = false --TODO: move these down later to be arguments +DigshapeUIversion = "20210402b" +--dfhack.screen.invalidate() --force an immediate redraw. +--TODO: use dfhack.print(args...) for better printing + +function shallowcopy(orig) + --tired of trying to figure out when lua will alter the wrong thing. From http://lua-users.org/wiki/CopyTable + local orig_type = type(orig) + local copy + if orig_type == 'table' then + copy = {} + for orig_key, orig_value in pairs(orig) do + copy[orig_key] = orig_value + end + else + -- number, string, boolean, etc + copy = orig + end + return copy +end + +function printtable(table, note, recursecount) + --pretty print a table, adds $note and source line number as a header + --printall_recurse(obj)..sigh. Why couldn't I find this when I went looking for it. + if not verbose then + return + end + local prefix = note or "|" + local recursecount = recursecount or 5 --how deep down into the nested tables are we allowed? hack circular protection, plus it's all we really need. + local title = "" + if not string.match(prefix, "^ *|$") then + title = " " .. prefix .. " " + prefix = "|" + end + + if recursecount <= 0 then + print(prefix .. "-->> MAX DEPTH <<--") + return + end + + if prefix == "|" then + dfhack.color(COLOR_LIGHTGREEN)--reset colour + dfhack.println("=PRINT=TABLE=" .. title .. "============ <>") + dfhack.color(nil)--reset colour + end + if type(table) ~= "table" then + print("->>" .. prefix .. table) + return + end + print(string.gsub(prefix, "|", "") .. "@>-----------------@") + for k, v in pairs(table) do + if type(v) ~= "table" then + print(prefix .. "[" .. k .. "]: <" .. tostring(v) .. ">") + else + print(prefix .. "[" .. k .. "]: ")--.. ":") + printtable(v, " " .. prefix, recursecount - 1) --indent prefix and recurse. Yes, no sanity checking, user knows when to use, not for production. + end + end + print(string.gsub(prefix, "|", "") .. "@<-----------------@") + dfhack.color(nil)--reset colour +end + +--[[ +=======Useful dfhack tidbits======== +"kill-lua": kills running lua scripts, opt: "force" +"devel/pop-screen": exit an active gui script +"devel/clear-script-env SCRIPTNAME" +"lua onelinescript........": run a lua command directly. + ----"lua _G.digshape-gui_saved_options=nil": clears digshapegui's special save +"devel/click-monitor start|stop" : prints coordinates of mouse clicks to console + +=====Console color constants +COLOR_RESET = -1 +COLOR_BLACK = 0 +COLOR_BLUE = 1 +COLOR_GREEN = 2 +COLOR_CYAN = 3 +COLOR_RED = 4 +COLOR_MAGENTA = 5 +COLOR_BROWN = 6 +COLOR_GREY = 7 +COLOR_DARKGREY = 8 +COLOR_LIGHTBLUE = 9 +COLOR_LIGHTGREEN = 10 +COLOR_LIGHTCYAN = 11 +COLOR_LIGHTRED = 12 +COLOR_LIGHTMAGENTA = 13 +COLOR_YELLOW = 14 +COLOR_WHITE = 15 + +====GUI stuff +Lua api: https://docs.dfhack.org/en/stable/docs/Lua%20API.html# +1st half of GUI module: https://docs.dfhack.org/en/stable/docs/Lua%20API.html#gui-module +2nd half of GUI module: https://docs.dfhack.org/en/stable/docs/Lua%20API.html#screen-api +3rd half of GUI module: https://docs.dfhack.org/en/stable/docs/Lua%20API.html#in-game-ui-library +Painter: https://docs.dfhack.org/en/stable/docs/Lua%20API.html#painter-class +Widgets: https://docs.dfhack.org/en/stable/docs/Lua%20API.html#gui-widgets +See also: dfhack.lua, class.lua, dwarfmode.lua, gui.lua, widgets.lua, dialogs.lua as the html helpfiles do not adaquately describe the functionality. + +====Label widget: +Label.ATTRS{ + text_pen = COLOR_WHITE, + text_dpen = COLOR_DARKGREY, -- disabled + text_hpen = DEFAULT_NIL, -- highlight - default is text_pen with reversed brightness + disabled = DEFAULT_NIL, + enabled = DEFAULT_NIL, + auto_height = true, + auto_width = false, + on_click = DEFAULT_NIL, + on_rclick = DEFAULT_NIL, +} +--]] + +local utils = require "utils" +local gui = require "gui" +local guidm = require "gui.dwarfmode" +local dialog = require "gui.dialogs" +local widgets = require 'gui.widgets' + +stdout = function(msgtype, ...) + if type(msgtype) == "string" then + local _, _, temp = string.find(msgtype, "^(...)$") + if temp == "OUT" then + --only print the final output + dfhack.color(COLOR_WHITE) + dfhack.print(msgtype:sub(4)) + dfhack.println(...) + dfhack.color(nil) + end + end +end--silently discard everything but output. + +if verbose then + --atm verbose must be set manually, see line2 of this script. + dfhack.console.clear() + stdout = function(msgtype, ...) + --A stupid pretty console print command. + local prefix = "DS/UI" .. "(" .. debug.getinfo(2).currentline .. "): " + if type(msgtype) == "string" then + local _, _, temp = string.find(msgtype, "^(...)$") + if temp == "ERR" or temp == "MSG" or temp == "WRN" or temp == "OUT" or temp == "CMD" or temp == "RBY" or temp == ">>>" or temp == "CAL" then + --output channels: MSG/WRN/ERR: information, OUT: results, RBY: digshape output passed through. + if temp == "IGNORE" or temp == "ALSOIGNORE" or temp == "RjBY" then + return --don't print + end + local color = { RBY = COLOR_DARKGREY, MSG = COLOR_WHITE, WRN = COLOR_YELLOW, ERR = COLOR_LIGHTRED, CMD = COLOR_LIGHTCYAN, OUT = COLOR_MAGENTA, [">>>"] = COLOR_LIGHTMAGENTA, CAL = COLOR_BROWN } + dfhack.color(color[temp]) + + if verbose == false and temp ~= "ERR" then + return --only print errors when not verbose + end + + --[[ if temp=="CAL" then + temp= + if temp ~=nil then + msgtype=msgtype.."@ "..temp + + end + end]] + + prefix = prefix .. msgtype .. ": " + msgtype = "" + --else + -- prefix = " DS: " + end + + end + dfhack.print(prefix .. msgtype)-- + dfhack.println(...) + dfhack.color(nil)--reset colour + end + --TOOD: fancy! https://www.lua.org/pil/6.html +else + -- print=nil --suckers, don't print anything. +end + +--stdout("gui/Digshape verbose output"," ".."!") +--stdout("ERR", "gui/Digshape verbose output"," ".."!") +--stdout("ERR", "A", "B", "C","D".."D") +--stdout("A", "B", "C","D".."D") + +--[[ TODO: split DigshapeUI into UI and I: +DigshapeInterface = defclass(DigshapeInterface) --a class to hold the digshape specific stuff, the translation and parsing stuff, rather than the UI/display stuff +This may get us a readOnly attr set for each command/command specific persistance if we make a new copy of this for each command. + +DigshapeInterface.ATTRS {} +DigshapeInterface:nameCommand(string)--extract the name command (eg "digshape lua preview ellipse bbox hollow d" -> "ellipse"), should this check the commandBase arg to translate? +DigshapeInterface:parseCommand(string) -- strip any UI only args, put into form that digshape will accept. Add "digshape lua", "preview" or not should be done by UI. +DigshapeInterface:runDigshapeCommand() +]] + + + +DigshapeUI = defclass(DigshapeUI, guidm.MenuOverlay) + +DigshapeUI.ATTRS { + state = "preview", + activeDesgination = 'd', + activeCommand = { name = "spiral", args = {}, digshapeArgs = { fill = "NA", digmode = "@", mode = "designating" }, digshapeString = "" }, + resetActiveCommand = function() + return { name = "spiral", args = {}, digshapeArgs = { fill = "NA", digmode = "@", mode = "designating" }, digshapeString = "" } + end, + --currentCommand = 'spiral', --TODO: replace this with self.activeCommand.name + --parsedCommand = DEFAULT_NIL, --TODO: move this to self.activeCommand.name.parsedCommand + + --currentOutput = {}, + --currentError = {}, + --currentDig = {}, + origin = nil, + major = nil, + autosetZtoCurrent = true, + -- default properties for self here + + --Preview mode variables + blink = false, --should the preview blink? + blinkrate = { 3, 750, 350, 125 }, --how fast do we blink. blinkrate[1] is the index of the chosen rate. + pens = { + --{key, symbol character code, fgcolor, bgcolor} + --https://docs.dfhack.org/en/stable/docs/Lua%20API.html#pen-api + + --tiles and map icons + origin = dfhack.pen.make({ ch = '+', fg = COLOR_CYAN, bg = COLOR_LIGHTGREEN }), + cursor = dfhack.pen.make({ ch = 'X', fg = COLOR_CYAN, bg = COLOR_BLACK }), + ctrl_A = dfhack.pen.make({ ch = 'a', fg = COLOR_CYAN, bg = COLOR_LIGHTCYAN }), + designation = dfhack.pen.make({ fg = COLOR_BROWN, bg = COLOR_YELLOW }), + mark = dfhack.pen.make({ fg = COLOR_YELLOW, bg = COLOR_LIGHTCYAN }), + clear = dfhack.pen.make({ fg = COLOR_BROWN, bg = COLOR_RED }), + + --digmode menu + digMode = { + selected = dfhack.pen.make({ fg = COLOR_BLACK, bg = COLOR_LIGHTGREEN }), + deselected = dfhack.pen.make({ fg = COLOR_LIGHTGREEN, bg = COLOR_BLACK }), + delete = dfhack.pen.make({ fg = COLOR_BLACK, bg = COLOR_LIGHTRED }), + mark = dfhack.pen.make({ fg = COLOR_BLACK, bg = COLOR_LIGHTCYAN }), + }, + + --other menu + enabledMenu = dfhack.pen.make({ fg = COLOR_GREY, bg = COLOR_BLACK }), --visible and interactable + disabledMenu = CLEAR_PEN, --visible but locked + alertMenu = dfhack.pen.make({ fg = COLOR_DARKGREY, bg = COLOR_RED }), --visible and redbackground for required input + }, + + --Designation mode variables (to be saved between commands) + -- designateDigMode = {}, --what is the UI selected digMode? + --designateMarking = false, --are we designating "marking" rather than standard? + --designateFilled = false, --filled or hollow? (will be ignored if current command does not allow/use it) + + + + digButtons = { + --, keypen={selected=self.pens.digMode.,deselected=self.pens.digMode.,}}, + d = { key = 'd', symbol = " ", text = "Mine", }, + i = { key = 'i', symbol = "X", text = "U/D Stair" }, + h = { key = 'h', symbol = "_", text = "Channel" }, + r = { key = 'r', symbol = 30, text = "Up Ramp" }, + j = { key = 'j', symbol = ">", text = "Down Stair" }, + u = { key = 'u', symbol = "<", text = "Up Stair" }, + x = { key = 'x', symbol = " ", text = "Remove Designation" }, + M = { key = 'M', symbol = " ", text = "Toggle Dig/Mark" }, + order = "_dihrjuxM_", --we need to know the order so we can insert/remove cursor markers in the designation list. It's a hack, sorry. + }, + + + --Digshape command reference + digshapeCommands = { + --[[ + --This table records the names, requiremens, and arguments for each digshape command. + + --todo: make this read only so we can reset to defaults. Dunno. Maybe https://www.lua.org/pil/13.4.5.html? maybe there's a idiomatic way. + + --requireOrigin[bool]: do we need the origin to be set? + --requireMajor[bool]: " controlPointA (major) set? + --requireZ[bool]: " cursor/view on same z as origin/controlPoints? + --allowFilled[bool]: Can this command accept filled/hollow? (eg. "line" has no volume and so cannot be either). + --runSilent[bool]: should this command always be run silent? --TODO: I don't use this yet, and don't quite remember why I thought we needed it. + --digMode [string OR nil]: what the digMode is allowed to be. "@": any, "nil": none, "[character]": this/these and only these. + --desc: Short description of this command, used as in-game help. + --args [{{}{}...{}} OR nil]: details on the arguments to this command. If no args accepted, args=nil + -- --args element[{}]: details on one argument to a digshape command + -- -- --name[string]: name for this argument, used as in-game display + -- -- --required[bool]: does digshape require we pass this arg? Almost always true atm. + -- -- --desc[string]: short description of what this argument controls, used for in-game help. + -- -- --default[any valid OR nil OR {}]: the default value for this arg if not supplied by user. If a {}, then should contain validity checking: + -- -- -- --default=[any]: the actual default value, only if args.default={} + -- -- -- --min=[numeric]: lower (inclusive) bound on value. If not present, then value can be lowered to min(type) + -- -- -- --max=[numeric]: upper (inclusive) bound on value. If not present, then value can be raised to max(type) + -- -- -- --values=[{} OR string]: ordered list of ONLY acceptable values. If a string, treated as lua String.Pattern that must match entire value. + -- -- -- --type=[string: "int, float, bool, string, pos, luatable"]: what data type is acceptable, only one type is allowed. + -- -- -- --inc=[numeric]: what is the default +- amount to change this value by when adjusted by GUI. If values={}, increment must be int,and is the number of indicies to advance(% len). TODO: SHIFT-inc: *2, CTRL-inc: *5, ALT-inc: /10; modifiers stack. + --]] + + --COMMAND={requireOrigin = false, requireMajor = false, requireZ = false, allowFilled = false, args = {name="",required=true,default=nil,type="",desc=""}, runSilent = true, digMode = nil, desc = "Set the origin"}, + + origin = { requireOrigin = false, requireMajor = false, requireZ = false, allowFilled = false, args = nil, runSilent = true, digMode = nil, desc = "Set the origin" }, + controla = { requireOrigin = true, requireMajor = false, requireZ = true, allowFilled = true, args = nil, runSilent = false, digMode = "@", desc = "Set the first control point (A, or 'major')" }, + swap = { requireOrigin = true, requireMajor = false, requireZ = true, allowFilled = true, args = nil, runSilent = false, digMode = "@", desc = "Swap the origin and cursor" }, + + allCommands = { requireOrigin = true, requireMajor = false, requireZ = false, allowFilled = false, args = { "command", default = "circle", values = { "circle", "line", "curve", "ellipse", "star", "polygon", "spiral" }, commandBase = { "circle", "line", "curve", "ellipse", "star", "polygon", "spiral" }, type = "string", runSilent = false, digMode = "@", desc = "Command selection" }, aliases = { "all" } }, + + circle = { requireOrigin = true, requireMajor = false, requireZ = true, allowFilled = true, args = nil, runSilent = false, digMode = "@", desc = "Draw a circle", aliases = { "c", "c2", "circle2p" } }, + + line = { requireOrigin = true, requireMajor = false, requireZ = true, allowFilled = false, args = nil, runSilent = false, digMode = "@", desc = "Draw a line", aliases = { "l", "ray" } }, + + ellipse = { requireOrigin = true, requireMajor = false, requireZ = true, allowFilled = false, args = { { name = "mode", required = true, default = { default = "bbox", values = { "bbox", "2axis" }, commandBase = { "ellipse", "ellipse3p" }, inc = 1 }, type = "string", desc = "Method of layout", guiOnlyArg = true } }, runSilent = false, digMode = "@", desc = "Draw a ellipse", aliases = { "e" } }, --default.commandBase is used to call different commands when the mode argument is cycled. Used in runCurrentCommand() + + ellipse3p = { requireOrigin = true, requireMajor = true, requireZ = true, allowFilled = false, args = { { name = "mode", required = true, default = { default = "2axis", values = { "bbox", "2axis" }, commandBase = { "ellipse", "ellipse3p" }, inc = 1 }, type = "string", desc = "Method of layout", guiOnlyArg = true } }, runSilent = false, digMode = "@", desc = "Draw a ellipse", aliases = { "e3" } }, --crude hack to allow ellipse and ellipse3p to switch named on the arg. + + polygon = { requireOrigin = true, requireMajor = false, requireZ = true, allowFilled = false, args = { { name = "sides", required = true, default = 3, type = "int", desc = "Number of sides of the polygon" }, { name = "vertex", required = true, default = false, type = "bool", desc = "Is the cursor on a vertex, or the midpoint of a side?" }, }, runSilent = false, digMode = "@", desc = "Draw a polygon", aliases = { "p", "poly", "ngon" } }, + + star = { requireOrigin = true, requireMajor = false, requireZ = true, allowFilled = false, args = { { name = "points", required = true, default = 5, type = "int", desc = "Number of points of the star" }, { name = "skip", required = true, default = 2, type = "int", desc = "How many to skip when connecting...?" }, }, runSilent = false, digMode = "@", desc = "Draw a star", aliases = { "s", "st" } }, + + spiral = { requireOrigin = true, requireMajor = false, requireZ = true, allowFilled = false, args = { { name = "coils", required = true, default = { default = 2, min = 1, max = 1000, inc = 1, type = "int" }, type = "int", desc = "Number of turns the spiral makes." }, { name = "skip", required = true, default = { default = 1, min = 1, max = 1000, inc = 1, type = "int" }, type = "int", desc = "Draw every # points along spiral." }, { name = "rotate", required = true, default = { default = 0, mod = 360, inc = 15, type = "int" }, type = "int", desc = "Rotate the spiral, 0-360." }, }, runSilent = false, digMode = "@", desc = "Draw a spiral", aliases = { "sp", "coil" } }, + + flood = { requireOrigin = false, requireMajor = false, requireZ = false, allowFilled = false, args = { { name = "max", required = false, default = { default = 2000, min = 1, max = 10000, inc = 1000, type = "int" }, type = "int", desc = "Maximum number of tiles filled before aborting. Larger numbers just take longer to complete." }, { name = "diagonals", required = false, default = false, type = "bool", desc = "Should the flood escape through corners?", guiOnlyArg = true }, }, runSilent = false, digMode = "@", desc = "Floodfill current designation at cursor.", aliases = { "f", "fl", "fill", "floodfill" } }, + + resetz = { requireOrigin = true, requireMajor = false, requireZ = true, allowFilled = true, args = nil, runSilent = false, digMode = "@", desc = "Move all control points to current z level", aliases = { "z" } }, + radial = { requireOrigin = true, requireMajor = false, requireZ = true, allowFilled = true, args = { { name = "ways", required = true, default = 3, type = "int", desc = "Number of radially symetrical points to draw." }, }, runSilent = false, digMode = "@", "Draw points with radial symmetry around origin", aliases = { "r", "rad" } }, --todo: code this + curve = { requireOrigin = true, requireMajor = true, requireZ = true, allowFilled = false, args = { { name = "Sharpness", required = true, default = { default = 1.5, min = 0, max = 100, inc = 0.1, type = "float" }, type = "float", desc = "How strongly the curve is pulled towards the cursor" } }, runSilent = false, digMode = "@", desc = "Draw a curve (bezier) from origin to major pulled towards cursor", aliases = { "cu", "b", "bezier", "bez", "bezeir" } }, --todo: allow filled. Also draw line, then fill shape + --{ default = 1.5, min = 0, max = 100, inc = 0.1, type = "float" } + --arc = { requireOrigin = true, requireMajor = false, requireZ = true, allowFilled = true, args = nil, runSilent = false,digMode="@",desc="Draw an arc from origin to major passing through cursor." }, + downstair = { requireOrigin = false, requireMajor = false, requireZ = false, allowFilled = false, args = { { name = "depth", required = true, default = 10, type = "int", desc = "Number of z levels down to designate." }, { name = "start", required = true, default = true, type = "bool", desc = "Should the starting level be updown [false] or down [true]" }, }, runSilent = false, digMode = "", desc = "Designate a 3x3 block of updown stairs, corners and center only", aliases = { "k", "ks", "stair", "down", "downstairs", "stairs" } }, + }, + + + --Mouse variables + mouse = true, + mousebuttons = { 'commit', 'origin', 'major' }, + dragging = false, + lastMouse = xyz2pos(0, 0, 0), + lerp = true, --lerp is laggy because it repeatedly inefficiently accesses memory; it would be faster if we cached some but thats above my paygrade ;) + customOffset = { x = 0, y = 0 }, +} + +function DigshapeUI:init() + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + self.saved_mode = df.global.ui.main.mode + df.global.ui.main.mode = df.ui_sidebar_mode.LookAround + + --grab the current digshape status + self:runDigshapeCommand("digshape lua status") + + --set up the layout of the menu + self:addviews { + + widgets.Label { + frame = { t = 3, l = 1 }, --place it inset one tile off the bottom left + view_id = "controlPointsMenu", + text = { + { key = "CUSTOM_O", text = "Set origin", key_sep = ": ", + on_activate = self:callback('buttonCallback_setOrigin') + }, NEWLINE, + + { key = "CUSTOM_S", text = "Swap origin/cursor", key_sep = ": ", + on_activate = self:callback('buttonCallback_swapOrigin') + }, NEWLINE, + + { key = "CUSTOM_A", text = "Set major", key_sep = ": ", id = "button_setCtrlA", + on_activate = self:callback('buttonCallback_setControlA') + }, NEWLINE, + + { id = "button_toggleFill", key = "CUSTOM_F", text = "Toggle fill: " .. self:getCurrentFill(), key_sep = ": ", + on_activate = self:callback('buttonCallback_toggleFilled') + }, NEWLINE, + }, + }, + + + widgets.Label { + frame = { t = 9, l = 1 }, --place it inset one tile off the bottom left + view_id = "digshapeMenu", + text = { + { key = "CUSTOM_P", text = "Set digshape command", key_sep = ": ", + on_activate = self:callback('buttonCallback_setCommand'), + }, NEWLINE, + { text = "[ ", pen = COLOR_YELLOW, gap = 2 }, + { text = "[ digshape command ]", pen = COLOR_YELLOW, id = "label_digshapeCommand" }, + { text = " ]", pen = COLOR_YELLOW }, NEWLINE, + NEWLINE, + + + --------------Arg 1 + { key = "SECONDSCROLL_UP", key_sep = ",", id = "button_arg1dec", + on_activate = self:callback('buttonCallback_argAdjust', 1, "-", 1) + + }, + { key = "SECONDSCROLL_DOWN", id = "button_arg1inc", + on_activate = self:callback('buttonCallback_argAdjust', 1, "+", 1) + + }, + { text = ": ", id = "label_arg1sepA", }, --Adjust: "}, + { text = "ARG1NAME", id = "label_arg1name", width = 8, pad_char = ".", }, + { text = ": [ ", id = "label_arg1sepB", }, + { text = "#", id = "label_arg1value" }, + { text = " ]", id = "label_arg1sepC", }, + NEWLINE, + + --------------Arg 2 + { key = "SECONDSCROLL_PAGEUP", key_sep = ",", id = "button_arg2dec", + on_activate = self:callback('buttonCallback_argAdjust', 2, "-", 1) + + }, + { key = "SECONDSCROLL_PAGEDOWN", id = "button_arg2inc", + on_activate = self:callback('buttonCallback_argAdjust', 2, "+", 1) + + }, + { text = ": ", id = "label_arg2sepA", }, --Adjust: "}, + { text = "ARG2NAME", id = "label_arg2name", width = 8, pad_char = ".", }, + { text = ": [ ", id = "label_arg2sepB", }, + { text = "#", id = "label_arg2value" }, + { text = " ]", id = "label_arg2sepC", }, + NEWLINE, + + --------------Arg 3 + { key = "STRING_A091", key_sep = ",", id = "button_arg3dec", --STANDARDSCROLL_UP + on_activate = self:callback('buttonCallback_argAdjust', 3, "-", 1) + + }, + { key = "STRING_A093", id = "button_arg3inc", + on_activate = self:callback('buttonCallback_argAdjust', 3, "+", 1) + + }, + { text = ": ", id = "label_arg3sepA", }, --Adjust: "}, + { text = "ARG3NAME", id = "label_arg3name", width = 8, pad_char = ".", }, + { text = ": [ ", id = "label_arg3sepB", }, + { text = "#", id = "label_arg3value" }, + { text = " ]", id = "label_arg3sepC", }, + NEWLINE, + + + --[[ { key = "CUSTOM_SHIFT_P", text = "Reset arguments", key_sep = ": ", id = "button_resetArgs", + on_activate = self:callback('buttonCallback_argAdjust', -1, "reset") + --self:callback('buttonCallback_setCommand')("Q1") + }, NEWLINE,]] + + } + }, + + widgets.Label { + frame = { t = 18, l = 1 }, + view_id = "digmodeMenu", + text = { + { text = "Set Designation:" }, + NEWLINE, + { text = "[" }, --Adjust: "},Designate: + { text = "", id = "label_digmodeStart" }, + { key = "CUSTOM_D", text = "", key_sep = "", + on_activate = self:callback('buttonCallback_setDig', 'd'), id = "button_digmode_d", + --pen=self.pens.digMode.selected,dpen=self.pens.digMode.deselected,enabled=true, + }, + { key = "CUSTOM_I", text = "", key_sep = "", + on_activate = self:callback('buttonCallback_setDig', 'i'), id = "button_digmode_i", + -- pen=self.pens.digMode.selected,dpen=self.pens.digMode.deselected,enabled=false, + }, + { key = "CUSTOM_H", text = "", key_sep = "", + on_activate = self:callback('buttonCallback_setDig', 'h'), id = "button_digmode_h", + --pen=self.pens.digMode.selected,dpen=self.pens.digMode.deselected,enabled=false, + }, + { key = "CUSTOM_R", text = "", key_sep = "", + on_activate = self:callback('buttonCallback_setDig', 'r'), id = "button_digmode_r", + -- pen=self.pens.digMode.selected,dpen=self.pens.digMode.deselected,enabled=false, + }, + { key = "CUSTOM_J", text = "", key_sep = "", + on_activate = self:callback('buttonCallback_setDig', 'j'), id = "button_digmode_j", + -- pen=self.pens.digMode.selected,dpen=self.pens.digMode.deselected,enabled=false, + }, + { key = "CUSTOM_U", text = "", key_sep = "", + on_activate = self:callback('buttonCallback_setDig', 'u'), id = "button_digmode_u", + -- pen=self.pens.digMode.selected,dpen=self.pens.digMode.deselected,enabled=false, + }, + { key = "CUSTOM_X", text = "", key_sep = "", + on_activate = self:callback('buttonCallback_setDig', 'x'), id = "button_digmode_x", + -- pen=self.pens.digMode.selected,dpen=self.pens.digMode.deselected,enabled=false, + }, + { key = "CUSTOM_SHIFT_M", text = "", key_sep = "", + on_activate = self:callback('buttonCallback_setDig', 'M'), id = "button_digmode_M", + -- pen=self.pens.digMode.selected,dpen=self.pens.digMode.deselected,enabled=false, + }, + --NEWLINE, + + -- { text = "ARG3NAME", id = "label_arg3name" }, + --{ text = "", id = "label_digmodeEnd" }, + { text = "]: ", pen = { CLEAR_PEN, bg = COLOR_BLACK }, }, + { text = "digmode name", id = "label_digmodeName" }, + --{ text = " ]" },NEWLINE, + + NEWLINE, NEWLINE, + { key = "CUSTOM_ALT_F", text = "Floodfill at cursor", key_sep = ": ", + on_activate = self:callback('buttonCallback_floodAtCursor'), + }, + NEWLINE, + + + }, + }, + + widgets.Label { + frame = { t = 24, l = 1 }, + view_id = "shapeModeMenu", + text = { + { text = "Choose shape: CTRL+..." }, + NEWLINE, + --{ text = "[" }, --Adjust: "},Designate: + { text = "", id = "label_shapemodeStart" }, + + { key = "CUSTOM_CTRL_I", text = "Spiral, ", key_sep = ":", + on_activate = self:callback('buttonCallback_setShape', 'i'), id = "button_shapemode_spiral", + }, + + { key = "CUSTOM_CTRL_C", text = "Circle, ", key_sep = ":", + on_activate = self:callback('buttonCallback_setShape', 'c'), id = "button_shapemode_circle", + }, + NEWLINE, + { key = "CUSTOM_CTRL_E", text = "Ellipse, ", key_sep = ":", + on_activate = self:callback('buttonCallback_setShape', 'e'), id = "button_shapemode_ellipse", + --pen=self.pens.digMode.selected,dpen=self.pens.digMode.deselected,enabled=false, + }, + { key = "CUSTOM_CTRL_P", text = "Polygon, ", key_sep = ":", + on_activate = self:callback('buttonCallback_setShape', 'p'), id = "button_shapemode_polygon", + -- pen=self.pens.digMode.selected,dpen=self.pens.digMode.deselected,enabled=false, + }, + NEWLINE, + { key = "CUSTOM_CTRL_L", text = "Line, ", key_sep = ":", + on_activate = self:callback('buttonCallback_setShape', 'l'), id = "button_shapemode_line", + -- pen=self.pens.digMode.selected,dpen=self.pens.digMode.deselected,enabled=false, + }, + { key = "CUSTOM_CTRL_T", text = "Star, ", key_sep = ":", + on_activate = self:callback('buttonCallback_setShape', 't'), id = "button_shapemode_star", + -- pen=self.pens.digMode.selected,dpen=self.pens.digMode.deselected,enabled=false, + }, + NEWLINE, + { key = "CUSTOM_CTRL_F", text = "Flood, ", key_sep = ":", + on_activate = self:callback('buttonCallback_setShape', 'f'), id = "button_shapemode_flood", + -- pen=self.pens.digMode.selected,dpen=self.pens.digMode.deselected,enabled=false, + }, + { key = "CUSTOM_CTRL_B", text = "Curve, ", key_sep = ":", + on_activate = self:callback('buttonCallback_setShape', 'b'), id = "button_shapemode_curve", + -- pen=self.pens.digMode.selected,dpen=self.pens.digMode.deselected,enabled=false, + }, + NEWLINE, + { key = "CUSTOM_CTRL_D", text = "Stair", key_sep = ":", + on_activate = self:callback('buttonCallback_setShape', 'd'), id = "button_shapemode_downstair", + -- pen=self.pens.digMode.selected,dpen=self.pens.digMode.deselected,enabled=false, + }, + --NEWLINE, + -- { text = "]" }, + + + + + }, + + }, + widgets.Label { + frame = { b = 1, l = 1 }, --place it inset one tile off the bottom left + view_id = "bottomMenu", + text = { + --{ key = "HELP", text = "Help for cur. cmd.", key_sep = ": ", + -- on_activate = self:callback('buttonCallback_showHelpPopup'), + --}, NEWLINE, + + { text = "CTRL+move: Move origin & cursor", key_sep = "", + on_activate = self:callback('buttonCallback_dualmove'), + }, NEWLINE, + + { key = "STRING_A092", text = "Move view to see origin", key_sep = ": ", + on_activate = self:callback('buttonCallback_recenterView'), + }, NEWLINE, + + { key = "CUSTOM_Z", text = "Undo last digshape", key_sep = ": ", + on_activate = self:callback('buttonCallback_undo'), + }, NEWLINE, + + { key = "SELECT", text = "Execute Command", key_sep = ": ", + on_activate = self:callback('buttonCallback_commit'), + }, NEWLINE, + + { key = "LEAVESCREEN", text = "Back", key_sep = ": ", + on_activate = self:dismiss(), + }, NEWLINE, + }, + }, + } + + --if origin isn't set, just put it at the cursor + --todo: make this a skippable option named on argument. Other values might include "here", "last", "coords" + if self.origin == nil then + self:runDigshapeCommand("digshape lua origin") + end + + --assume we're always working on the current z level, todo: make this an option named on argument + self.autosetZtoCurrent = true + + --display the current command preview + --self:buttonCallback_setDig(self.activeDesgination) + self:setCommand(self.activeCommand.name) + self:buttonCallback_setDig("d") + self:updateMenuDisplay() + --self:previewCurrentCommand() + --dfhack.gui.revealInDwarfmodeMap(self.origin) +end + +function DigshapeUI:onDestroy() + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + df.global.ui.main.mode = self.saved_mode +end + +function DigshapeUI:toggleSubViewVis(viewID, setActiveByVis) + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + --toggles visibility (and by default, active status) of a id'd subview. + if self.subviews == nil then + stdout("ERR", "View element not found:", viewID) + return + end + if setActiveByVis == nil then + setActiveByVis = true + end + --self.subviews[viewID]=nil + --for i,v in ipairs(self.subviews) do + -- if v.view_id==viewID then + -- self.subviews[i]=nil + -- end + --end + -- + self.subviews[viewID].visible = not self.subviews[viewID].visible + if setActiveByVis then + self.subviews[viewID].active = self.subviews[viewID].visible + end +end + +function DigshapeUI:toggleSubViewActive(viewID) + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + --toggles View.Active, which can change the display, and stops it from getting keypresses. + if self.subviews == nil then + stdout("ERR", "View element not found:", viewID) + return + end + self.subviews[viewID].active = not self.subviews[viewID].active +end + +local lastX = df.global.cursor.x +local lastY = df.global.cursor.y +local lastZ = df.global.cursor.z + + + +--local function deepcopy(orig) +--http://lua-users.org/wiki/CopyTable +-- local orig_type = type(orig) +-- local copy +-- if orig_type == 'table' then +-- copy = {} +-- for orig_key, orig_value in next, orig, nil do +-- copy[deepcopy(orig_key)] = deepcopy(orig_value) +-- end +-- setmetatable(copy, deepcopy(getmetatable(orig))) +-- else -- number, string, boolean, etc +-- copy = orig +-- end +-- return copy +--end + + + +function DigshapeUI:updateMenuDisplay() + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + -- update all arg display and value, plus major/fill;;or update given arg to current value. TODO + local command = self.activeCommand.name + --if command ~=self.activeCommand.name then + -- print(command, self.activeCommand.name, self.activeCommand.digshapeString) + -- --stdout("WRN", "self.activeCommand.name != self.activeCommand.name",self.activeCommand.name ,self.activeCommand.name) + -- -- printtable(self.activeCommand.name) + -- -- worms() + --end + + local showArgs + showArgs = function(self, command) + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + command = command or self.activeCommand.name + -- stdout("MSG", ">showargs>" .. command.."<") + -- print(self.activeCommand.name) + if self.digshapeCommands[command] == nil then + stdout("WRN", "No args for:" .. command) + return nil + end + + local args = self.digshapeCommands[command].args + local ids = { "button_arg#dec", "button_arg#inc", "label_arg#name", "label_arg#sepA", "label_arg#sepB", "label_arg#sepC", "label_arg#value" }--, "button_resetArgs" }--,"label_arg#desc"} + + local nargs = 0 + if args ~= nil then + for i, v in ipairs(args) do + for k = 1, #ids do + local index = string.gsub(ids[k], "#", i) + -- print("vis", i, k, index) + self:updateMenuArg("digshapeMenu", index, { disabled = false }) + + end + --printtable(v, "ARG " .. i) + self:updateMenuArg("digshapeMenu", "label_arg" .. i .. "name", { text = v.name }) + self:updateMenuArg("digshapeMenu", "label_arg" .. i .. "value", { text = tostring(v.currentValue) }) + nargs = nargs + 1 + end + end + + if nargs < 3 then + for i = nargs + 1, 3 do + for k = 1, #ids do + local index = string.gsub(ids[k], "#", i) + -- print("hide", i, k, index) + self:updateMenuArg("digshapeMenu", string.gsub(ids[k], "#", i), { dpen = self.pens.disabledMenu, disabled = true }) + end + + self:updateMenuArg("digshapeMenu", "label_arg" .. i .. "name", { text = "--------" }) + self:updateMenuArg("digshapeMenu", "label_arg" .. i .. "value", { text = "-" }) + end + + end + end + --printtable(self.activeCommand) + + --update digshape command display + self:rebuildDigshapeArgumentString() + self:updateMenuArg("digshapeMenu", "label_digshapeCommand", { text = self.activeCommand.digshapeString:gsub("@", self.activeDesgination) }) + + --update control point display/lockout + showArgs(self) + local fillToggle = self:getCurrentFill() + self:updateMenuArg("controlPointsMenu", "button_toggleFill", { text = "Toggle fill: " .. fillToggle, disabled = (fillToggle == "NA"), dpen = self.pens.disabledMenu }) + + --update setmajor display + --printtable(self.digshapeCommands[command]) + if self.digshapeCommands[command].requireMajor then + if self.major == nil then + stdout("ERR","Ctrl pt A 'major' must be set for this command.") + self:updateMenuArg("controlPointsMenu", "button_setCtrlA", { dpen = self.pens.alertMenu, disabled = false }) + end + --print("enable major") + self:updateMenuArg("controlPointsMenu", "button_setCtrlA", { dpen = self.pens.enabledMenu, disabled = false }) + else + --print("disable major") + self:updateMenuArg("controlPointsMenu", "button_setCtrlA", { dpen = self.pens.disabledMenu, disabled = false }) + --don't actually disable the key, just darken it to indicate we don't need it + end + + --update argument display + --print(self.activeCommand.name) + --printtable(self.activeCommand.name) + -- + --printtable(self.digshapeCommands[self.activeCommand.name]) + --printtable(self.digshapeCommands[self.activeCommand.name].args) + --[self.activeCommand.name] + if self.activeCommand.args ~= nil and self.activeCommand.args ~= {} then + showArgs(self, command)--showargs first so that preview updates their values. + else + -- printtable(self.activeCommand) + stdout("WRN", "Can't update args in menu, no args set.") + end + + --update designation display + + +end + +--self:updateMenuArg("","",{}) +function DigshapeUI:updateMenuArg(menu, textID, newvalues) + if type(verbose) == "int" and verbose > 1 then + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + end + local temp = self.subviews[menu].text_ids[textID] + + if type(newvalues) == "table" then + if newvalues.dpen ~= nil then + temp.dpen = newvalues.dpen + end + if newvalues.disabled ~= nil then + temp.disabled = newvalues.disabled + end + if newvalues.text ~= nil then + temp.text = newvalues.text + end + end +end + +function DigshapeUI:changeCommandTo(newCommand) + --this function is what the input prompt returns the input with. We will detect this value becoming not nil at the top of onRenderBody() and call setCommand(self.changeCommandToCommand). + self.changeCommandToCommand = newCommand +end + +function DigshapeUI:setCommand(newCommand) + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + stdout(">>>", "Setcommand ---------------------") + stdout("MSG", "Set command=", newCommand) + local commandBase = "" + for k, v in pairs(self.digshapeCommands) do + if newCommand == k then + commandBase = k + break + elseif v.aliases ~= nil then + for i = 1, #v.aliases do + if newCommand == v.aliases[i] then + commandBase = k + break + end + end + + end + end + newCommand = commandBase + + self.activeCommand.name = newCommand + self.activeCommand.digshapeString = nil + local command = self:parseCommand() + self:runDigshapeCommand("digshape lua status") + + --this is the one exception to calling preview outside of onInput, as onInput has already resolved at this point. TODO: simulate input to force redraw + self:updateMenuDisplay() + self:previewCurrentCommand() +end + +function DigshapeUI:rebuildDigshapeArgumentString() + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + --does not set "digshape lua preview" + local newstring = "" + local words = {} + if self.activeCommand.digshapeString ~= nil then + while true do + local w = string.gsub(self.activeCommand.digshapeString, "^(%S*)( ?.-)$", "%1")--:sub(1) + if w == "" then + break + end + table.insert(words, w) + self.activeCommand.digshapeString = self.activeCommand.digshapeString:gsub(w, "") + end + newstring = words[1] + words[1] = nil + else + self.activeCommand.digshapeString = "" + end + + for index, w in ipairs(words) do + print(index, w) + if w == "digshape" or w == "lua" or w == "preview" then + newstring = newstring .. " " .. w + end + for k, v in pairs(self.digshapeCommands) do + if w == k then + newstring = newstring .. " " .. w:tostring() + end + end + end + + if self.activeCommand.args ~= nil then + for k, v in pairs(self.activeCommand.args) do + -- print(k) + -- printtable(v) + if v.guiOnlyArg == nil then + newstring = newstring .. " " .. tostring(v.currentValue) + end + end + end + --filled/hollow + local filledstring = "NA" + if self.activeCommand.digshapeArgs.fill ~= "NA" then + filledstring = tostring(self.activeCommand.digshapeArgs.fill) + else + filledstring = "" + end + if filledstring ~= "" then + newstring = newstring .. " " .. filledstring + end + --digMode + local digstring = "@" + if self.activeCommand.digshapeArgs.digmode ~= "@" then + digstring = self.activeCommand.digshapeArgs.digmode:gsub("%s", "") + else + digstring = self.activeDesgination + end + newstring = newstring .. " " .. digstring + self.activeCommand.digshapeString = newstring + -- = self.activeCommand.digshapeString:gsub("^((digshape )?(lua )?(preview )?(%a+)(.-)$","%1%2%3%4") +end + +function DigshapeUI:clearCommand() + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + stdout(">>>", "Clear Command") + + local currentFill = shallowcopy(self.activeCommand.digshapeArgs.fill) -- save this so it carries between commands + local commandBase = self.activeCommand.name--:lower():match("^%a+") + self.activeCommand = self.resetActiveCommand() + self.activeCommand = { name = commandBase, args = shallowcopy(self.digshapeCommands[commandBase].args), digshapeString = commandBase, digshapeArgs = { + fill = "NA", --NA if unsupported by this command, "filled" or "hollow" if digshape supports it for this command + digmode = "@", --'@': replace with current digmode. + mode = "designating"--"designating" or "marking" or "toggling" + } } + + --Make the self.activeCommand.digshapeArgs correct + if self.digshapeCommands[commandBase].allowFilled then + if currentFill ~= "NA" then + self.activeCommand.digshapeArgs.fill = currentFill + + else + self.activeCommand.digshapeArgs.fill = "hollow" + end + end + self.activeCommand.digshapeArgs.digmode = shallowcopy(self.digshapeCommands[commandBase].digMode) + + --make sure any commandBase args are set to not autoswap + if self.activeCommand.args ~= nil then + for argname, arg in pairs(self.activeCommand.args) do + if type(arg.default) == "table" and arg.default.commandBase ~= nil then + arg.currentValue = arg.default.default + end + end + + end +end + +function DigshapeUI:parseCommand() + --change a self.activeCommand.name into a full self.activeCommand, with complete digshapeString (not inc "digshape lua preview?") + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + stdout("MSG", "parsecommand:", self.activeCommand.name) + + --reset and clear out self.activeCommand + self:clearCommand() + + --Setup the command arguments (to defaults), and build the digshapeString + if self.activeCommand.args ~= nil then + for argi = 1, #self.activeCommand.args do + if self.activeCommand.args[argi].currentValue == nil then + self.activeCommand.args[argi].currentValue = self.activeCommand.args[argi].default + + if type(self.activeCommand.args[argi].currentValue) == "table" then + self.activeCommand.args[argi].currentValue = self.activeCommand.args[argi].default.default + --todo: delete this once all the commands have full default={} stuff. + end + + end + + if self.activeCommand.args[argi].guiOnlyArg ~= true then + self.activeCommand.digshapeString = self.activeCommand.digshapeString .. " " .. tostring(self.activeCommand.args[argi].currentValue) + else + -- print("don't add this arg(" .. self.activeCommand.name .. ":" .. self.activeCommand.args[argi].name .. ") to digshape, internal use only.") + + end + end + end + --print("fill", self.activeCommand.digshapeArgs.fill) + if self.activeCommand.digshapeArgs.fill ~= "NA" then + self.activeCommand.digshapeString = self.activeCommand.digshapeString .. " " .. self.activeCommand.digshapeArgs.fill + end + --print("dig", self.activeCommand.digshapeArgs.digmode) + if self.activeCommand.digshapeArgs.digmode ~= nil then + self.activeCommand.digshapeString = self.activeCommand.digshapeString .. " " .. self.activeCommand.digshapeArgs.digmode:gsub("@", self.activeDesgination) + end + + self:rebuildDigshapeArgumentString() + + stdout("MSG", "Parsed command:", self.activeCommand.digshapeString) +end + +function DigshapeUI:runCurrentCommand(preview) + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + -- print(">>>>", self.activeCommand.name, self.activeCommand.digshapeString) + local command = ("digshape lua %s"):format(preview and "preview" or "") + + --Check for a BASECOMMAND argument, in case this is a command that needs to be run as another command. (eg. GUI ellipse runs both "ellipse bbox" and "ellipse 2axis") + local nameCommand = self.activeCommand.name--self:parseCommand() + local oldCommand = nil --if we swap commands during exicution, save here to revert. + + if self.digshapeCommands[nameCommand].args ~= nil then + --check all args to see if one has a defaults table including 'commandBase'. + --if it does, and nameCommand(currently trying to run) does not match commandBase(what this option wants to be run as), then we need to switch command to it. + --printtable(self.digshapeCommands[nameCommand],i) + local thisCommandIndex = nil + local matchCommandIndex = nil + -- printtable(self.digshapeCommands[nameCommand]) + for i = 1, #self.digshapeCommands[nameCommand].args do + if type(self.digshapeCommands[nameCommand].args[i].default) == "table" and self.digshapeCommands[nameCommand].args[i].default.commandBase ~= nil then + --get index of this arg in it's values list, and use that to select the right commandBase + local swaptocommand = "" + local valueToMatch = self.activeCommand.args[i].currentValue + for ii, vv in ipairs(self.digshapeCommands[nameCommand].args[i].default.values) do + if self.digshapeCommands[nameCommand].args[i].default.commandBase[ii] == self.activeCommand.name then + thisCommandIndex = ii + end + if valueToMatch == vv then + --print("matchfound", vv, ii) + matchCommandIndex = ii + end + end + + if thisCommandIndex ~= matchCommandIndex then + swaptocommand = self.digshapeCommands[nameCommand].args[i].default.commandBase[matchCommandIndex] + --printtable(self.activeCommand) + stdout(">>>", "Command swap: ", self.activeCommand.name, "to", swaptocommand, thisCommandIndex, matchCommandIndex) + oldCommand = self.activeCommand + self:clearCommand() + self:setCommand(swaptocommand) + break + end + end + end + end + + self:rebuildDigshapeArgumentString() + command = command .. " " .. self.activeCommand.digshapeString + + stdout("CMD", command, preview) + if not verbose and not preview then + local out, _ = command:gsub("lua ", "") + stdout("OUT", out) + end + + --check to make sure digshape will like the command, if not, don't bother calling and just return. + local commandTests = self.digshapeCommands[nameCommand] + if nameCommand == nil then + stdout("WRN", "Current command will not exicute, skipping. 1") + return + end + if commandTests.requireOrigin == true and self.origin == nil then + stdout("WRN", "Current command will not exicute, skipping. 2") + --try and recover by asking digshape + self:runDigshapeCommand("digshape lua status") + if commandTests.requireOrigin == true and self.origin == nil then + self:runDigshapeCommand("digshape lua origin") + --worms("Hard fail to debug. Function intentionally does not exist. Nondebug: just return nil.") + return nil + end + end + if commandTests.requireMajor == true and self.major == nil then + stdout("WRN", "Current command will not exicute, skipping. 3") + return nil + end + --if commandTests.requireZ==true and self.origin==nil then return nil end + if ((commandTests.digMode ~= "@" and commandTests.digMode ~= "") and commandTests.digMode ~= self.activeDesgination) then + stdout("WRN", "Current command will not exicute, skipping. 4") + return nil + end + + self:runDigshapeCommand(command) +end + +function DigshapeUI:runDigshapeCommand(command) + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + --simple validity checks + stdout("CMD", "RUNNING: '" .. command .. "'") + if command == nil then + stdout("ERR", "nil command") + return nil + end + + local output = dfhack.run_command_silent(command) + self.currentOutput = {} + self.currentError = {} + self.currentDig = {} + self.origin = nil + self.major = nil + + for line in output:gmatch("[^\r\n]+") do + stdout("RBY", ">>" .. line) + messageType = line:match("^([^:]+):") + if messageType == "msg" then + messageContents = line:match("^msg:(.*)$") + table.insert(self.currentOutput, messageContents) + elseif messageType == "err" then + messageContents = line:match("^err:(.*)$") + if line:match("Origin and target must be on the same z") then + stdout("CMD", "-------------------------------RECURSIVELY RESETZ-----------------------") + self:runDigshapeCommand("digshape lua resetz") + self:runDigshapeCommand(command) + else + stdout("ERR", line) + end + table.insert(self.currentError, messageContents) + elseif messageType == "dig" then + --these are the designations from digshape + digMode, x, y, z = line:match("^dig:([^:]+):([^:]+):([^:]+):([^:]+)") + table.insert(self.currentDig, { digMode = digMode, x = tonumber(x), y = tonumber(y), z = tonumber(z), symbol = self.digButtons[digMode].symbol }) + elseif messageType == "pos" then + posname, x, y, z = line:match("^pos:([^:]+):%(([^,]+),([^,]+),([^,]+)%)") + x, y, z = tonumber(x), tonumber(y), tonumber(z) + if posname == "origin" then + self.origin = xyz2pos(x, y, z) + elseif posname == "major" then + self.major = xyz2pos(x, y, z) + --elseif posname == "cursor" then + -- self.cursor = xyz2pos(x, y, z) + end + elseif messageType == "ref" then + messageContents = line:match("^ref:(.*)$") + table.insert(self.currentOutput, messageContents) + stdout("RBY", "ref:", messageContents) + else + --stdout("WRN", "Digshape Unhandled Output:", line) + end + end + + if self.autosetZtoCurrent then + local currentz = df.global.cursor.z + for i = 1, #self.currentDig do + self.currentDig[i].z = currentz + end + if self.origin ~= nil then + self.origin.z = currentz + end + if self.major ~= nil then + self.major.z = currentz + end + end + + stdout(">>>", "<<<<<<<< End call") +end + +function DigshapeUI:previewCurrentCommand() + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + self.runCurrentCommand(self, true) +end + +function DigshapeUI:commitCurrentCommand() + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + self.runCurrentCommand(self, false) +end + +local function paintMapTile(dc, vp, cursor, pos, ...) + if not same_xyz(cursor, pos) then + local stile = vp:tileToScreen(pos) + if stile.z == 0 then + -- FIXME: reduce lag by increasing overlay + dc:map(true):seek(stile.x, stile.y):char(...):map(false) + end + end +end + +function DigshapeUI:buttonCallback_setOrigin() + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + stdout("MSG", ">setorigin>") + self:runDigshapeCommand("digshape lua origin") + + self:previewCurrentCommand() +end + +function DigshapeUI:buttonCallback_swapOrigin() + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + self:runDigshapeCommand("digshape lua swap") + self:previewCurrentCommand() + + +end + +function DigshapeUI:buttonCallback_setControlA() + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + self:runDigshapeCommand("digshape lua major") + self:previewCurrentCommand() +end + +function DigshapeUI:buttonCallback_toggleFilled() + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + if self.activeCommand.digshapeArgs.fill == "NA" then + return + end + if self.activeCommand.digshapeArgs.fill == "filled" then + self.activeCommand.digshapeArgs.fill = "hollow" + else + self.activeCommand.digshapeArgs.fill = "filled" + end + + --since this is a callback we need to manually call these updates. + self:updateMenuDisplay() + self:previewCurrentCommand() +end + +function DigshapeUI:buttonCallback_setCommand() + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + --TODO: can maybe fix the stupid transparent edit box by making own class that supers all except changes the root gui:framedScreen.frame_background pen to not CLEAR_PEN.... or maybe editfield.on_char or on_change + dialog.showInputPrompt("Set digshape command", "Enter a digshape command", COLOR_WHITE, "", function(result) + self:changeCommandTo(result) + end + ) +end + +function DigshapeUI:updateDigMode(mode, set) + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + set = set == "set" and "set" or "clear" + --In the button list, insert a > and < before and after the currently selected item. + + local pitem = string.match(self.digButtons.order, "(.)" .. mode) + local titem = "button_digmode_" .. mode + + if pitem == "_" then + --we're outside the list of actual buttons, use the text []. + pitem = "label_digmodeStart" + else + pitem = "button_digmode_" .. pitem + end + + local kpen = self.pens.digMode.deselected + local text = "" + + if set == "set" then + local temp = self.pens.digMode.selected + if mode == 'x' then + temp = self.pens.digMode.delete + elseif mode == 'M' then + temp = self.pens.digMode.mark + end + kpen = temp + end + + local doset + doset = function(label, kpen, text, set) + if text == "<" then + self.subviews.digmodeMenu.text_ids[label].key_pen = kpen + end + self.subviews.digmodeMenu.text_ids[label].pen = kpen + if set == "set" then + self.subviews.digmodeMenu.text_ids[label].text = " "--text + else + self.subviews.digmodeMenu.text_ids[label].text = "" + end + end + + doset(pitem, kpen, ">", set)--before + doset(titem, kpen, "<", set)--thisitem +end + +function DigshapeUI:buttonCallback_setDig(mode) + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + -- local buttonCallback_setDig_labelhelper + + + + self:updateDigMode(self.activeDesgination, "clear") + self:updateDigMode(mode, "set") + self.subviews.digmodeMenu.text_ids["label_digmodeName"].text = self.digButtons[mode].text + + --do the update: + self.activeDesgination = mode + --regen digshape command + self:parseCommand() + self:previewCurrentCommand() +end + +function DigshapeUI:getCurrentFill() + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + local value = "" + + if self.digshapeCommands[self.activeCommand.name].allowFilled == false then + value = "NA" + else + value = self.activeCommand.digshapeArgs.fill + + end + return value +end + +function DigshapeUI:buttonCallback_argAdjust(argNum, argDir, argMod) + --TODO: make these curried as ARG#, INC/DEC={"+", "-"}, MODIFIER?S? + -- --(TODO: argMod:: SHIFT-inc: *2, CTRL-inc: *5, ALT-inc: /10; modifiers stack. ) + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + local tempDir = 1 + if argDir == "-" then + tempDir = tempDir * -1 + end + local tempMod = 1 + + local arg = self.activeCommand.args[argNum] + stdout(">>>", "argadj---------------------", argNum, argDir, argMod) + -- printtable(self.activeCommand, "Arg Adj") + if arg == nil then + stdout("ERR", "NIL ARG", argNum, argDir, argMod) + return + end + if argDir == "reset" then + stdout("MSG", "Reset all args to default.") + self:setCommand(self.activeCommand.name) + return + end + + if type(arg.default) == "table" then + tempMod = tempMod * arg.default.inc + end + tempMod = tempMod * (argMod) + + stdout("MSG", "argAdj: " .. argNum .. ":: " .. tempDir .. "," .. tempMod) + local newvalue + + if type(arg.currentValue) == "boolean" then + newvalue = not arg.currentValue + elseif type(arg.default) == "table" and arg.default.values ~= nil then + local index = 1 + local found = false + while not found do + for currentindex = 1, #arg.default.values do + if arg.default.values[currentindex] == arg.currentValue then + index = currentindex + found = true + break + end + --the while loop lets us loop around the back of the list. TODO: fix this dirty hack. + end + index = index + 1 + if index > #arg.default.values then + index = 1 + end + newvalue = arg.default.values[index] + end + else + newvalue = arg.currentValue + (1 * tempDir * tempMod) + if type(arg.default) == "table" then + if arg.default.mod ~= nil then + newvalue = newvalue % arg.default.mod + elseif newvalue < arg.default.min then + newvalue = arg.default.min + elseif newvalue > arg.default.max then + newvalue = arg.default.max + end + end + end + arg.currentValue = shallowcopy(newvalue) + stdout("MSG", "ArgAdj:" .. arg.name .. "=" .. tostring(arg.currentValue)) + self:updateMenuDisplay() + self:previewCurrentCommand() +end + +-- +--function DigshapeUI:buttonCallback_() +-- +--end + +function DigshapeUI:buttonCallback_undo() + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + dfhack.run_command("digshape undo") +end + +function DigshapeUI:buttonCallback_commit() + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + self:commitCurrentCommand() +end + +--function DigshapeUI:buttonCallback_setCommand() +-- +--end + + + +function DigshapeUI:buttonCallback_recenterView() + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + stdout("MSG", "recenter view:", self.origin) + dfhack.gui.revealInDwarfmodeMap(self.origin) + --todo: move cursor back on screen too... in it's relative position? +end + +function DigshapeUI:buttonCallback_showHelpPopup() + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + +end + +function DigshapeUI:buttonCallback_dualmove() + --move origin and cursor by same amount as current move key. + --todo: code this. +end + +function DigshapeUI:buttonCallback_floodAtCursor() + local digmode = self.activeDesgination + --if digmode == "M" then + -- digmode="d" + --end + self:runDigshapeCommand(("digshape lua flood 1000 " .. digmode)) +end + +function DigshapeUI:buttonCallback_setShape(keypress) + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + if keypress=="c" then + self:setCommand("circle") + elseif keypress=="e" then + self:setCommand("ellipse") + elseif keypress=="i" then + self:setCommand("spiral") + elseif keypress=="p" then + self:setCommand("polygon") + elseif keypress=="l" then + self:setCommand("line") + elseif keypress=="t" then + self:setCommand("star") + elseif keypress=="f" then + self:setCommand("flood") + elseif keypress=="b" then + self:setCommand("curve") + elseif keypress=="d" then + self:setCommand("downstair") + else + stdout("ERR","you can't get here.") + self:setCommand("spiral") + end +end + +--function DigshapeUI:buttonCallback_() +-- +--end + +--function DigshapeUI:buttonCallback_() +-- +--end + +--function DigshapeUI:buttonCallback_() +-- +--end + + + + + + + + + + + + + + + + +function DigshapeUI:renderOverlay() + --stdout("CAL",debug.getinfo(1,'n').name or "@ line "..debug.getinfo(1,'S').linedefined) + --todo: consider --https://docs.dfhack.org/en/stable/docs/Lua%20API.html#penarray-class for speedup + local vp = self:getViewport() + local dc = gui.Painter.new(self.df_layout.map) + local visible = gui.blink_visible(500) + + local cursorX, cursorY, cursorZ = df.global.cursor.x, df.global.cursor.y, df.global.cursor.z + if lastX ~= cursorX or lastY ~= cursorY or lastZ ~= cursorZ then + lastX, lastY, lastZ = cursorX, cursorY, cursorZ + --we have moved cursor, so update the state of the preview + self:previewCurrentCommand() + end + + for _, dig in ipairs(self.currentDig) do + paintMapTile(dc, vp, df.global.cursor, xyz2pos(dig.x, dig.y, dig.z), dig.symbol, self.activeDesgination == 'x' and self.pens['clear'] or self.pens['designation']) + end + + if self.digshapeCommands[self.activeCommand.name].requireOrigin then + if self.origin then + paintMapTile(dc, vp, df.global.cursor, self.origin, '+', self.pens['origin']) + end + end + if self.activeCommand.name ~= nil then + if self.major then + if self.digshapeCommands[self.activeCommand.name].requireMajor then + paintMapTile(dc, vp, df.global.cursor, self.major, 'a', self.pens['ctrl_A']) + + end + end + + end + + +end + +function DigshapeUI:onRenderBody(dc) + --stdout("CAL",debug.getinfo(1,'n').name or "@ line "..debug.getinfo(1,'S').linedefined) + if self.changeCommandToCommand ~= nil then + self:setCommand(self.changeCommandToCommand) + self.changeCommandToCommand = nil + end + self:renderOverlay() + + dc:clear():seek(1, 1):pen(COLOR_WHITE):string("Digshape(" .. DigshapeUIversion .. ") - " .. self.state) + + +end + +function DigshapeUI:onInput(keys) + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + stdout(">>>", "===========ON INPUT===========") + + --TODO: deal with multi-key presses, because keys is an array of individuals. + + if df.global.cursor.x == -30000 then + stdout("ERR", "Cursor offscreen, resetting.") + local vp = self:getViewport() + df.global.cursor = xyz2pos(math.floor((vp.x1 + math.abs((vp.x2 - vp.x1)) / 2) + .5), math.floor((vp.y1 + math.abs((vp.y2 - vp.y1) / 2)) + .5), vp.z) + return + end + + for k, v in pairs(keys) do + if k:match("^A_MOVE_") then + self.refresh = 1 + end + end + + DigshapeUI.super.onInput(self, keys) --call super so subviews (eg. the widges.Labels) can capture keypresses too. + + if keys.LEAVESCREEN then + self:dismiss() + elseif self:propagateMoveKeys(keys) then + return + end + stdout("CAL", debug.getinfo(1, 'n').name or "@ line " .. debug.getinfo(1, 'S').linedefined) + --self:updateMenuDisplay() + --self:previewCurrentCommand() +end + +if not (dfhack.gui.getCurFocus():match("^dwarfmode/Default") or dfhack.gui.getCurFocus():match("^dwarfmode/Designate") or dfhack.gui.getCurFocus():match("^dwarfmode/LookAround")) then + qerror("This screen requires the main dwarfmode view or the designation screen") +end + +local list = DigshapeUI { state = "mark", blink = false, cull = true } +list:show() \ No newline at end of file