Skip to content

Commit 0f8985b

Browse files
authored
Merge pull request #1438 from ryanbennitt/feature/design_star
gui/design: add option to draw N-point stars, hollow or filled or inverted, and change the main axis to orient in any direction
2 parents 3f82177 + 6c6aeea commit 0f8985b

File tree

3 files changed

+153
-10
lines changed

3 files changed

+153
-10
lines changed

changelog.txt

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ Template for new versions:
1616

1717
## New Features
1818

19+
- `gui/design`: add option to draw N-point stars, hollow or filled or inverted, and change the main axis to orient in any direction
20+
1921
## Fixes
2022

2123
## Misc Improvements

gui/design.lua

+11-4
Original file line numberDiff line numberDiff line change
@@ -336,22 +336,29 @@ function Design:init()
336336
table.insert(mode_button_specs_selected, mode_option.value.button_selected_spec)
337337
end
338338

339-
local shape_tileset = dfhack.textures.loadTileset('hack/data/art/design.png', 8, 12, true)
339+
local DESIGN_ICONS_WIDTH = 224 -- Must match design.png image width
340+
local DESIGN_ICONS_HEIGHT = 72 -- Must match design.png image height
341+
local DESIGN_ICON_ROW_COUNT = 2
342+
local DESIGN_CHAR_WIDTH = 8
343+
local DESIGN_CHAR_HEIGHT = 12
344+
local shape_tileset = dfhack.textures.loadTileset('hack/data/art/design.png', DESIGN_CHAR_WIDTH, DESIGN_CHAR_HEIGHT, true)
345+
local STRIDE = DESIGN_ICONS_WIDTH / DESIGN_CHAR_WIDTH
346+
local CHARS_PER_ROW = DESIGN_ICONS_HEIGHT / (DESIGN_ICON_ROW_COUNT * DESIGN_CHAR_HEIGHT)
340347
local shape_options, shape_button_specs, shape_button_specs_selected = {}, {}, {}
341348
for _, shape in ipairs(shapes.all_shapes) do
342349
table.insert(shape_options, {label=shape.name, value=shape})
343350
table.insert(shape_button_specs, {
344351
chars=shape.button_chars,
345352
tileset=shape_tileset,
346353
tileset_offset=shape.texture_offset,
347-
tileset_stride=24,
354+
tileset_stride=STRIDE,
348355
})
349356
table.insert(shape_button_specs_selected, {
350357
chars=shape.button_chars,
351358
pens=COLOR_YELLOW,
352359
tileset=shape_tileset,
353-
tileset_offset=shape.texture_offset+(24*3),
354-
tileset_stride=24,
360+
tileset_offset=shape.texture_offset+(STRIDE*CHARS_PER_ROW),
361+
tileset_stride=STRIDE,
355362
})
356363
end
357364

internal/design/shapes.lua

+140-6
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,9 @@ function Diag:has_point(x, y)
402402
end
403403
end
404404

405-
Line = defclass(Line, Shape)
405+
LineDrawer = defclass(LineDrawer, Shape)
406+
407+
Line = defclass(Line, LineDrawer)
406408
Line.ATTRS {
407409
name = "Line",
408410
extra_points = { { label = "Curve Point" }, { label = "Second Curve Point" } },
@@ -431,18 +433,19 @@ function Line:init()
431433
}
432434
end
433435

434-
function Line:plot_bresenham(x0, y0, x1, y1, thickness)
436+
function LineDrawer:plot_bresenham(x0, y0, x1, y1, thickness)
435437
local dx = math.abs(x1 - x0)
436438
local dy = math.abs(y1 - y0)
437439
local sx = x0 < x1 and 1 or -1
438440
local sy = y0 < y1 and 1 or -1
439-
local err = dx - dy
440441
local e2, x, y
441442

442443
for i = 0, thickness - 1 do
443444
x = x0
444445
y = y0 + i
445-
while true do
446+
local err = dx - dy
447+
local p = math.max(dx, dy)
448+
while p >= 0 do
446449
for j = -math.floor(thickness / 2), math.ceil(thickness / 2) - 1 do
447450
if not self.arr[x + j] then self.arr[x + j] = {} end
448451
if not self.arr[x + j][y] then
@@ -451,7 +454,7 @@ function Line:plot_bresenham(x0, y0, x1, y1, thickness)
451454
end
452455
end
453456

454-
if x == x1 and y == y1 + i then
457+
if sx * x >= sx * x1 and sy * y >= sy * (y1 + i) then
455458
break
456459
end
457460

@@ -466,6 +469,7 @@ function Line:plot_bresenham(x0, y0, x1, y1, thickness)
466469
err = err + dx
467470
y = y + sy
468471
end
472+
p = p - 1
469473
end
470474
end
471475

@@ -684,7 +688,137 @@ function FreeForm:point_in_polygon(x, y)
684688
return inside
685689
end
686690

