z_mirror/Tapeline

git clone https://git.raiguard.me/z_mirror/Tapeline

Caleb HeuerUse slash requires37bb3aa

color-setting
14.5 KiB474 linesraw
1local area = require("__flib__/area")
2local table = require("__flib__/table")
3
4local bring_to_front = rendering.bring_to_front
5local destroy = rendering.destroy
6local draw_line = rendering.draw_line
7local draw_rectangle = rendering.draw_rectangle
8local draw_text = rendering.draw_text
9local set_color = rendering.set_color
10local set_draw_on_ground = rendering.set_draw_on_ground
11local set_from = rendering.set_from
12local set_left_top = rendering.set_left_top
13local set_right_bottom = rendering.set_right_bottom
14local set_target = rendering.set_target
15local set_text = rendering.set_text
16local set_time_to_live = rendering.set_time_to_live
17local set_to = rendering.set_to
18local set_visible = rendering.set_visible
19local set_width = rendering.set_width
20
21local tape = {}
22
23local opposite_corners = {
24  left_top = "right_bottom",
25  left_bottom = "right_top",
26  right_top = "left_bottom",
27  right_bottom = "left_top",
28}
29
30--- @param objects TapeObjects
31--- @param func fun(id: uint64, ...)
32local function apply_to_all_objects(objects, func, ...)
33  for _, v in pairs(objects) do
34    if type(v) == "table" then
35      apply_to_all_objects(v, func, ...)
36    else
37      func(v, ...)
38    end
39  end
40end
41
42--- @param player_index uint
43--- @param tape_data TapeData
44--- @param visual_settings VisualSettings
45local function create_objects(player_index, tape_data, visual_settings)
46  local TapeArea = tape_data.Area
47  --- @class TapeObjects
48  local objects = {
49    background = draw_rectangle({
50      color = visual_settings.tape_background_color,
51      filled = true,
52      left_top = TapeArea.left_top,
53      right_bottom = TapeArea.right_bottom,
54      surface = tape_data.surface,
55      players = { player_index },
56      draw_on_ground = visual_settings.draw_tape_on_ground,
57    }),
58    border = draw_rectangle({
59      color = visual_settings.tape_border_color,
60      width = visual_settings.tape_line_width,
61      filled = false,
62      left_top = TapeArea.left_top,
63      right_bottom = TapeArea.right_bottom,
64      surface = tape_data.surface,
65      players = { player_index },
66      draw_on_ground = visual_settings.draw_tape_on_ground,
67    }),
68    labels = {
69      x = draw_text({
70        text = tostring(TapeArea:width()),
71        surface = tape_data.surface,
72        target = { x = TapeArea:center().x, y = TapeArea.left_top.y - 0.85 },
73        color = visual_settings.tape_label_color,
74        scale = 1.5,
75        alignment = "center",
76        visible = false,
77        players = { player_index },
78      }),
79      y = draw_text({
80        text = tostring(TapeArea:height()),
81        surface = tape_data.surface,
82        target = { x = TapeArea.left_top.x - 0.85, y = TapeArea:center().y },
83        color = visual_settings.tape_label_color,
84        scale = 1.5,
85        orientation = 0.75,
86        alignment = "center",
87        visible = false,
88        players = { player_index },
89      }),
90    },
91    lines = {
92      { x = {}, y = {} },
93      { x = {}, y = {} },
94      { x = {}, y = {} },
95      { x = {}, y = {} },
96    },
97  }
98  return objects
99end
100
101--- @param player_index uint
102--- @param tape_data TapeData
103--- @param tape_settings TapeSettings
104--- @param visual_settings VisualSettings
105local function update_objects(player_index, tape_data, tape_settings, visual_settings)
106  local TapeArea = tape_data.Area
107  local objects = tape_data.objects
108
109  local background = objects.background
110  set_left_top(background, TapeArea.left_top)
111  set_right_bottom(background, TapeArea.right_bottom)
112
113  local border = objects.border
114  set_left_top(border, TapeArea.left_top)
115  set_right_bottom(border, TapeArea.right_bottom)
116
117  local center = TapeArea:center()
118  local height = TapeArea:height()
119  local width = TapeArea:width()
120
121  local x_label = objects.labels.x
122  set_text(x_label, tostring(width))
123  set_target(x_label, { x = center.x, y = TapeArea.left_top.y - 0.85 })
124  if width > 1 then
125    set_visible(x_label, true)
126  else
127    set_visible(x_label, false)
128  end
129
130  local y_label = objects.labels.y
131  set_text(y_label, tostring(height))
132  set_target(y_label, { x = TapeArea.left_top.x - 0.85, y = center.y })
133  if height > 1 then
134    set_visible(y_label, true)
135  else
136    set_visible(y_label, false)
137  end
138
139  -- GRID LINES
140
141  local function delete_lines(axis, grid_index)
142    apply_to_all_objects(objects.lines[grid_index][axis], destroy)
143    objects.lines[grid_index][axis] = {}
144  end
145
146  local function delete_grid(grid_index)
147    delete_lines("x", grid_index)
148    delete_lines("y", grid_index)
149  end
150
151  -- TODO: optimize to not touch lines that don't need updating
152  local function update_lines(axis, grid_index, grid_step)
153    local lines = objects.lines[grid_index][axis]
154
155    -- iterate from the origin corner outwards
156    local start = TapeArea[tape_data.origin_corner][axis]
157    local finish = TapeArea[opposite_corners[tape_data.origin_corner]][axis]
158
159    local i = 0
160
161    if (axis == "x" and width or height) > 1 then
162      for pos = start, finish, start < finish and grid_step or -grid_step do
163        local from, to
164        if axis == "x" then
165          from = { x = pos, y = TapeArea.left_top.y }
166          to = { x = pos, y = TapeArea.right_bottom.y }
167        else
168          from = { x = TapeArea.left_top.x, y = pos }
169          to = { x = TapeArea.right_bottom.x, y = pos }
170        end
171
172        i = i + 1
173        local line = lines[i]
174        if line then
175          set_from(line, from)
176          set_to(line, to)
177          bring_to_front(line)
178        else
179          lines[i] = draw_line({
180            color = visual_settings["tape_line_color_" .. grid_index],
181            width = visual_settings.tape_line_width,
182            from = from,
183            to = to,
184            surface = tape_data.surface,
185            players = { player_index },
186            draw_on_ground = visual_settings.draw_tape_on_ground,
187          })
188        end
189      end
190    end
191
192    for j = i + 1, #lines do
193      destroy(lines[j])
194      lines[j] = nil
195    end
196  end
197
198  local function update_grid(grid_index, grid_step)
199    update_lines("x", grid_index, grid_step)
200    update_lines("y", grid_index, grid_step)
201  end
202
203  -- base grid
204  update_grid(1, 1)
205
206  local mode = tape_settings.mode
207  if mode == "subgrid" then
208    local subgrid_size = tape_settings.subgrid_divisor
209    update_grid(2, subgrid_size)
210    update_grid(3, subgrid_size ^ 2)
211    update_grid(4, subgrid_size ^ 3)
212  elseif mode == "split" then
213    local num_splits = tape_settings.split_divisor
214    update_lines("x", 2, width / num_splits)
215    update_lines("y", 2, height / num_splits)
216    update_lines("x", 3, width / 2)
217    update_lines("y", 3, height / 2)
218    delete_grid(4)
219  end
220
221  bring_to_front(border)
222end
223
224--- @param player LuaPlayer
225--- @param player_table PlayerTable
226--- @param origin MapPosition
227--- @param surface LuaSurface
228function tape.start_draw(player, player_table, origin, surface)
229  -- TODO: Convert to flib bounding-box module
230  local TapeArea = area.from_position(origin):ceil():corners()
231  --- @class TapeData
232  local tape_data = {
233    Area = TapeArea,
234    last_position = origin,
235    last_update_tick = game.ticks_played,
236    origin = origin,
237    origin_corner = "left_top",
238    surface = surface,
239  }
240  tape_data.objects = create_objects(player.index, tape_data, player_table.visual_settings)
241  player_table.tapes.drawing = tape_data
242  player_table.flags.drawing = true
243end
244
245--- @param player LuaPlayer
246--- @param player_table PlayerTable
247--- @param new_position MapPosition?
248--- @param is_ghost boolean?
249function tape.update_draw(player, player_table, new_position, is_ghost)
250  local tape_data = player_table.tapes.drawing
251  local TapeArea = area.load(tape_data.Area)
252  local origin = tape_data.origin
253
254  if new_position then
255    if not is_ghost then
256      if math.abs(new_position.x - origin.x) >= math.abs(new_position.y - origin.y) then
257        new_position.y = math.floor(origin.y)
258      else
259        new_position.x = math.floor(origin.x)
260      end
261
262      -- if the new position is the same as the last, don't actually do anything
263      local last_position = tape_data.last_position
264      if new_position.x == last_position.x and new_position.y == last_position.y then
265        return
266      end
267    end
268    tape_data.last_position = new_position
269  else
270    new_position = tape_data.last_position
271  end
272
273  tape_data.last_update_tick = game.ticks_played
274
275  -- update area corners
276  local x_less = new_position.x < origin.x
277  local y_less = new_position.y < origin.y
278  TapeArea.left_top = {
279    x = math.floor(x_less and new_position.x or origin.x),
280    y = math.floor(y_less and new_position.y or origin.y),
281  }
282  TapeArea.right_bottom = {
283    x = math.ceil(x_less and origin.x or new_position.x),
284    y = math.ceil(y_less and origin.y or new_position.y),
285  }
286  TapeArea:corners()
287  -- update origin corner
288  local new_origin_corner = ((x_less and "right" or "left") .. "_" .. (y_less and "bottom" or "top"))
289  tape_data.origin_corner = new_origin_corner
290
291  update_objects(player.index, tape_data, player_table.tape_settings, player_table.visual_settings)
292end
293
294--- @param player LuaPlayer
295--- @param player_table PlayerTable
296--- @param auto_clear boolean
297function tape.complete_draw(player, player_table, auto_clear)
298  local tapes = player_table.tapes
299  local tape_data = tapes.drawing
300  local TapeArea = tape_data.Area
301  local objects = tape_data.objects
302
303  player_table.flags.drawing = false
304
305  -- immediately destroy the tape if it is 1x1
306  if TapeArea:height() == 1 and TapeArea:width() == 1 then
307    apply_to_all_objects(objects, destroy)
308    tapes.drawing = nil
309    return
310  end
311
312  tape_data.last_position = nil
313
314  if auto_clear then
315    apply_to_all_objects(objects, set_time_to_live, player_table.visual_settings.tape_clear_delay * 60)
316    -- update to fix draw order
317    update_objects(player.index, tape_data, player_table.tape_settings, player_table.visual_settings)
318  else
319    -- copy settings into tape so they can be changed later
320    tape_data.settings = table.deep_copy(player_table.tape_settings)
321    tapes[#tapes + 1] = tape_data
322  end
323
324  if player_table.visual_settings.log_selection_area then
325    player.print(TapeArea:width() .. "x" .. TapeArea:height())
326  end
327  tapes.drawing = nil
328end
329
330--- @param player_table PlayerTable
331function tape.cancel_draw(player_table)
332  player_table.flags.drawing = false
333
334  local tape_data = player_table.tapes.drawing
335  apply_to_all_objects(tape_data.objects, destroy)
336  player_table.tapes.drawing = nil
337end
338
339--- @param player_table PlayerTable
340--- @param tape_index number
341function tape.delete(player_table, tape_index)
342  local tapes = player_table.tapes
343  local tape_data = tapes[tape_index]
344  apply_to_all_objects(tape_data.objects, destroy)
345  if player_table.flags.editing then
346    tape.exit_edit_mode(player_table)
347  end
348  table.remove(tapes, tape_index)
349end
350
351--- @param player_index uint
352--- @param TapeArea BoundingBox
353--- @param surface LuaSurface
354local function create_highlight_box(player_index, TapeArea, surface)
355  return surface.create_entity({
356    name = "tl-highlight-box",
357    position = TapeArea:center(),
358    bounding_box = area.expand(TapeArea:strip(), 0.3),
359    cursor_box_type = "electricity",
360    render_player_index = player_index,
361    blink_interval = 30,
362  })
363end
364
365--- @param player LuaPlayer
366--- @param player_table PlayerTable
367--- @param tape_index number
368function tape.enter_edit_mode(player, player_table, tape_index)
369  local tape_data = player_table.tapes[tape_index]
370  local TapeArea = area.load(tape_data.Area)
371
372  tape_data.highlight_box = create_highlight_box(player.index, TapeArea, tape_data.surface)
373
374  player_table.flags.editing = true
375  player_table.tapes.editing = tape_data
376end
377
378--- @param player_table PlayerTable
379function tape.exit_edit_mode(player_table)
380  local tape_data = player_table.tapes.editing
381  tape_data.highlight_box.destroy()
382  tape_data.last_position = nil
383  player_table.flags.editing = false
384  player_table.tapes.editing = nil
385end
386
387--- @param player_index uint
388--- @param player_table PlayerTable
389--- @param mode string
390--- @param divisor number
391function tape.edit_settings(player_index, player_table, mode, divisor)
392  local tape_data = player_table.tapes.editing
393  tape_data.settings.mode = mode
394  tape_data.settings[mode .. "_divisor"] = divisor
395
396  update_objects(player_index, tape_data, tape_data.settings, player_table.visual_settings)
397end
398
399--- @param player LuaPlayer
400--- @param player_table PlayerTable
401--- @param new_position MapPosition
402--- @param surface LuaSurface
403function tape.move(player, player_table, new_position, surface)
404  local tape_data = player_table.tapes.editing
405  if surface ~= tape_data.surface then
406    return
407  end
408
409  local TapeArea = area.load(tape_data.Area)
410
411  local last_position = tape_data.last_position
412  if last_position then
413    -- calculate delta
414    local delta = {
415      x = new_position.x - last_position.x,
416      y = new_position.y - last_position.y,
417    }
418    -- move area and origin
419    TapeArea:move(delta):corners()
420    tape_data.origin = {
421      x = tape_data.origin.x + delta.x,
422      y = tape_data.origin.y + delta.y,
423    }
424    -- update last updated tick
425    tape_data.last_update_tick = game.ticks_played
426    -- update render objects
427    update_objects(player.index, tape_data, tape_data.settings, player_table.visual_settings)
428    -- update highlight box
429    tape_data.highlight_box.destroy()
430    tape_data.highlight_box = create_highlight_box(player.index, TapeArea, tape_data.surface)
431    tape_data.last_position = new_position
432  elseif TapeArea:contains_position(new_position) then
433    tape_data.last_position = new_position
434  end
435end
436
437--- @param player_table PlayerTable
438function tape.complete_move(player_table)
439  player_table.tapes.editing.last_position = nil
440end
441
442--- @param tape_data TapeData
443--- @param settings VisualSettings
444function tape.update_visual_settings(tape_data, settings)
445  local objects = tape_data.objects
446
447  local line_width = settings.tape_line_width
448  local draw_on_ground = settings.draw_tape_on_ground
449
450  local background = objects.background
451  set_color(background, settings.tape_background_color)
452  set_draw_on_ground(background, draw_on_ground)
453
454  local border = objects.border
455  set_color(border, settings.tape_border_color)
456  set_draw_on_ground(border, draw_on_ground)
457  set_width(border, line_width)
458
459  set_color(objects.labels.x, settings.tape_label_color)
460  set_color(objects.labels.y, settings.tape_label_color)
461
462  -- GRID LINES
463
464  local lines = objects.lines
465  apply_to_all_objects(lines, set_draw_on_ground, draw_on_ground)
466  apply_to_all_objects(lines, set_width, line_width)
467
468  apply_to_all_objects(lines[1], set_color, settings.tape_line_color_1)
469  apply_to_all_objects(lines[2], set_color, settings.tape_line_color_2)
470  apply_to_all_objects(lines[3], set_color, settings.tape_line_color_3)
471  apply_to_all_objects(lines[4], set_color, settings.tape_line_color_4)
472end
473
474return tape