diff --git a/changelog.txt b/changelog.txt index 9b676acc1e..e251accf7c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -16,6 +16,8 @@ Template for new versions: ## New Features +- `gui/design`: add option to draw N-point stars, hollow or filled or inverted, and change the main axis to orient in any direction + ## Fixes ## Misc Improvements diff --git a/gui/design.lua b/gui/design.lua index 103ab59c8e..5cd3f1d824 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -336,7 +336,14 @@ function Design:init() table.insert(mode_button_specs_selected, mode_option.value.button_selected_spec) end - local shape_tileset = dfhack.textures.loadTileset('hack/data/art/design.png', 8, 12, true) + local DESIGN_ICONS_WIDTH = 224 -- Must match design.png image width + local DESIGN_ICONS_HEIGHT = 72 -- Must match design.png image height + local DESIGN_ICON_ROW_COUNT = 2 + local DESIGN_CHAR_WIDTH = 8 + local DESIGN_CHAR_HEIGHT = 12 + local shape_tileset = dfhack.textures.loadTileset('hack/data/art/design.png', DESIGN_CHAR_WIDTH, DESIGN_CHAR_HEIGHT, true) + local STRIDE = DESIGN_ICONS_WIDTH / DESIGN_CHAR_WIDTH + local CHARS_PER_ROW = DESIGN_ICONS_HEIGHT / (DESIGN_ICON_ROW_COUNT * DESIGN_CHAR_HEIGHT) local shape_options, shape_button_specs, shape_button_specs_selected = {}, {}, {} for _, shape in ipairs(shapes.all_shapes) do table.insert(shape_options, {label=shape.name, value=shape}) @@ -344,14 +351,14 @@ function Design:init() chars=shape.button_chars, tileset=shape_tileset, tileset_offset=shape.texture_offset, - tileset_stride=24, + tileset_stride=STRIDE, }) table.insert(shape_button_specs_selected, { chars=shape.button_chars, pens=COLOR_YELLOW, tileset=shape_tileset, - tileset_offset=shape.texture_offset+(24*3), - tileset_stride=24, + tileset_offset=shape.texture_offset+(STRIDE*CHARS_PER_ROW), + tileset_stride=STRIDE, }) end diff --git a/internal/design/shapes.lua b/internal/design/shapes.lua index 5199aa1d10..a9eb9897d8 100644 --- a/internal/design/shapes.lua +++ b/internal/design/shapes.lua @@ -402,7 +402,9 @@ function Diag:has_point(x, y) end end -Line = defclass(Line, Shape) +LineDrawer = defclass(LineDrawer, Shape) + +Line = defclass(Line, LineDrawer) Line.ATTRS { name = "Line", extra_points = { { label = "Curve Point" }, { label = "Second Curve Point" } }, @@ -431,18 +433,19 @@ function Line:init() } end -function Line:plot_bresenham(x0, y0, x1, y1, thickness) +function LineDrawer:plot_bresenham(x0, y0, x1, y1, thickness) local dx = math.abs(x1 - x0) local dy = math.abs(y1 - y0) local sx = x0 < x1 and 1 or -1 local sy = y0 < y1 and 1 or -1 - local err = dx - dy local e2, x, y for i = 0, thickness - 1 do x = x0 y = y0 + i - while true do + local err = dx - dy + local p = math.max(dx, dy) + while p >= 0 do for j = -math.floor(thickness / 2), math.ceil(thickness / 2) - 1 do if not self.arr[x + j] then self.arr[x + j] = {} end if not self.arr[x + j][y] then @@ -451,7 +454,7 @@ function Line:plot_bresenham(x0, y0, x1, y1, thickness) end end - if x == x1 and y == y1 + i then + if sx * x >= sx * x1 and sy * y >= sy * (y1 + i) then break end @@ -466,6 +469,7 @@ function Line:plot_bresenham(x0, y0, x1, y1, thickness) err = err + dx y = y + sy end + p = p - 1 end end @@ -684,7 +688,137 @@ function FreeForm:point_in_polygon(x, y) return inside end +Star = defclass(Star, LineDrawer) +Star.ATTRS { + name = "Star", + texture_offset = 25, + button_chars = util.make_ascii_button('*', 15), + extra_points = { { label = "Main Axis" } } +} + +function Star:init() + self.options = { + hollow = { + name = "Hollow", + type = "bool", + value = false, + key = "CUSTOM_H", + }, + thickness = { + name = "Line thickness", + type = "plusminus", + value = 1, + enabled = { "hollow", true }, + min = 1, + max = function(shape) if not shape.height or not shape.width then + return nil + else + return math.ceil(math.min(shape.height, shape.width) / 2) + + end + end, + keys = { "CUSTOM_T", "CUSTOM_SHIFT_T" }, + }, + total_points = { + name = "Total points", + type = "plusminus", + value = 5, + min = 3, + max = 100, + keys = { "CUSTOM_B", "CUSTOM_SHIFT_B" }, + }, + next_point_offset = { + name = "Next point offset", + type = "plusminus", + value = 2, + min = 1, + max = 100, + keys = { "CUSTOM_N", "CUSTOM_SHIFT_N" }, + }, + } +end + +function Star:has_point(x, y) + if 1 < ((x-self.center.x) / self.center.x) ^ 2 + ((y-self.center.y) / self.center.y) ^ 2 then return false end + + local inside = 0 + for l = 1, self.options.total_points.value do + if x * self.lines[l].slope.x - self.lines[l].intercept.x < y * self.lines[l].slope.y - self.lines[l].intercept.y then + inside = inside + 1 + else + inside = inside - 1 + end + end + return self.threshold > 0 and inside > self.threshold or inside < self.threshold +end + +function vmagnitude(point) + return math.sqrt(point.x * point.x + point.y * point.y) +end + +function vnormalize(point) + local magnitude = vmagnitude(point) + return { x = point.x / magnitude, y = point.y / magnitude } +end + +function add_offset(coord, offset) + return coord + (offset > 0 and math.floor(offset+0.5) or math.ceil(offset-0.5)) +end + +function Star:update(points, extra_points) + self.num_tiles = 0 + self.points = copyall(points) + self.arr = {} + if #points < self.min_points then return end + self.threshold = self.options.total_points.value - 2 * self.options.next_point_offset.value + local top_left, bot_right = self:get_point_dims() + self.height = bot_right.y - top_left.y + self.width = bot_right.x - top_left.x + if self.height == 1 or self.width == 1 then return end + self.center = { x = self.width * 0.5, y = self.height * 0.5 } + local axes = {} + + axes[1] = (#extra_points > 0) and { x = extra_points[1].x - self.center.x - top_left.x, y = extra_points[1].y - self.center.y - top_left.y } or { x = 0, y = -self.center.y } + if vmagnitude(axes[1]) < 0.5 then axes[1].y = -self.center.y end + axes[1] = vnormalize(axes[1]) + + for a = 2, self.options.total_points.value do + local angle = math.pi * (a - 1.0) * 2.0 / self.options.total_points.value + axes[a] = { x = math.cos(angle) * axes[1].x - math.sin(angle) * axes[1].y, y = math.sin(angle) * axes[1].x + math.cos(angle) * axes[1].y } + end + + local thickness = 1 + if self.options.hollow.value then + thickness = self.options.thickness.value + end + + self.lines = {} + for l = 1, self.options.total_points.value do + local p1 = { x = self.center.x + axes[l].x * self.width * 0.5, y = self.center.y + axes[l].y * self.height * 0.5 } + local next_axis = axes[(l-1+self.options.next_point_offset.value) % self.options.total_points.value + 1] + local p2 = { x = self.center.x + next_axis.x * self.width * 0.5, y = self.center.y + next_axis.y * self.height * 0.5 } + self.lines[l] = { slope = { x = p2.y - p1.y, y = p2.x - p1.x }, intercept = { x = (p2.y - p1.y) * p1.x, y = (p2.x - p1.x) * p1.y } } + self:plot_bresenham(add_offset(top_left.x, p1.x), add_offset(top_left.y, p1.y), add_offset(top_left.x, p2.x), add_offset(top_left.y, p2.y), thickness) + self:plot_bresenham(add_offset(top_left.x, p2.x), add_offset(top_left.y, p2.y), add_offset(top_left.x, p1.x), add_offset(top_left.y, p1.y), thickness) + end + + if not self.options.hollow.value or self.invert then + for x = top_left.x, bot_right.x do + if not self.arr[x] then self.arr[x] = {} end + for y = top_left.y, bot_right.y do + local value = self.arr[x][y] or (not self.options.hollow.value and self:has_point(x - top_left.x, y - top_left.y)) + if self.invert then + self.arr[x][y] = not value + else + self.arr[x][y] = value + end + + self.num_tiles = self.num_tiles + (self.arr[x][y] and 1 or 0) + end + end + end +end -- module users can get shapes through this global, shape option values -- persist in these as long as the module is loaded -- idk enough lua to know if this is okay to do or not -all_shapes = { Rectangle {}, Ellipse {}, Rows {}, Diag {}, Line {}, FreeForm {} } +all_shapes = { Rectangle {}, Ellipse {}, Rows {}, Diag {}, Line {}, FreeForm {}, Star {} }