Stop redrawing what only needs moving

Reviewed against the rule issues #6 and #10 set for the buildings: geometry
is rasterised once and thereafter only moved, scaled or faded, because every
NeonDraw call expands into four antialiased polylines.

The planet visuals never got that refactor. The black hole rebuilt ten
glowing paths of twenty-five points every frame - forty polylines - to spin
five rings; each ring is a NeonPiece that rotates now. The research lab spent
nine draw calls a frame, eight of them fixed geometry, to turn one dish and
blink six lights; the dish is a child that rotates and the lights a child
whose modulate breathes. The gas giant's bands genuinely deform, so they
still draw, but its constant rim moved out to a child. NeonPiece gained
add_path, which it needed for open arcs.

And I had broken the rule myself. ScannerGuidance re-queued a redraw of its
arrow every frame for a colour that changes only with the destination - and
it runs with PROCESS_MODE_ALWAYS, so it was re-tessellating through every
dialog that stops the rest of the game. Guarded, and pinned by a test that
counts CanvasItem's own draw signal: 36 rasterisations in 0.6s before, one
after.

Two smaller things from the same pass: the station menu drew its condition
bar by hand where the ship lab already uses a ProgressBar node, so it uses
one now and its _draw is gone; and station/upkeep refreshes were wired into
_refresh_garrison(), which nothing but a test calls, so both menus showed
whatever was true when they opened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
This commit is contained in:
Loïc Wisniewski 2026-09-08 11:35:45 +02:00
commit 7d1a584c15
11 changed files with 350 additions and 78 deletions

View file

@ -160,6 +160,14 @@ func _process(delta: float) -> void:
_refresh_lab()
if transfer_menu.visible:
transfer_menu.refresh()
# Both of these move on their own while they are open - a transporter
# wears down, an office starves, the fleet changes under the bill - so
# they follow the same rule as the menus above: refreshed while visible,
# untouched when not.
if station_menu.visible:
station_menu.refresh()
if upkeep_menu.visible:
upkeep_menu.refresh()
if world_map.visible or _menu_is_open():
_clear_detected_context()
return
@ -817,8 +825,6 @@ func _layout_hangar() -> void:
func _refresh_garrison() -> void:
garrison_menu.refresh()
station_menu.refresh()
upkeep_menu.refresh()
func _refresh_planning() -> void:

View file

@ -5,6 +5,13 @@ extends Node2D
# core stays genuinely empty (nothing drawn inside CORE_RADIUS), and an
# accretion disc of rotating arc segments - not closed rings - stands in
# for a surface, brighter and thicker toward the centre.
#
# Each ring is a NeonPiece that is SPUN, not redrawn. Issues #6 and #10 made
# that the rule for the buildings and it applies here more than anywhere: the
# disc was ten glowing paths of twenty-five points re-tessellated every frame
# - forty antialiased polylines - to turn five rings at five different rates.
# Rotation is a node property, so now nothing is rasterised after the first
# frame and the disc turns for free.
var disc_phase: float = 0.0
@ -14,36 +21,54 @@ const RING_STEP: float = 16.0
const DISC_COLOR: Color = Color(0.55, 0.15, 0.85)
const DISC_HOT_COLOR: Color = Color(0.9, 0.6, 1.0)
# One per ring, with the rate and direction it turns at.
var _rings: Array = []
func _ready() -> void:
_build()
func configure(_shape_seed: int) -> void:
queue_redraw()
if _rings.is_empty():
_build()
func extractor_anchor() -> Vector2:
return Vector2(0.0, -(CORE_RADIUS + float(RING_COUNT - 1) * RING_STEP + 12.0))
func _process(delta: float) -> void:
disc_phase += delta
queue_redraw()
func _draw() -> void:
func _build() -> void:
for child in get_children():
child.queue_free()
_rings.clear()
for i in range(RING_COUNT):
var radius: float = CORE_RADIUS + float(i) * RING_STEP
# 1.0 at the innermost ring, fading to 0.0 at the outermost - drives
# both colour (hotter inward) and stroke weight/brightness.
var t: float = 1.0 - float(i) / float(maxi(RING_COUNT - 1, 1))
var ring_color: Color = DISC_COLOR.lerp(DISC_HOT_COLOR, t * 0.65)
# Alternating spin direction per ring and a speed tied to t reads as
var piece: NeonPiece = NeonPiece.create(self, Vector2.ZERO,
DISC_COLOR.lerp(DISC_HOT_COLOR, t * 0.65))
# The two arcs are built at their resting angles; the ring node turns
# them. Alternating direction per ring and a speed tied to t reads as
# genuine turbulence rather than one disc rotating as a rigid whole.
var spin: float = disc_phase * (0.12 + t * 0.22) * (1.0 if i % 2 == 0 else -1.0)
for arc_i in range(2):
var start: float = spin + float(arc_i) * PI + 0.15
var start: float = float(arc_i) * PI + 0.15
var end: float = start + PI - 0.3
var points := PackedVector2Array()
var steps: int = 24
for j in range(steps + 1):
var angle: float = lerpf(start, end, float(j) / float(steps))
points.append(Vector2(cos(angle), sin(angle)) * radius)
NeonDraw.draw_glowing_path(self, points, ring_color, 0.5 + t * 0.5, 0.45 + t * 0.35)
piece.add_path(points, 0.5 + t * 0.5, 0.45 + t * 0.35)
_rings.append({
"piece": piece,
"rate": (0.12 + t * 0.22) * (1.0 if i % 2 == 0 else -1.0),
})
func _process(delta: float) -> void:
disc_phase += delta
# One property per ring. Nothing is rasterised again.
for ring in _rings:
(ring.piece as NeonPiece).rotation = disc_phase * float(ring.rate)