691+
Star = defclass(Star, LineDrawer)
692+
Star.ATTRS {
693+
name = "Star",
694+
texture_offset = 25,
695+
button_chars = util.make_ascii_button('*', 15),
696+
extra_points = { { label = "Main Axis" } }
697+
}
698+
699+
function Star:init()
700+
self.options = {
701+
hollow = {
702+
name = "Hollow",
703+
type = "bool",
704+
value = false,
705+
key = "CUSTOM_H",
706+
},
707+
thickness = {
708+
name = "Line thickness",
709+
type = "plusminus",
710+
value = 1,
711+
enabled = { "hollow", true },
712+
min = 1,
713+
max = function(shape) if not shape.height or not shape.width then
714+
return nil
715+
else
716+
return math.ceil(math.min(shape.height, shape.width) / 2)
717+
718+
end
719+
end,
720+
keys = { "CUSTOM_T", "CUSTOM_SHIFT_T" },
721+
},
722+
total_points = {
723+
name = "Total points",
724+
type = "plusminus",
725+
value = 5,
726+
min = 3,
727+
max = 100,
728+
keys = { "CUSTOM_B", "CUSTOM_SHIFT_B" },
729+
},
730+
next_point_offset = {
731+
name = "Next point offset",
732+
type = "plusminus",
733+
value = 2,
734+
min = 1,
735+
max = 100,
736+
keys = { "CUSTOM_N", "CUSTOM_SHIFT_N" },
737+
},
738+
}
739+
end
740+
741+
function Star:has_point(x, y)
742+
if 1 < ((x-self.center.x) / self.center.x) ^ 2 + ((y-self.center.y) / self.center.y) ^ 2 then return false end
743+
744+
local inside = 0
745+
for l = 1, self.options.total_points.value do
746+
if x * self.lines[l].slope.x - self.lines[l].intercept.x < y * self.lines[l].slope.y - self.lines[l].intercept.y then
747+
inside = inside + 1
748+
else
749+
inside = inside - 1
750+
end
751+
end
752+
return self.threshold > 0 and inside > self.threshold or inside < self.threshold
753+
end
754+
755+
function vmagnitude(point)
756+
return math.sqrt(point.x * point.x + point.y * point.y)
757+
end
758+
759+
function vnormalize(point)
760+
local magnitude = vmagnitude(point)
761+
return { x = point.x / magnitude, y = point.y / magnitude }
762+
end
763+
764+
function add_offset(coord, offset)
765+
return coord + (offset > 0 and math.floor(offset+0.5) or math.ceil(offset-0.5))
766+
end
767+
768+
function Star:update(points, extra_points)
769+
self.num_tiles = 0
770+
self.points = copyall(points)
771+
self.arr = {}
772+
if #points < self.min_points then return end
773+
self.threshold = self.options.total_points.value - 2 * self.options.next_point_offset.value
774+
local top_left, bot_right = self:get_point_dims()
775+
self.height = bot_right.y - top_left.y
776+
self.width = bot_right.x - top_left.x
777+
if self.height == 1 or self.width == 1 then return end
778+
self.center = { x = self.width * 0.5, y = self.height * 0.5 }
779+
local axes = {}
780+
781+
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 }
782+
if vmagnitude(axes[1]) < 0.5 then axes[1].y = -self.center.y end
783+
axes[1] = vnormalize(axes[1])
784+
785+
for a = 2, self.options.total_points.value do
786+
local angle = math.pi * (a - 1.0) * 2.0 / self.options.total_points.value
787+
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 }
788+
end
789+
790+
local thickness = 1
791+
if self.options.hollow.value then
792+
thickness = self.options.thickness.value
793+
end
794+
795+
self.lines = {}
796+
for l = 1, self.options.total_points.value do
797+
local p1 = { x = self.center.x + axes[l].x * self.width * 0.5, y = self.center.y + axes[l].y * self.height * 0.5 }
798+
local next_axis = axes[(l-1+self.options.next_point_offset.value) % self.options.total_points.value + 1]
799+
local p2 = { x = self.center.x + next_axis.x * self.width * 0.5, y = self.center.y + next_axis.y * self.height * 0.5 }
800+
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 } }
801+
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)
802+
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)
803+
end
804+
805+
if not self.options.hollow.value or self.invert then
806+
for x = top_left.x, bot_right.x do
807+
if not self.arr[x] then self.arr[x] = {} end
808+
for y = top_left.y, bot_right.y do
809+
local value = self.arr[x][y] or (not self.options.hollow.value and self:has_point(x - top_left.x, y - top_left.y))
810+
if self.invert then
811+
self.arr[x][y] = not value
812+
else
813+
self.arr[x][y] = value
814+
end
815+
816+
self.num_tiles = self.num_tiles + (self.arr[x][y] and 1 or 0)
817+
end
818+
end
819+
end
820+
end
687821
-- module users can get shapes through this global, shape option values
688822
-- persist in these as long as the module is loaded
689823
-- idk enough lua to know if this is okay to do or not
690-
all_shapes = { Rectangle {}, Ellipse {}, Rows {}, Diag {}, Line {}, FreeForm {} }
824+
all_shapes = { Rectangle {}, Ellipse {}, Rows {}, Diag {}, Line {}, FreeForm {}, Star {} }

0 commit comments

Comments
 (0)