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..52b7af0 100644 --- a/digshape.rb +++ b/digshape.rb @@ -3,6 +3,7 @@ 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: @@ -11,22 +12,25 @@ 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 +39,358 @@ 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 -TODO: mark origin should not change the digging designation, ellipse cleanup should restore not clear it. +============ 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 -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 + + + + + + + + + +=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 + +$isLuaMode = $script_args[0] == "lua" +$isPreviewOnly = false + +if $isLuaMode then + $script_args.delete_at(0) + $isPreviewOnly = $script_args[0] == "preview" + if $isPreviewOnly then + $script_args.delete_at(0) 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. + + 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 +415,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 +440,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 +457,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 +576,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 +665,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 +701,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 +715,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 +723,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 +740,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 +751,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 @@ -647,6 +929,7 @@ def drawEllipse(x0, y0, z0, x1, y1, z1, x2=nil, y2=nil, z2=nil, filled = false, end def digKeupoStair(x, y, z, depth) + #Dig an X of updown stairs (corners and center of a 3x3) centered on cursor, down a number of zlevels. iz = z digAt(x, y, iz, 'j') digAt(x - 1, y + 1, iz, 'j') @@ -663,15 +946,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 +979,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 +1003,54 @@ 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, 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 + # ('0'=no rotation, '1'=360 degrees, '180/360'=180 degrees) + rotation = 0 -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 + # value of theta corresponding to end of last coil + thetaMax = coils * 2 * Math::PI -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 + 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 + + 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 +1060,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 @@ -812,7 +1087,7 @@ def floodfill(x,y,z,targetDig, digMode, maxCounter= 10000) 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 + if !t || xi == 0 || t.designation.dig != targetDig || !isDigPermitted(digMode,t.shape_basic) then break end xw = xi @@ -823,7 +1098,7 @@ def floodfill(x,y,z,targetDig, digMode, maxCounter= 10000) 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 + if !t || xi == 0 || t.designation.dig != targetDig || !isDigPermitted(digMode,t.shape_basic) then break end xe = xi @@ -835,18 +1110,20 @@ def floodfill(x,y,z,targetDig, digMode, maxCounter= 10000) 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]'." + 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 #check N/S t = df.map_tile_at(xi,y+1,z) - if t && t.designation.dig == targetDig && digPermitted(digMode,t.shape_basic) then + if t && t.designation.dig == targetDig && isDigPermitted(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 + if t && t.designation.dig == targetDig && isDigPermitted(digMode,t.shape_basic) then xStack.push(xi) yStack.push(y-1) end @@ -859,331 +1136,378 @@ def floodfill(x,y,z,targetDig, digMode, maxCounter= 10000) 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" + 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' + #end + args.delete_at(0) -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 + return digMode end -command = $script_args[0] -argument1 = $script_args[1] -argument2 = $script_args[2] -argument3 = $script_args[3] +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 -if df.cursor.x == -30000 then - puts " Error: cursor must be on map" - throw :script_finished +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 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 -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' #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 - 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" - else - puts " Error: origin and target must be on the same z level" - throw :script_finished + noMoreArguments($script_args) + + setOrigin($origin.x, $origin.y, z) + if $major then + setMajor($major.x, $major.y, z) 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 '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' + stdout("origin: #{$origin != nil ? $origin.to_s : ''}") + stdout("major : #{$major != nil ? $major.to_s : ''}") + stdout("cursor: #{cursorAsDigPos().to_s}") + + 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($scripts_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' #digshape circle2p [filled] [digmode] + filled = getFilledArgument($script_args) + digMode = getDigModeArgument($script_args) #check argument 1 for dig instructions + 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' #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' #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") + digMode = getDigModeArgument($script_args) + noMoreArguments($script_args) + + drawSpiral($origin.x, $origin.y, $origin.z, df.cursor.x, df.cursor.y, df.cursor.x, coils, chord, digMode) + + when 'keupo', 'stairs', 'downstairs', 'downstair' #digshape keupo depth + depth = getIntegerArgument($script_args, type: "depth") + + 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) + + when 'flood', 'f' + 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 - - dig = getDigMode(argument3) + floodfill(df.cursor.x, df.cursor.y, df.cursor.z, targetDig, digMode, maxArea) - 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 -end + userSucks("Invalid command") +end \ No newline at end of file diff --git a/gui/digshape.lua b/gui/digshape.lua new file mode 100644 index 0000000..44c5559 --- /dev/null +++ b/gui/digshape.lua @@ -0,0 +1,294 @@ +--designating tool + +--[====[ + +gui/digshape +=========== +gui front-end for digshape.rb + +]====] + +local utils = require "utils" +local gui = require "gui" +local guidm = require "gui.dwarfmode" +local dialog = require "gui.dialogs" + +DigshapeUI = defclass(DigshapeUI, guidm.MenuOverlay) + +DigshapeUI.ATTRS { + state = "preview", + activeDesgination = 'd', + currentCommand = 'circle hollow @', + currentOutput = {}, + currentError = {}, + currentDig = {}, + -- 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} + ['origin']=dfhack.pen.make({ch='+',fg=COLOR_CYAN,bg=COLOR_LIGHTGREEN}), + ['cursor']=dfhack.pen.make({ch='X',fg=COLOR_CYAN,bg=COLOR_LIGHTGREEN}), + ['major']=dfhack.pen.make({ch='a',fg=COLOR_CYAN,bg=COLOR_LIGHTGREEN}), + ['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}) + }, + + --Designation mode variables + designateMarking = false, --are we designating "marking" rather than standard? + + + --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 }, +} + + +local digButtons={ + {key='d', symbol=" ", text="Mine"}, + {key='i', symbol="X", text="U/D Stair"}, + {key='h', symbol="_", text="Channel"}, + {key='r', symbol=30, text="Up Ramp"}, + {key='j', symbol=">", text="Down Stair"}, + {key='u', symbol="<", text="Up Stair"}, + {key='x', symbol=" ", text="Remove Designation"}, + {key='M', symbol=" ", text="Toggle Dig/Mark"} +} + +local digModeToButton = {} +for _, data in pairs(digButtons) do + data.keybind = ("CUSTOM_".. (string.match(data.key,"%u") ~=nil and "SHIFT_" or "") .."%s"):format(data.key:upper()) + digModeToButton[data.key] = data + print("~") +end + +local buttons = { + --key: the key that will be bound to callback. For non-button entries (eg blank lines), set key="NOTKEY" to skip the keybinding. + --text: the label for the keybind + --prelabel: a menu rendering function exicuted before displaying the keybinding, use to newline or set color, etc + --postlabel: as prelabel, but after the keybinding label is printed. + --callback: the function called by this button + {key="p", text="Set digshape command", postlabel=function(self, dc) + dc:newline(1):advance(3):pen(COLOR_YELLOW):string("[ "..self.currentCommand.." ]") + end, callback=function(self) + dialog.showInputPrompt("Set digshape command", "Enter a digshape command", COLOR_WHITE, self.currentCommand, function(result) + self.currentCommand=result + self:runCurrentCommand(true) + end) + end}, + {key="o", text="Set origin",prelabel=function(self,dc)dc:newline(1) if self.origin ~=nil then dc:pen(COLOR_LIGHTGREEN) else dc:pen(COLOR_LIGHTRED) end end, callback=function(self) + dfhack.run_command_silent("digshape lua origin") + self:runCurrentCommand(true) + end}, + {key="s", text="Swap origin/cursor", prelabel=function(self,dc)dc:newline(1):advance(2) if self.origin ~=nil then dc:pen(COLOR_LIGHTGREEN) else dc:pen(COLOR_LIGHTRED) end end, callback=function(self) + dfhack.run_command_silent("digshape lua swap") + self:runCurrentCommand(true) + end}, + + {key="a", text="Set major", callback=function(self) + dfhack.run_command_silent("digshape lua major") + self:runCurrentCommand(true) + end}, + {key="NOTKEY",prelabel=function(self,dc)dc:newline(2) end}, + {key="SELECT", keybind="SELECT", text="Execute command", callback=function(self) + self:runCurrentCommand(false) + end}, + {key="z", text="Undo digshape command", callback=function(self) + dfhack.run_command("digshape undo") + end} +} +for _, data in pairs(buttons) do + data.keybind = data.keybind or ("CUSTOM_%s"):format(data.key:upper()) +end + +local lastX = df.global.cursor.x +local lastY = df.global.cursor.y +local lastZ = df.global.cursor.z + +function DigshapeUI:runCurrentCommand(preview) + local command = ("digshape lua %s%s"):format(preview and "preview " or "", self.currentCommand):gsub("@", self.activeDesgination) + print(("command='%s'"):format(command)) + local output = dfhack.run_command_silent(command) + self.currentOutput = {} + self.currentError = {} + self.currentDig = {} + self.origin = nil + self.major = nil + print("output=", output) + for line in output:gmatch("[^\r\n]+") do + messageType = line:match("^([^:]+):") + if messageType == "msg" then + messageContents = line:match("^msg:(.*)$") + table.insert(self.currentOutput, messageContents) + elseif messageType == "err" then + messageContents = line:match("^err:(.*)$") + table.insert(self.currentError, messageContents) + elseif messageType == "dig" then + digMode, x, y, z = line:match("^dig:([^:]+):([^:]+):([^:]+):([^:]+)") + table.insert(self.currentDig, {digMode=digMode, x=tonumber(x), y=tonumber(y), z=tonumber(z), symbol=digModeToButton[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) + end + + else + print("unhandled output:", line) + end + end +end + +function DigshapeUI:init() + self.saved_mode = df.global.ui.main.mode + df.global.ui.main.mode=df.ui_sidebar_mode.LookAround + self:runCurrentCommand(true) +end + +function DigshapeUI:onDestroy() + df.global.ui.main.mode = self.saved_mode +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:renderOverlay() + 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 + self:runCurrentCommand(true) + 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.origin then + paintMapTile(dc, vp, df.global.cursor, self.origin, '+', self.pens['origin']) + end + if self.major then + paintMapTile(dc, vp, df.global.cursor, self.major, '+', self.pens['major']) + end + + +end + +function DigshapeUI:onRenderBody(dc) + self:renderOverlay() + + dc:clear():seek(1, 1):pen(COLOR_WHITE):string("Digshape - " .. self.state--:gsub("^%a", function(x) + -- return x:upper() + -- end) + ) + dc:seek(1,3) + + if true or self.state=="preview" then + for _, data in pairs(digButtons) do + builder = dc:newline(1):key_string(data.keybind, data.text, self.activeDesgination==data.key and COLOR_WHITE or COLOR_GREY) + -- if data.key=='x' then + -- builder:newline(1) + -- end + end + + dc:newline(2) + + for _, data in pairs(buttons) do + if data.prelabel ~= nil then + data.prelabel(self,dc) + else + dc:newline(1):pen(COLOR_GREY) + end + if data.key~="NOTKEY" then + builder = dc:key_string(data.keybind, data.text) + end + if data.postlabel ~= nil then + data.postlabel(self,dc) + end + --if data.key=='m' then + --builder:newline(1) + --end + end + + + --[[ dc:key_string("CUSTOM_S", "Set Brush",COLOR_GREY) + dc:newline():newline(1) + dc:key_string("CUSTOM_H", "Flip Horizontal",COLOR_GREY):newline(1) + dc:key_string("CUSTOM_V", "Flip Vertical",COLOR_GREY):newline(1) + dc:key_string("CUSTOM_R", "Rotate 90",COLOR_GREY):newline(1) + dc:key_string("CUSTOM_T", "Rotate -90",COLOR_GREY):newline(1) + dc:key_string("CUSTOM_G", "Cycle Corner",COLOR_GREY):newline(1) + dc:key_string("CUSTOM_I", "Invert",COLOR_GREY):newline(1) + dc:key_string("CUSTOM_C", "Convert to...",COLOR_GREY):newline(1) + dc:newline(1) + dc:key_string("CUSTOM_E", (self.option=="erase" and "Erasing" or "Erase"),self.option=="erase" and COLOR_RED or COLOR_GREY):newline(1) --make red + dc:key_string("CUSTOM_X", (self.option=="construction" and "Removing" or "Remove").." Constructions",self.option=="construction" and COLOR_GREEN or COLOR_GREY):newline(1) --make red + dc:newline():newline(1) + dc:key_string("CUSTOM_B", "Blink Brush",self.blink and COLOR_WHITE or COLOR_GREY):newline(1) + dc:newline() ]] + end + + dc:newline():newline(1):key_string("LEAVESCREEN", "Back") +end + + +function DigshapeUI:onInput(keys) + if df.global.cursor.x==-30000 then + 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 + if true or self.state=="preview" then + for _, data in ipairs(digButtons) do + if keys[data.keybind] then + self.activeDesgination = data.key + self:runCurrentCommand(true) + end + end + for _, data in ipairs(buttons) do + if keys[data.keybind] then + data.callback(self) + end + end + if keys.SELECT then + --self:pasteBuffer(copyall(df.global.cursor)) + end + end + + if keys.LEAVESCREEN then + self:dismiss() + elseif self:propagateMoveKeys(keys) then + return + end +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