View file

@ -1,6 +1,7 @@
extends Node2D
var gas_phase: float = 0.0
var _rim: NeonPiece
func configure(_shape_seed: int) -> void:
@ -16,12 +17,24 @@ func _process(delta: float) -> void:
queue_redraw()
func _draw() -> void:
var rim := PackedVector2Array()
# The bands genuinely deform - each point moves along its own sine - so they
# are the one thing here that has to be re-tessellated to animate. The rim
# does not move at all, so it is a NeonPiece child rasterised once rather
# than a thirty-two point polygon rebuilt every frame alongside them
# (issues #6/#10).
func _ensure_rim() -> void:
if _rim != null:
return
var points := PackedVector2Array()
for i in range(32):
var angle := TAU * float(i) / 32.0
rim.append(Vector2(cos(angle) * 118.0, sin(angle) * 118.0))
NeonDraw.draw_glowing_polygon(self, rim, Color(0.4, 0.78, 1.0), 0.9)
points.append(Vector2(cos(angle) * 118.0, sin(angle) * 118.0))
_rim = NeonPiece.create(self, Vector2.ZERO, Color(0.4, 0.78, 1.0))
_rim.add_polygon(points, 0.9)
func _draw() -> void:
_ensure_rim()
for row in [-62.0, -28.0, 8.0, 43.0, 73.0]:
var band := PackedVector2Array()
var half_width: float = sqrt(maxf(0.0, 118.0 * 118.0 - row * row))

View file

