From c5f4568cbf8f26ec72ce39e7d0449bec9bde471b Mon Sep 17 00:00:00 2001 From: Quatch Date: Thu, 25 Mar 2021 15:17:25 -0400 Subject: [PATCH 1/6] digshape refactor and gui, from flavourstreet Added flavourstreet's refactor of digshape, and it's new lua gui [keupo's discord, 2021-02-17] [8:10 PM]flavorstreet: Attachment file type: archive digshape-gui-ALPHA.zip 14.32 KB [8:12 PM]flavorstreet: Yall can do whatever u want with it i'll probably never get around to fixing the bugs im so burned out lol --811767111344193566 --- digshape.rb | 787 +++++++++++++++++++++++++++-------------------- gui/digshape.lua | 237 ++++++++++++++ 2 files changed, 691 insertions(+), 333 deletions(-) create mode 100644 gui/digshape.lua diff --git a/digshape.rb b/digshape.rb index c0bc16b..295f826 100644 --- a/digshape.rb +++ b/digshape.rb @@ -47,33 +47,113 @@ TODO: mark origin should not change the digging designation, ellipse cleanup should restore not clear it. =end +DigPos = Struct.new(:x, :y, :z) do + def to_s + return "(#{x},#{y},#{z})" + end + def clone + return DigPos.new(x,y,z) + end +end + +def cursorAsDigPos() + return DigPos.new(df.cursor.x, df.cursor.y, df.cursor.z) +end + +# really nasty hack so that lua can use digshape +$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 setOrigin(x, y, z) # sets the origin and marks if it iff we are in console. cleans up last mark too. + $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) + $major = DigPos.new(x,y,z) +end + +def stdout(msg) + if $isLuaMode == false then + puts msg + else + puts "msg:"+msg + end +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 +def stderr(msg) # write an error mesage + 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 + stderr(msg) + raise "oopsie! script errored! ;)" +end + +def userSucks(msg) # call this when we don't like the user's input + stderr(msg) + throw :script_finished +end + + -def unDig() - #Exicute one level of undo. +def undo() # BUG! Does not keep track of Z levels + #Execute one level of undo. #z level is presumed to be the current. + # todo, have multiple levels of undo / redo i=$digBufferX.length - while i >= 0 do + 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,df.cursor.z, enum2dig(d), buffer=false) + 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. - clearDigBuffer() + end + #clear buffer for next dig + $digBufferX = newBufferX + $digBufferY = newBufferY + $digBufferZ = newBufferZ + $digBufferD = newBufferD end @@ -81,40 +161,83 @@ def clearDigBuffer() #clear buffer for next dig, or initialize it's existance on first run. $digBufferX=[] $digBufferY=[] + $digBufferZ=[] $digBufferD=[] 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 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 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 + +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) + 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 def drawLineLow(x0, y0, z0, x1, y1, z1, digMode = 'd') dx = x1 - x0 @@ -179,7 +302,6 @@ 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 @@ -296,8 +418,6 @@ 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 */ @@ -386,7 +506,6 @@ 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 @@ -421,8 +540,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 */ @@ -437,7 +554,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 @@ -445,12 +562,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 @@ -463,7 +579,6 @@ 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') # 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 @@ -721,57 +836,7 @@ 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 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 - end - end def floodfill(x,y,z,targetDig, digMode, maxCounter= 10000) #targetDig: what designation type can we overwrite? @@ -783,14 +848,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 @@ -808,7 +875,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 @@ -819,7 +886,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 @@ -831,18 +898,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 @@ -857,266 +926,318 @@ def floodfill(x,y,z,targetDig, digMode, maxCounter= 10000) # script execution start 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 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'" + stdout " To draw downstair: digshape downstair depth" + 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 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 " All commands accept a one letter digging designation [dujihrx] at the end, or will default to 'd'" throw :script_finished end command = $script_args[0] -argument1 = $script_args[1] -argument2 = $script_args[2] -argument3 = $script_args[3] +$script_args.delete_at(0) if df.cursor.x == -30000 then - puts " Error: cursor must be on map" - throw :script_finished + userSucks "Cursor must be on map" end -if command=="o" or command=="set" then #alias - command="origin" -elsif command=="keupo" or command=="stairs" or command=="downstairs" then - command="downstair" +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 -if command != 'undo' then - clearDigBuffer() #clear the dig buffer so we can undo the following command. Or initialize it's first run +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)") + if df.cursor.z != $origin.z then + userSucks(msg) + end + writeLuaPos("origin",$origin) # visualize them for the user 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) +def requireMajor(msg: "Set a point for the end of the major axis with the cursor and 'digshape major'") + 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 + +def getDigModeArgument(args) + argument = args[0] + digMode = getDigMode(argument) + args.delete_at(0) + + return digMode +end + +def getFilledArgument(args, default: false) # 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 - 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 + return default # doesn't consume if nothing matches + end + args.delete_at(0); + return filled +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 +def getFloatArgument(args, default: nil, type: "(unnamed number)", positive: true) + num = args[0] + result = nil + defaultMessage = "" - 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') + if default != nil then + defaultMessage = "Use `-' for the default value (#{default})" + end - # remove origin designation - digAt($originx, $originy, $originz, 'x') + 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 - 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 - - 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 + 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) + num = args[0] + result = nil + defaultMessage = "" - 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') + 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 - puts " Error: origin and target must be on the same z level" - throw :script_finished + 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 makeDefaultPosMap(oldPos) + return { + '~' => cursorAsDigPos(), # cursor positon + '-' => oldPos.clone() # old value of this (i.e. leave it unchanged) + } +end + +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 + +#def registerCommand(name, aliases, usage) +# return "TODO:" +#end + +case command + 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) + + setOrigin(newOrigin.x, newOrigin.y, newOrigin.z) # need to refactor setOrigin + + writeLuaPos("origin", $origin) + + 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 + + writeLuaPos("major", $major) + + + + 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 '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 '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() + floodfill(df.cursor.x, df.cursor.y, df.cursor.z, targetDig, digMode, maxArea) + 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..ce07a7b --- /dev/null +++ b/gui/digshape.lua @@ -0,0 +1,237 @@ +--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 +} + + +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"}, +} + +local digModeToButton = {} +for _, data in pairs(digButtons) do + data.keybind = ("CUSTOM_%s"):format(data.key:upper()) + digModeToButton[data.key] = data +end + +local buttons = { + {key="p", text="Set digshape command", 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", callback=function(self) + dfhack.run_command_silent("digshape lua origin") + self:runCurrentCommand(true) + end}, + {key="m", text="Set major", callback=function(self) + dfhack.run_command_silent("digshape lua major") + self:runCurrentCommand(true) + 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, COLOR_BLACK, self.activeDesgination=='x' and COLOR_RED or COLOR_BROWN) + end + + if self.origin then + paintMapTile(dc, vp, df.global.cursor, self.origin, '+', COLOR_YELLOW) + end + if self.major then + paintMapTile(dc, vp, df.global.cursor, self.major, '+', COLOR_LIGHTGREEN) + end + + +end + +function DigshapeUI:onRenderBody(dc) + self:renderOverlay() + + dc:clear():seek(1,1):pen(COLOR_WHITE):string("Digshape - Main menu") + dc:seek(1,3) + if true or self.state=="preview" then + for _, data in pairs(digButtons) do + builder = dc:key_string(data.keybind, data.text, self.activeDesgination==data.key and COLOR_WHITE or COLOR_GREY):newline(1) + if data.key=='x' then + builder:newline(1) + end + end + for _, data in pairs(buttons) do + builder = dc:key_string(data.keybind, data.text, COLOR_GREY):newline(1) + 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 From 59d9cfb613d68e677fbf1b553de21cdb4b4ac83d Mon Sep 17 00:00:00 2001 From: Quatch Date: Thu, 25 Mar 2021 18:10:06 -0400 Subject: [PATCH 2/6] Tidying and commenting Regrouped functions, added more comments. --- digshape.rb | 418 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 307 insertions(+), 111 deletions(-) diff --git a/digshape.rb b/digshape.rb index 295f826..7780c54 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,32 +39,89 @@ 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 draw a star using the origin as the center and the cursor as the radius|apothem (radius) + digshape star [skip] [digMode] To move all of the markers to the current z level: digshape resetz 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 -TODO: mark origin should not change the digging designation, ellipse cleanup should restore not clear it. +=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 -DigPos = Struct.new(:x, :y, :z) do - def to_s - return "(#{x},#{y},#{z})" - end - def clone - return DigPos.new(x,y,z) - end -end -def cursorAsDigPos() - return DigPos.new(df.cursor.x, df.cursor.y, df.cursor.z) -end -# really nasty hack so that lua can use digshape + + + + + + +=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 @@ -79,32 +140,18 @@ def writeLuaPos(name, digPos) # pos:::: end end -def setOrigin(x, y, z) # sets the origin and marks if it iff we are in console. cleans up last mark too. - $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) - $major = DigPos.new(x,y,z) -end + + +=begin +======================== UTILITY FUNCTIONS +=end + def stdout(msg) + # print a message to the console if $isLuaMode == false then puts msg else @@ -112,7 +159,8 @@ def stdout(msg) end end -def stderr(msg) # write an error mesage +def stderr(msg) + # print an error message to the console if $isLuaMode == false then puts " Error: "+msg else @@ -120,51 +168,66 @@ def stderr(msg) # write an error mesage end end -def scriptError(msg) # call this when you reach corner cases / the fault perhaps isn't the user +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 +def userSucks(msg) + # call this when we don't like the user's input. Script may continue. stderr(msg) throw :script_finished end -def undo() # BUG! Does not keep track of Z levels - #Execute one level of undo. - #z level is presumed to be the current. - # 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 + + + + + + +=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 - #clear buffer for next dig - $digBufferX = newBufferX - $digBufferY = newBufferY - $digBufferZ = newBufferZ - $digBufferD = newBufferD 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 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 @@ -196,6 +259,78 @@ def enum2digMode(digEnum) 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? @@ -211,6 +346,8 @@ def isDigPermitted(digMode, tileShape) 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) @@ -238,8 +375,16 @@ def digAt(x, y, z, digMode = 'd', buffer: true, bufferX: $digBufferX, bufferY: $ -# 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 @@ -264,6 +409,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 @@ -287,6 +434,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) @@ -303,11 +452,14 @@ def drawLine(x0, y0, z0, x1, y1, z1, digMode = 'd') 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 @@ -419,8 +571,9 @@ def plotQuadRationalBezierSeg(x0, y0, z0, x1, y1, z1, x2, y2, z2, w, 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 @@ -507,18 +660,20 @@ def plotQuadRationalBezier(x0, y0, z0, x1, y1, z1, x2, y2, z2, w=1.5, 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 @@ -554,7 +709,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. - stdout "zd=0 degenerate case" + stdout("zd=0 degenerate case") drawEllipse(x0,y0,z0, x1,y1,z0) return end @@ -565,7 +720,7 @@ def plotRotatedEllipseRect(x0, y0, z0, x1, y1, zd, digMode='d') end if not(w <= 1.0 && w >= 0.0) then #/* limit angle to |zd|<=xd*yd */ - scriptError "Limit angle to |zd|<=xd*yd" + scriptError("Limit angle to |zd|<=xd*yd") end ## snap xe,ye to int */ @@ -580,6 +735,9 @@ def plotRotatedEllipseRect(x0, y0, z0, x1, y1, zd, digMode='d') 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 @@ -587,6 +745,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 @@ -758,6 +923,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') @@ -774,15 +940,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 @@ -813,6 +973,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 @@ -836,9 +997,9 @@ def drawStar(x0, y0, z0, x1, y1, z1, n = 5, skip = 2, digMode = 'd') 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. @@ -848,7 +1009,7 @@ def floodfill(x,y,z,targetDig, digMode, maxCounter= 10000) if not t then #ignore impossible tiles (eg air.) - stdout "Tile does not exist" + stdout("Tile does not exist") throw :script_finished return end @@ -857,7 +1018,7 @@ def floodfill(x,y,z,targetDig, digMode, maxCounter= 10000) if t.designation.dig == digNum then #don't dig tiles that are already dug - stdout "Tile is already dug" + stdout("Tile is already dug") throw :script_finished return end @@ -898,8 +1059,8 @@ def floodfill(x,y,z,targetDig, digMode, maxCounter= 10000) counter = counter -1 if counter <=0 then - 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" + 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 @@ -923,10 +1084,21 @@ def floodfill(x,y,z,targetDig, digMode, maxCounter= 10000) end end -# script execution start + + + + + + + + +=begin +======================== SCRIPT CONTROL +script execution start +=end if not $script_args[0] or $script_args[0]=="help" or $script_args[0]=="?" then - stdout " To draw downstair: digshape downstair depth" + 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 " @@ -936,6 +1108,8 @@ def floodfill(x,y,z,targetDig, digMode, maxCounter= 10000) 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 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" @@ -947,7 +1121,7 @@ def floodfill(x,y,z,targetDig, digMode, maxCounter= 10000) $script_args.delete_at(0) if df.cursor.x == -30000 then - userSucks "Cursor must be on map" + userSucks("Cursor must be on map") end if not (command == 'undo' or command=='u') and not $isPreviewOnly then @@ -955,6 +1129,7 @@ def floodfill(x,y,z,targetDig, digMode, maxCounter= 10000) 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 @@ -962,6 +1137,7 @@ def requireOriginZLevel(msg: "Origin and target must be on the same z-level (use 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 @@ -971,14 +1147,20 @@ def requireMajor(msg: "Set a point for the end of the major axis with the cursor end 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) return digMode end -def getFilledArgument(args, default: false) # this doesn't *expect* and argument and so only consumes an argument when something matches +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 @@ -991,6 +1173,7 @@ def getFilledArgument(args, default: false) # this doesn't *expect* and argument end 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 = "" @@ -1020,6 +1203,7 @@ def getFloatArgument(args, default: nil, type: "(unnamed number)", positive: tru 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 = "" @@ -1082,10 +1266,20 @@ def noMoreArguments(args) end end + + + + +=begin +======================== DIGSHAPE COMMANDS +=end + + #def registerCommand(name, aliases, usage) # return "TODO:" #end + case command when 'origin', 'o', 'set' # $usage = createUsage(name: 'origin', aliases: ['o', 'set']) # TODO ADD USAGES TO EACH COMMANDS, make scriptError/userSucks print them out @@ -1108,9 +1302,8 @@ def noMoreArguments(args) writeLuaPos("major", $major) - + stdout("Now move the cursor to the minor axis radius (extent) and call ellipse3p") - stdout "Now move the cursor to the minor axis radius (extent) and call ellipse3p" when 'resetz', 'setz' z = df.cursor.z # default @@ -1123,10 +1316,11 @@ def noMoreArguments(args) if $major then setMajor($major.x, $major.y, z) end + when 'ls', 'status' - stdout "origin: #{$origin != nil ? $origin.to_s : ''}" - stdout "major : #{$major != nil ? $major.to_s : ''}" - stdout "cursor: #{cursorAsDigPos().to_s}" + 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) @@ -1163,7 +1357,7 @@ def noMoreArguments(args) requireMajor() if filled then - stdout "Filled not yet supported for 3p ellipses." + stdout("Filled not yet supported for 3p ellipses.") filled = false end @@ -1215,10 +1409,11 @@ def noMoreArguments(args) noMoreArguments($script_args) if depth <= 0 then - userSucks "Depth must be an integer greater than zero" + userSucks("Depth must be an integer greater than zero") 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) @@ -1230,14 +1425,15 @@ def noMoreArguments(args) if targetDig != :No then if targetDig == digMode2enum(digMode) then - userSucks "Floodfill must be centered on an undesignated/matching tile." + userSucks("Floodfill must be centered on an undesignated/matching tile.") end end floodfill(df.cursor.x, df.cursor.y, df.cursor.z, targetDig, digMode, maxArea) + when 'undo', 'u' undo() else - userSucks "Invalid command" + userSucks("Invalid command") end \ No newline at end of file From 3907dea586f640499dacb747ac025d9f0d85f828 Mon Sep 17 00:00:00 2001 From: Quatch Date: Thu, 25 Mar 2021 15:17:25 -0400 Subject: [PATCH 3/6] digshape refactor and gui, from flavourstreet Added flavourstreet's refactor of digshape, and it's new lua gui [keupo's discord, 2021-02-17] [8:10 PM]flavorstreet: Attachment file type: archive digshape-gui-ALPHA.zip 14.32 KB [8:12 PM]flavorstreet: Yall can do whatever u want with it i'll probably never get around to fixing the bugs im so burned out lol --811767111344193566 --- digshape.rb | 806 +++++++++++++++++++++++++++-------------------- gui/digshape.lua | 237 ++++++++++++++ 2 files changed, 695 insertions(+), 348 deletions(-) create mode 100644 gui/digshape.lua diff --git a/digshape.rb b/digshape.rb index 919ba50..7fc8013 100644 --- a/digshape.rb +++ b/digshape.rb @@ -51,33 +51,113 @@ TODO: mark origin should not change the digging designation, ellipse cleanup should restore not clear it. =end +DigPos = Struct.new(:x, :y, :z) do + def to_s + return "(#{x},#{y},#{z})" + end + def clone + return DigPos.new(x,y,z) + end +end + +def cursorAsDigPos() + return DigPos.new(df.cursor.x, df.cursor.y, df.cursor.z) +end + +# really nasty hack so that lua can use digshape +$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 setOrigin(x, y, z) # sets the origin and marks if it iff we are in console. cleans up last mark too. + $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) + $major = DigPos.new(x,y,z) +end + +def stdout(msg) + if $isLuaMode == false then + puts msg + else + puts "msg:"+msg + end +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 +def stderr(msg) # write an error mesage + 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 + stderr(msg) + raise "oopsie! script errored! ;)" +end + +def userSucks(msg) # call this when we don't like the user's input + stderr(msg) + throw :script_finished +end + + -def unDig() - #Exicute one level of undo. +def undo() # BUG! Does not keep track of Z levels + #Execute one level of undo. #z level is presumed to be the current. + # todo, have multiple levels of undo / redo i=$digBufferX.length - while i >= 0 do + 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,df.cursor.z, enum2dig(d), buffer=false) + 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. - clearDigBuffer() + end + #clear buffer for next dig + $digBufferX = newBufferX + $digBufferY = newBufferY + $digBufferZ = newBufferZ + $digBufferD = newBufferD end @@ -85,40 +165,83 @@ def clearDigBuffer() #clear buffer for next dig, or initialize it's existance on first run. $digBufferX=[] $digBufferY=[] + $digBufferZ=[] $digBufferD=[] 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 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 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 + +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) + 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 def drawLineLow(x0, y0, z0, x1, y1, z1, digMode = 'd') dx = x1 - x0 @@ -183,7 +306,6 @@ 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 @@ -300,8 +422,6 @@ 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 */ @@ -390,7 +510,6 @@ 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 @@ -425,8 +544,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 +558,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 +566,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,7 +583,6 @@ 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') # 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 @@ -725,57 +840,7 @@ 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 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 - end - end def floodfill(x,y,z,targetDig, digMode, maxCounter= 10000) #targetDig: what designation type can we overwrite? @@ -787,14 +852,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 +879,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 +890,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 +902,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 @@ -907,283 +976,324 @@ def drawSpiral(x0, y0, z0, x1, y1, z1, coils, chord = 10, digMode = 'd') # script execution start 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): + stdout " To draw downstair: digshape downstair depth" + 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 " - 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'" + 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 " All commands accept a one letter digging designation [dujihrx] at the end, or will default to 'd'" throw :script_finished end command = $script_args[0] -argument1 = $script_args[1] -argument2 = $script_args[2] -argument3 = $script_args[3] +$script_args.delete_at(0) if df.cursor.x == -30000 then - puts " Error: cursor must be on map" - throw :script_finished + userSucks "Cursor must be on map" end -if command=="o" or command=="set" then #alias - command="origin" -elsif command=="keupo" or command=="stairs" or command=="downstairs" then - command="downstair" +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 -if command != 'undo' then - clearDigBuffer() #clear the dig buffer so we can undo the following command. Or initialize it's first run +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)") + if df.cursor.z != $origin.z then + userSucks(msg) + end + writeLuaPos("origin",$origin) # visualize them for the user 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) +def requireMajor(msg: "Set a point for the end of the major axis with the cursor and 'digshape major'") + 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 + +def getDigModeArgument(args) + argument = args[0] + digMode = getDigMode(argument) + args.delete_at(0) + + return digMode +end + +def getFilledArgument(args, default: false) # 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 - 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 + return default # doesn't consume if nothing matches + end + args.delete_at(0); + return filled +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 +def getFloatArgument(args, default: nil, type: "(unnamed number)", positive: true) + num = args[0] + result = nil + defaultMessage = "" - 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') + if default != nil then + defaultMessage = "Use `-' for the default value (#{default})" + end - # remove origin designation - digAt($originx, $originy, $originz, 'x') + 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 - 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 - - 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 + 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) + num = args[0] + result = nil + defaultMessage = "" + + if default != nil then + defaultMessage = "Use `-' for the default value (#{default})" + end - 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') + 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 - puts " Error: origin and target must be on the same z level" - throw :script_finished + 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 makeDefaultPosMap(oldPos) + return { + '~' => cursorAsDigPos(), # cursor positon + '-' => oldPos.clone() # old value of this (i.e. leave it unchanged) + } +end + +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 + +#def registerCommand(name, aliases, usage) +# return "TODO:" +#end + +case command + 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) + + setOrigin(newOrigin.x, newOrigin.y, newOrigin.z) # need to refactor setOrigin + + writeLuaPos("origin", $origin) + + 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 + + writeLuaPos("major", $major) + + + + 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 '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 + floodfill(df.cursor.x, df.cursor.y, df.cursor.z, targetDig, dig, maxArea) 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) - - drawSpiral($originx, $originy, $originz, df.cursor.x, df.cursor.y, df.cursor.x, coils, chord, dig) - end 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..ce07a7b --- /dev/null +++ b/gui/digshape.lua @@ -0,0 +1,237 @@ +--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 +} + + +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"}, +} + +local digModeToButton = {} +for _, data in pairs(digButtons) do + data.keybind = ("CUSTOM_%s"):format(data.key:upper()) + digModeToButton[data.key] = data +end + +local buttons = { + {key="p", text="Set digshape command", 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", callback=function(self) + dfhack.run_command_silent("digshape lua origin") + self:runCurrentCommand(true) + end}, + {key="m", text="Set major", callback=function(self) + dfhack.run_command_silent("digshape lua major") + self:runCurrentCommand(true) + 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, COLOR_BLACK, self.activeDesgination=='x' and COLOR_RED or COLOR_BROWN) + end + + if self.origin then + paintMapTile(dc, vp, df.global.cursor, self.origin, '+', COLOR_YELLOW) + end + if self.major then + paintMapTile(dc, vp, df.global.cursor, self.major, '+', COLOR_LIGHTGREEN) + end + + +end + +function DigshapeUI:onRenderBody(dc) + self:renderOverlay() + + dc:clear():seek(1,1):pen(COLOR_WHITE):string("Digshape - Main menu") + dc:seek(1,3) + if true or self.state=="preview" then + for _, data in pairs(digButtons) do + builder = dc:key_string(data.keybind, data.text, self.activeDesgination==data.key and COLOR_WHITE or COLOR_GREY):newline(1) + if data.key=='x' then + builder:newline(1) + end + end + for _, data in pairs(buttons) do + builder = dc:key_string(data.keybind, data.text, COLOR_GREY):newline(1) + 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 From 96eae8ce8b767753bcdf50ee9da4bd69dcb10297 Mon Sep 17 00:00:00 2001 From: Quatch Date: Thu, 25 Mar 2021 22:05:15 -0400 Subject: [PATCH 4/6] Tidying and commenting Regrouped functions, added more comments. --- digshape.rb | 519 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 356 insertions(+), 163 deletions(-) diff --git a/digshape.rb b/digshape.rb index 7fc8013..3fb2f60 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,36 +39,92 @@ 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 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): digshape spiral + To move all of the markers to the current z level: + digshape resetz 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 -TODO: mark origin should not change the digging designation, ellipse cleanup should restore not clear it. +=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 -DigPos = Struct.new(:x, :y, :z) do - def to_s - return "(#{x},#{y},#{z})" - end - def clone - return DigPos.new(x,y,z) - end -end -def cursorAsDigPos() - return DigPos.new(df.cursor.x, df.cursor.y, df.cursor.z) -end -# really nasty hack so that lua can use digshape + + + + + + +=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 @@ -83,32 +143,18 @@ def writeLuaPos(name, digPos) # pos:::: end end -def setOrigin(x, y, z) # sets the origin and marks if it iff we are in console. cleans up last mark too. - $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) - $major = DigPos.new(x,y,z) -end + + +=begin +======================== UTILITY FUNCTIONS +=end + def stdout(msg) + # print a message to the console if $isLuaMode == false then puts msg else @@ -116,7 +162,8 @@ def stdout(msg) end end -def stderr(msg) # write an error mesage +def stderr(msg) + # print an error message to the console if $isLuaMode == false then puts " Error: "+msg else @@ -124,51 +171,66 @@ def stderr(msg) # write an error mesage end end -def scriptError(msg) # call this when you reach corner cases / the fault perhaps isn't the user +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 +def userSucks(msg) + # call this when we don't like the user's input. Script may continue. stderr(msg) throw :script_finished end -def undo() # BUG! Does not keep track of Z levels - #Execute one level of undo. - #z level is presumed to be the current. - # 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 + + + + + + +=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 - #clear buffer for next dig - $digBufferX = newBufferX - $digBufferY = newBufferY - $digBufferZ = newBufferZ - $digBufferD = newBufferD 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 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 @@ -200,6 +262,78 @@ def enum2digMode(digEnum) 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? @@ -215,6 +349,8 @@ def isDigPermitted(digMode, tileShape) 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) @@ -242,8 +378,16 @@ def digAt(x, y, z, digMode = 'd', buffer: true, bufferX: $digBufferX, bufferY: $ -# 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 @@ -268,6 +412,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 @@ -291,6 +437,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) @@ -307,11 +455,14 @@ def drawLine(x0, y0, z0, x1, y1, z1, digMode = 'd') 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 @@ -423,8 +574,9 @@ def plotQuadRationalBezierSeg(x0, y0, z0, x1, y1, z1, x2, y2, z2, w, 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 @@ -511,18 +663,20 @@ def plotQuadRationalBezier(x0, y0, z0, x1, y1, z1, x2, y2, z2, w=1.5, 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 @@ -558,7 +712,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. - stdout "zd=0 degenerate case" + stdout("zd=0 degenerate case") drawEllipse(x0,y0,z0, x1,y1,z0) return end @@ -569,7 +723,7 @@ def plotRotatedEllipseRect(x0, y0, z0, x1, y1, zd, digMode='d') end if not(w <= 1.0 && w >= 0.0) then #/* limit angle to |zd|<=xd*yd */ - scriptError "Limit angle to |zd|<=xd*yd" + scriptError("Limit angle to |zd|<=xd*yd") end ## snap xe,ye to int */ @@ -584,6 +738,9 @@ def plotRotatedEllipseRect(x0, y0, z0, x1, y1, zd, digMode='d') 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 @@ -591,6 +748,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 @@ -762,6 +926,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') @@ -778,15 +943,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 @@ -817,6 +976,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 @@ -840,9 +1000,54 @@ def drawStar(x0, y0, z0, x1, y1, z1, n = 5, skip = 2, digMode = 'd') 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 + + # 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 + + 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. @@ -852,7 +1057,7 @@ def floodfill(x,y,z,targetDig, digMode, maxCounter= 10000) if not t then #ignore impossible tiles (eg air.) - stdout "Tile does not exist" + stdout("Tile does not exist") throw :script_finished return end @@ -861,7 +1066,7 @@ def floodfill(x,y,z,targetDig, digMode, maxCounter= 10000) if t.designation.dig == digNum then #don't dig tiles that are already dug - stdout "Tile is already dug" + stdout("Tile is already dug") throw :script_finished return end @@ -902,8 +1107,8 @@ def floodfill(x,y,z,targetDig, digMode, maxCounter= 10000) counter = counter -1 if counter <=0 then - 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" + 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 @@ -928,55 +1133,17 @@ 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 - - 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 -# script execution start +=begin +======================== SCRIPT CONTROL +script execution start +=end if not $script_args[0] or $script_args[0]=="help" or $script_args[0]=="?" then - stdout " To draw downstair: digshape downstair depth" + 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 " @@ -988,6 +1155,8 @@ def drawSpiral(x0, y0, z0, x1, y1, z1, coils, chord = 10, digMode = 'd') 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" @@ -999,7 +1168,7 @@ def drawSpiral(x0, y0, z0, x1, y1, z1, coils, chord = 10, digMode = 'd') $script_args.delete_at(0) if df.cursor.x == -30000 then - userSucks "Cursor must be on map" + userSucks("Cursor must be on map") end if not (command == 'undo' or command=='u') and not $isPreviewOnly then @@ -1007,6 +1176,7 @@ def drawSpiral(x0, y0, z0, x1, y1, z1, coils, chord = 10, digMode = 'd') 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 @@ -1014,6 +1184,7 @@ def requireOriginZLevel(msg: "Origin and target must be on the same z-level (use 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 @@ -1023,14 +1194,20 @@ def requireMajor(msg: "Set a point for the end of the major axis with the cursor end 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) return digMode end -def getFilledArgument(args, default: false) # this doesn't *expect* and argument and so only consumes an argument when something matches +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 @@ -1043,6 +1220,7 @@ def getFilledArgument(args, default: false) # this doesn't *expect* and argument end 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 = "" @@ -1072,6 +1250,7 @@ def getFloatArgument(args, default: nil, type: "(unnamed number)", positive: tru 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 = "" @@ -1134,16 +1313,25 @@ def noMoreArguments(args) end end + + + + +=begin +======================== DIGSHAPE COMMANDS +=end + + #def registerCommand(name, aliases, usage) # return "TODO:" #end + case command 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) setOrigin(newOrigin.x, newOrigin.y, newOrigin.z) # need to refactor setOrigin @@ -1160,9 +1348,8 @@ def noMoreArguments(args) writeLuaPos("major", $major) - + stdout("Now move the cursor to the minor axis radius (extent) and call ellipse3p") - stdout "Now move the cursor to the minor axis radius (extent) and call ellipse3p" when 'resetz', 'setz' z = df.cursor.z # default @@ -1175,10 +1362,11 @@ def noMoreArguments(args) if $major then setMajor($major.x, $major.y, z) end + when 'ls', 'status' - stdout "origin: #{$origin != nil ? $origin.to_s : ''}" - stdout "major : #{$major != nil ? $major.to_s : ''}" - stdout "cursor: #{cursorAsDigPos().to_s}" + 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) @@ -1215,7 +1403,7 @@ def noMoreArguments(args) requireMajor() if filled then - stdout "Filled not yet supported for 3p ellipses." + stdout("Filled not yet supported for 3p ellipses.") filled = false end @@ -1242,7 +1430,6 @@ def noMoreArguments(args) end digMode = getDigModeArgument($script_args) - noMoreArguments($script_args) requireOriginZLevel() @@ -1260,23 +1447,26 @@ def noMoreArguments(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) + 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) - 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" + userSucks("Depth must be an integer greater than zero") 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) @@ -1288,12 +1478,15 @@ def noMoreArguments(args) if targetDig != :No then if targetDig == digMode2enum(digMode) then - userSucks "Floodfill must be centered on an undesignated/matching tile." + 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) - when 'undo' - unDig() + + floodfill(df.cursor.x, df.cursor.y, df.cursor.z, targetDig, digMode, maxArea) + + when 'undo', 'u' + undo() + else - userSucks "Invalid command" + userSucks("Invalid command") end \ No newline at end of file From ad6c0a8c379f278d65753e85f1c6879e469246b2 Mon Sep 17 00:00:00 2001 From: Quatch Date: Fri, 26 Mar 2021 02:11:15 -0400 Subject: [PATCH 5/6] Added 'swap' command added swap to digshape, and gui. added better pen options for gui. added a bit of chrome to menu. --- digshape.rb | 20 ++++++++++ gui/digshape.lua | 95 ++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 96 insertions(+), 19 deletions(-) diff --git a/digshape.rb b/digshape.rb index 3a2424f..52b7af0 100644 --- a/digshape.rb +++ b/digshape.rb @@ -49,6 +49,9 @@ 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 @@ -1161,6 +1164,7 @@ def floodfill(x,y,z,targetDig, digMode, maxCounter= 10000) 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 @@ -1363,6 +1367,22 @@ def noMoreArguments(args) if $major then setMajor($major.x, $major.y, z) 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 : ''}") diff --git a/gui/digshape.lua b/gui/digshape.lua index ce07a7b..44c5559 100644 --- a/gui/digshape.lua +++ b/gui/digshape.lua @@ -21,8 +21,33 @@ DigshapeUI.ATTRS { currentCommand = 'circle hollow @', currentOutput = {}, currentError = {}, - currentDig = {} + 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 }, } @@ -34,29 +59,44 @@ local digButtons={ {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_%s"):format(data.key:upper()) + 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="p", text="Set digshape command", callback=function(self) + --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", callback=function(self) + {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="m", text="Set major", callback=function(self) + {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}, @@ -74,14 +114,14 @@ 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)) + 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) + print("output=", output) for line in output:gmatch("[^\r\n]+") do messageType = line:match("^([^:]+):") if messageType == "msg" then @@ -140,14 +180,14 @@ function DigshapeUI:renderOverlay() end for _, dig in ipairs(self.currentDig) do - paintMapTile(dc, vp, df.global.cursor, xyz2pos(dig.x, dig.y, dig.z), dig.symbol, COLOR_BLACK, self.activeDesgination=='x' and COLOR_RED or COLOR_BROWN) + 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, '+', COLOR_YELLOW) + paintMapTile(dc, vp, df.global.cursor, self.origin, '+', self.pens['origin']) end if self.major then - paintMapTile(dc, vp, df.global.cursor, self.major, '+', COLOR_LIGHTGREEN) + paintMapTile(dc, vp, df.global.cursor, self.major, '+', self.pens['major']) end @@ -156,21 +196,38 @@ end function DigshapeUI:onRenderBody(dc) self:renderOverlay() - dc:clear():seek(1,1):pen(COLOR_WHITE):string("Digshape - Main menu") + 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:key_string(data.keybind, data.text, self.activeDesgination==data.key and COLOR_WHITE or COLOR_GREY):newline(1) - if data.key=='x' then - builder:newline(1) - end + 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 - builder = dc:key_string(data.keybind, data.text, COLOR_GREY):newline(1) - if data.key=='m' then - builder:newline(1) + 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 - end --[[ dc:key_string("CUSTOM_S", "Set Brush",COLOR_GREY) From 742b064e17dfcdb544afcc9e728e8ada7faca52b Mon Sep 17 00:00:00 2001 From: Quatch Date: Fri, 26 Mar 2021 02:24:43 -0400 Subject: [PATCH 6/6] Update README.md --- README.md | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) 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