@ -5,6 +5,15 @@ extends Node2D
# and armoured rather than a natural silhouette (#63: this is an ENEMY
# installation, not a celestial body), with a rotating dish and blinking
# warning lights to read as occupied/hostile at a glance.
#
# Built out of NeonPiece children rather than redrawn each frame. Issues #6
# and #10 established that rule for the buildings: every NeonDraw call
# expands into four antialiased polylines, so re-running _draw() to advance
# an animation re-tessellates the whole thing. This visual was doing exactly
# that - nine draw calls a frame, of which eight were fixed geometry - to
# rotate one dish and fade six lights. Both are node properties: the dish is
# a child that spins, the lights are a child whose modulate breathes, and the
# hull is rasterised once and never touched again.
var phase: float = 0.0
@ -14,42 +23,72 @@ const CORE_COLOR: Color = Color(0.9, 0.25, 0.18)
const WARNING_COLOR: Color = Color(1.0, 0.2, 0.15)
const DARK_CORE: Color = Color(0.14, 0.02, 0.02)
var _dish: NeonPiece
var _lights: NeonPiece
func _ready() -> void:
_build()
func configure(_shape_seed: int) -> void:
queue_redraw()
if _dish == null:
_build()
func extractor_anchor() -> Vector2:
return Vector2(0.0, -(RADIUS + 40.0))
func _build() -> void:
for child in get_children():
child.queue_free()
# The armoured hull and its core: fixed, so drawn once.
var shell: NeonPiece = NeonPiece.create(self, Vector2.ZERO, HULL_COLOR)
shell.core_color = DARK_CORE
shell.add_polygon(_ring(RADIUS, 0.0), 0.85)
var core: NeonPiece = NeonPiece.create(self, Vector2.ZERO, CORE_COLOR)
core.core_color = DARK_CORE
core.add_polygon(_ring(RADIUS * 0.55, PI / 6.0), 0.9)
# The dish, drawn along +X and then SPUN by rotating this node - the
# rotation is what used to cost a full re-tessellation every frame.
_dish = NeonPiece.create(self, Vector2.ZERO, CORE_COLOR)
_dish.core_color = DARK_CORE
var tip: Vector2 = Vector2(RADIUS * 1.35, 0.0)
_dish.add_line(Vector2.ZERO, tip, 0.55, 0.8)
_dish.add_polygon(_circle(tip, 11.0), 1.0)
# The warning lights blink by fading the whole piece, not by redrawing
# six circles at a new alpha.
_lights = NeonPiece.create(self, Vector2.ZERO, WARNING_COLOR)
_lights.core_color = DARK_CORE
for i in range(6):
var angle: float = TAU * float(i) / 6.0
_lights.add_polygon(_circle(Vector2(cos(angle), sin(angle)) * RADIUS, 5.0), 1.0)
func _ring(radius: float, offset: float) -> PackedVector2Array:
var points: PackedVector2Array = PackedVector2Array()
for i in range(6):
var angle: float = TAU * float(i) / 6.0 + offset
points.append(Vector2(cos(angle), sin(angle)) * radius)
return points
func _circle(centre: Vector2, radius: float) -> PackedVector2Array:
var points: PackedVector2Array = PackedVector2Array()
for i in range(12):
var angle: float = TAU * float(i) / 12.0
points.append(centre + Vector2(cos(angle), sin(angle)) * radius)
return points
func _process(delta: float) -> void:
phase += delta
queue_redraw()
func _draw() -> void:
var hull: PackedVector2Array = PackedVector2Array()
for i in range(6):
var angle: float = TAU * float(i) / 6.0
hull.append(Vector2(cos(angle), sin(angle)) * RADIUS)
NeonDraw.draw_glowing_polygon(self, hull, HULL_COLOR, 0.85, DARK_CORE)
var inner: PackedVector2Array = PackedVector2Array()
for i in range(6):
var angle: float = TAU * float(i) / 6.0 + PI / 6.0
inner.append(Vector2(cos(angle), sin(angle)) * RADIUS * 0.55)
NeonDraw.draw_glowing_polygon(self, inner, CORE_COLOR, 0.9, DARK_CORE)
# A slow-rotating dish - reads as actively transmitting, not derelict.
var dish_angle: float = phase * 0.6
var dish_tip: Vector2 = Vector2(cos(dish_angle), sin(dish_angle)) * RADIUS * 1.35
NeonDraw.draw_glowing_line(self, Vector2.ZERO, dish_tip, CORE_COLOR, 0.8, 0.55, DARK_CORE)
NeonDraw.draw_glowing_circle(self, dish_tip, 11.0, CORE_COLOR, 1.0, DARK_CORE)
# Blinking hull-corner warning lights.
var blink: float = 0.4 + 0.6 * absf(sin(phase * 4.0))
for i in range(6):
var angle: float = TAU * float(i) / 6.0
var at: Vector2 = Vector2(cos(angle), sin(angle)) * RADIUS
NeonDraw.draw_glowing_circle(self, at, 5.0, WARNING_COLOR, blink, DARK_CORE)
# Two node properties. Nothing is rasterised again.
if _dish != null:
_dish.rotation = phase * 0.6
if _lights != null:
_lights.modulate.a = 0.4 + 0.6 * absf(sin(phase * 4.0))

View file

@ -0,0 +1,32 @@
extends Node2D
# Preview, not a test - the research lab planet's own art, so the NeonPiece
# rebuild can be compared against what it replaced.
#
# bash tests/screenshot.sh res://tests/_lab_planet_visual_preview.tscn shots/lab_planet.png 20 720x720
func _ready() -> void:
# Both planet visuals that were rebuilt out of NeonPiece children, side by
# side, wound forward so their moving parts are off their rest positions -
# the dish turned, the lights mid-blink, the disc rings spun apart.
var lab: Node2D = load("res://planets/ResearchLabPlanetVisual.gd").new()
lab.position = Vector2(200.0, 360.0)
add_child(lab)
lab.configure(1)
lab.phase = 1.1
lab._process(0.0)
var hole: Node2D = load("res://planets/BlackHoleVisual.gd").new()
hole.position = Vector2(540.0, 360.0)
add_child(hole)
hole.configure(1)
hole.disc_phase = 6.0
hole._process(0.0)
var gas: Node2D = load("res://planets/GasGiantVisual.gd").new()
gas.position = Vector2(360.0, 620.0)
add_child(gas)
gas.configure(1)
gas.gas_phase = 2.0
gas._process(0.0)

View file

@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tests/_lab_planet_visual_preview.gd" id="1"]
[node name="LabPlanetVisualPreview" type="Node2D"]
script = ExtResource("1")

View file

@ -0,0 +1,111 @@
extends SceneTree
# The flight marker must not re-rasterise itself to point somewhere.
#
# Issues #6 and #10 set this rule for the rest of the game: geometry is drawn
# ONCE and thereafter moved, rotated, scaled or faded, because every NeonDraw
# call expands into four antialiased polylines and redrawing to animate
# re-tessellates all of them. The marker broke it - it re-queued a redraw
# every frame for a glow colour that only changes when the DESTINATION does -
# and it is the worst node in the game to get wrong, because it runs with
# PROCESS_MODE_ALWAYS: it kept working through every dialog that stops the
# rest of the game.
#
# Counted off CanvasItem's own `draw` signal rather than a counter of ours,
# so what is measured is real rasterisation.
#
# Covers:
# A. a marker tracking one destination settles - it stops redrawing
# B. it is genuinely still tracking (so this cannot pass by freezing)
# C. a destination of a different colour does redraw it, once
var state: Node
var scene: Node
var indicator: Control
var draws: int = 0
var stage: int = 0
var elapsed: float = 0.0
var settled_at: int = 0
var first_position: Vector2
func _initialize() -> void:
state = root.get_node("GameState")
state.reset_new_game()
state.intro_active = false
state.reset_quests()
state.buildings.clear()
scene = load("res://world/Main.tscn").instantiate()
root.add_child(scene)
current_scene = scene
indicator = scene.get_node("UI/ScannerGuidance/Indicator")
indicator.draw.connect(func(): draws += 1)
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
if elapsed < 0.4:
return false
# A destination far off, so the marker is pinned to a screen edge
# and its position keeps being recomputed every frame.
state.scanner_target = {
"planet_id": "redraw:probe", "definition_id": "rocky",
"name": "Probe", "world_position": Vector2(90000.0, -40000.0),
}
elapsed = 0.0
stage = 1
return false
1:
# Let it take up the target and draw whatever it needs to.
if elapsed < 0.5:
return false
settled_at = draws
first_position = indicator.position
elapsed = 0.0
stage = 2
return false
2:
# --- A. thirty-odd frames of tracking, no rasterising ---------
if elapsed < 0.6:
return false
var while_tracking: int = draws - settled_at
if while_tracking > 1:
return _fail("the marker rasterised %d times in %.1fs of tracking one destination" \
% [while_tracking, elapsed])
# --- B. and it really was tracking ---------------------------
if not scene.get_node("UI/ScannerGuidance").visible:
return _fail("the marker was not even visible, so this proves nothing")
print("tracking one destination: %d redraws in %.1fs" % [while_tracking, elapsed])
# --- C. a different world does repaint it --------------------
var before: int = draws
state.scanner_target = {
"planet_id": "redraw:probe2", "definition_id": "gas_giant",
"name": "Probe Two", "world_position": Vector2(-70000.0, 55000.0),
}
elapsed = 0.0
stage = 3
settled_at = before
return false
3:
if elapsed < 0.4:
return false
var repaints: int = draws - settled_at
if repaints < 1:
return _fail("changing destination never recoloured the marker")
if repaints > 2:
return _fail("changing destination rasterised %d times, expected about one" % repaints)
print("changing destination repaints it %d time(s), then settles" % repaints)
print("TEST_GUIDANCE_MARKER_REDRAW: PASS")
quit(0)
return true
return false
func _fail(message: String) -> bool:
print("TEST_GUIDANCE_MARKER_REDRAW: FAIL (%s)" % message)
quit(1)
return true

View file

@ -14,6 +14,13 @@ const EDGE_MARGIN: float = 34.0
@onready var target_label: Label = $TargetLabel
var fleet: Node2D
# The colour the arrow was last rasterised with. Issues #6/#10: a glyph is
# drawn once and thereafter moved and rotated, both of which reuse the cached
# geometry. Re-queueing a redraw every frame for a colour that changes only
# when the DESTINATION changes threw that away - and this node runs even
# while the game is stopped for a dialog, so it was the one piece of UI
# re-tessellating itself through the quietest moments in the game.
var _drawn_glow: Color = Color(0, 0, 0, 0)
func _ready() -> void:
@ -53,8 +60,9 @@ func _process(_delta: float) -> void:
# authored colour rather than being forced into one.
var definition: Resource = PLANET_DEFINITIONS.get_definition(
String(target.get("definition_id", "")))
if definition != null:
indicator.glow_color = definition.map_color
if definition != null and definition.map_color != _drawn_glow:
_drawn_glow = definition.map_color
indicator.glow_color = _drawn_glow
indicator.queue_redraw()
var sectors: float = toward_target.length() / WorldGen.CHUNK_SIZE
target_label.text = "%s · %.1f SECTORS" % [

View file

@ -12,23 +12,25 @@ extends "res://ui/menus/ModalMenu.gd"
# it does, so the bar row below shows what each of them has left, worst first,
# which is the number that decides whether another needs building.
# One bar, wide, for the vessel closest to being spent - the one that decides
# One bar, for the vessel closest to being spent - the one that decides
# whether another needs building. The per-vessel figures are listed as TEXT
# below it: a row of small bars was unreadable at the size a phone draws this
# menu, and "cycles left" is a number the player wants to actually read.
const BAR_WIDTH: float = 420.0
const BAR_HEIGHT: float = 40.0
const BAR_TOP: float = 494.0
# beneath it: a row of small bars was unreadable at the size a phone draws
# this menu, and "cycles left" is a number the player wants to read.
#
# A ProgressBar NODE rather than rects in _draw(), the way the ship lab draws
# its research progress. Issues #6/#10 set the rule for the rest of the game -
# geometry is rasterised once and thereafter only moved, scaled or recoloured
# - and a menu that refreshes while it is open would otherwise re-tessellate
# its bar every frame to move one edge.
const WORN_COLOR: Color = Color(1.0, 0.35, 0.25)
const FRESH_COLOR: Color = Color(0.5, 1.0, 0.55)
const PLATE_COLOR: Color = Color(0.02, 0.02, 0.06, 0.9)
const FRAME_COLOR: Color = Color(0.85, 0.96, 1.0, 0.85)
@onready var title: Label = $TitleLabel
@onready var assigned_label: Label = $AssignedLabel
@onready var status: Label = $StatusLabel
@onready var condition_label: Label = $ConditionLabel
@onready var condition_list: Label = $ConditionList
@onready var condition_bar: ProgressBar = $ConditionBar
@onready var less_button: IconButton = $LessButton
@onready var more_button: IconButton = $MoreButton
@ -89,6 +91,7 @@ func refresh() -> void:
var left: Array = GameState.station_condition(building_id)
var full: int = maxi(GameState.station_work_capacity(building_id), 1)
var unit: String = "RUNS" if vessel_type == GameState.VESSEL_TRANSPORTER else "REPAIRS"
condition_bar.visible = not left.is_empty()
if left.is_empty():
condition_label.text = ""
condition_list.text = ""
@ -101,27 +104,11 @@ func refresh() -> void:
lines.append("%s %d %d / %d %s" % [
name.to_upper(), i + 1, int(left[i]), full, unit])
condition_list.text = "\n".join(lines)
queue_redraw()
func _draw() -> void:
if building_id == 0:
return
# One bar per vessel standing here, worst first: what is left of the work
# it was built to do. Drawn rather than built from nodes because the count
# changes as vessels arrive and wear out, and a row of bars is cheaper to
# draw than to keep a pool of Controls in step with.
var left: Array = GameState.station_condition(building_id)
if left.is_empty():
return
var full: int = maxi(GameState.station_work_capacity(building_id), 1)
# The worst of them. station_condition() answers worst first.
var fraction: float = clampf(float(left[0]) / float(full), 0.0, 1.0)
var x: float = (size.x - BAR_WIDTH) * 0.5
draw_rect(Rect2(x - 3.0, BAR_TOP - 3.0, BAR_WIDTH + 6.0, BAR_HEIGHT + 6.0),
PLATE_COLOR)
draw_rect(Rect2(x, BAR_TOP, BAR_WIDTH, BAR_HEIGHT),
Color(FRESH_COLOR.r, FRESH_COLOR.g, FRESH_COLOR.b, 0.16))
draw_rect(Rect2(x, BAR_TOP, BAR_WIDTH * fraction, BAR_HEIGHT),
WORN_COLOR.lerp(FRESH_COLOR, fraction))
draw_rect(Rect2(x, BAR_TOP, BAR_WIDTH, BAR_HEIGHT), FRAME_COLOR, false, 2.0)
# Worst first, so this is the one about to go.
var fraction: float = clampf(float(left[0]) / float(full), 0.0, 1.0)
condition_bar.value = fraction
# The fill is recoloured rather than redrawn: a StyleBox is shared
# geometry, and setting its colour does not re-tessellate anything.
var fill: StyleBoxFlat = condition_bar.get_theme_stylebox("fill")
if fill != null:
fill.bg_color = WORN_COLOR.lerp(FRESH_COLOR, fraction)

View file

@ -1,8 +1,27 @@
[gd_scene load_steps=3 format=3]
[gd_scene load_steps=5 format=3]
[ext_resource type="Script" path="res://ui/IconButton.gd" id="icon"]
[ext_resource type="Script" path="res://ui/menus/StationMenu.gd" id="menu"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_condition_background"]
bg_color = Color(0.025, 0.045, 0.09, 0.92)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color(0.18, 0.48, 0.62, 0.9)
corner_radius_top_left = 8
corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_condition_fill"]
bg_color = Color(0.5, 1, 0.55, 0.88)
corner_radius_top_left = 6
corner_radius_top_right = 6
corner_radius_bottom_right = 6
corner_radius_bottom_left = 6
[node name="StationMenu" type="Control" groups=["modal_menus"]]
visible = false
mouse_filter = 2
@ -83,6 +102,21 @@ theme_override_colors/font_color = Color(0.55, 0.78, 0.86, 1)
text = ""
horizontal_alignment = 1
[node name="ConditionBar" type="ProgressBar" parent="."]
visible = false
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = -210.0
offset_top = 494.0
offset_right = 210.0
offset_bottom = 534.0
theme_override_styles/background = SubResource("StyleBoxFlat_condition_background")
theme_override_styles/fill = SubResource("StyleBoxFlat_condition_fill")
max_value = 1.0
step = 0.001
show_percentage = false
[node name="ConditionList" type="Label" parent="."]
mouse_filter = 2
anchor_right = 1.0

View file

@ -16,6 +16,9 @@ extends Node2D
# owner can move, spin, scale and fade it without touching the shape.
var polygons: Array = []
# Open runs of points, as distinct from `polygons`, which close. An accretion
# disc is arcs, not rings, so it needs the open form to be a piece at all.
var paths: Array = []
var lines: Array = []
var glow_color: Color = Color.WHITE
var core_color: Color = NeonDraw.DEFAULT_CORE
@ -35,6 +38,11 @@ func add_polygon(points: PackedVector2Array, piece_pulse: float = 1.0) -> void:
polygons.append({"points": points, "pulse": piece_pulse})
func add_path(points: PackedVector2Array, piece_pulse: float = 1.0,
width: float = 1.0) -> void:
paths.append({"points": points, "pulse": piece_pulse, "width": width})
func add_line(from: Vector2, to: Vector2, width: float, piece_pulse: float = 1.0) -> void:
lines.append({"from": from, "to": to, "width": width, "pulse": piece_pulse})
@ -42,6 +50,9 @@ func add_line(from: Vector2, to: Vector2, width: float, piece_pulse: float = 1.0
func _draw() -> void:
for poly in polygons:
NeonDraw.draw_glowing_polygon(self, poly.points, glow_color, poly.pulse * pulse, core_color)
for path in paths:
NeonDraw.draw_glowing_path(self, path.points, glow_color,
path.pulse * pulse, path.width, core_color)
for line in lines:
NeonDraw.draw_glowing_line(self, line.from, line.to, glow_color,
line.pulse * pulse, line.width, core_color)