Clip the cooldown fill with Geometry2D, not by hand
Every cooling vessel logged "Invalid polygon data, triangulation failed" once per frame, for as long as its cooldown ran - 16 of them in a single test_laser_vessel run. The fill was drawn anyway, so nothing looked wrong, but it buried real errors in test logs. _filled_hull cut each hull edge against the fill line itself, which is Sutherland-Hodgman, and Sutherland-Hodgman is only sound for a CONVEX subject. Hulls are not: the gas bomber has a tail notch and the distorder a waist. On those it stitched the pieces back together with a zero-width bridge, and a polygon with a zero-width bridge is not one the triangulator accepts. Geometry2D.intersect_polygons does the clipping properly and returns a list of polygons, which is also the truer answer - a notched hull genuinely fills as two puddles until the level rises past the notch. A weld-and-area guard stays in front of the renderer for repeated vertices and slivers. Zero triangulation errors across the vessel tests now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
c8d4731232
commit
12bd5eb7bf
2 changed files with 82 additions and 21 deletions
|
|
@ -44,8 +44,16 @@ func _process(delta: float) -> bool:
|
||||||
skill._process(0.0)
|
skill._process(0.0)
|
||||||
if first.is_ready() or not second.is_ready() or not skill.button.enabled:
|
if first.is_ready() or not second.is_ready() or not skill.button.enabled:
|
||||||
return _fail("the first shot disabled more than its firing bomber")
|
return _fail("the first shot disabled more than its firing bomber")
|
||||||
if not first._filled_hull(fleet.ships[0].definition.hull_points, 0.0).is_empty() \
|
# _filled_hull returns a LIST of polygons: a notched hull fills as two
|
||||||
or first._filled_hull(fleet.ships[0].definition.hull_points, 0.5).size() < 3:
|
# separate puddles until the level rises past the notch, and the gas
|
||||||
|
# bomber's tail is exactly such a notch.
|
||||||
|
var hull: PackedVector2Array = fleet.ships[0].definition.hull_points
|
||||||
|
var empty: Array = first._filled_hull(hull, 0.0)
|
||||||
|
var half: Array = first._filled_hull(hull, 0.5)
|
||||||
|
var half_points: int = 0
|
||||||
|
for piece in half:
|
||||||
|
half_points += piece.size()
|
||||||
|
if not empty.is_empty() or half_points < 3:
|
||||||
return _fail("bomber cooldown did not produce an empty-to-filling hull")
|
return _fail("bomber cooldown did not produce an empty-to-filling hull")
|
||||||
if skill.projectiles.size() != 1:
|
if skill.projectiles.size() != 1:
|
||||||
return _fail("the first bomber did not launch exactly one projectile")
|
return _fail("the first bomber did not launch exactly one projectile")
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,14 @@ extends Node2D
|
||||||
var cooldown_duration: float = 1.0
|
var cooldown_duration: float = 1.0
|
||||||
var cooldown_left: float = 0.0
|
var cooldown_left: float = 0.0
|
||||||
|
|
||||||
|
# Two vertices this close together are the same vertex as far as the fill is
|
||||||
|
# concerned - a hull is tens of units across, so this is well below anything
|
||||||
|
# that could be a real edge.
|
||||||
|
const WELD_DISTANCE: float = 0.01
|
||||||
|
# Below this the fill is a sliver nobody can see, and drawing it is all cost
|
||||||
|
# and no picture.
|
||||||
|
const MIN_FILL_AREA: float = 0.5
|
||||||
|
|
||||||
|
|
||||||
func setup() -> void:
|
func setup() -> void:
|
||||||
show_behind_parent = true
|
show_behind_parent = true
|
||||||
|
|
@ -37,30 +45,76 @@ func cooldown_progress() -> float:
|
||||||
return 1.0 - cooldown_left / maxf(cooldown_duration, 0.001)
|
return 1.0 - cooldown_left / maxf(cooldown_duration, 0.001)
|
||||||
|
|
||||||
|
|
||||||
func _filled_hull(points: PackedVector2Array, progress: float) -> PackedVector2Array:
|
# The part of the hull that is "filled up" at this progress, as a list of
|
||||||
|
# polygons - plural, because a hull with a notch in it (the gas bomber's
|
||||||
|
# tail, the distorder's waist) genuinely fills as two separate puddles
|
||||||
|
# before the level rises past the notch.
|
||||||
|
#
|
||||||
|
# Geometry2D does the clipping. This used to walk the hull itself and cut
|
||||||
|
# each edge against the fill line, which is Sutherland-Hodgman, and
|
||||||
|
# Sutherland-Hodgman is only sound for a CONVEX subject: on a notched hull
|
||||||
|
# it stitches the pieces back together with a zero-width bridge, and a
|
||||||
|
# polygon with a zero-width bridge is not one Godot's triangulator will
|
||||||
|
# accept. It said so, once per frame per cooling vessel, for as long as any
|
||||||
|
# cooldown ran.
|
||||||
|
func _filled_hull(points: PackedVector2Array, progress: float) -> Array:
|
||||||
if points.size() < 3 or progress <= 0.001:
|
if points.size() < 3 or progress <= 0.001:
|
||||||
return PackedVector2Array()
|
return []
|
||||||
if progress >= 0.999:
|
if progress >= 0.999:
|
||||||
return points.duplicate()
|
return [points.duplicate()]
|
||||||
var top: float = INF
|
var top: float = INF
|
||||||
var bottom: float = -INF
|
var bottom: float = -INF
|
||||||
|
var left: float = INF
|
||||||
|
var right: float = -INF
|
||||||
for point in points:
|
for point in points:
|
||||||
top = minf(top, point.y)
|
top = minf(top, point.y)
|
||||||
bottom = maxf(bottom, point.y)
|
bottom = maxf(bottom, point.y)
|
||||||
|
left = minf(left, point.x)
|
||||||
|
right = maxf(right, point.x)
|
||||||
var fill_y: float = lerpf(bottom, top, clampf(progress, 0.0, 1.0))
|
var fill_y: float = lerpf(bottom, top, clampf(progress, 0.0, 1.0))
|
||||||
var clipped := PackedVector2Array()
|
# Deliberately oversized sideways and downwards, so the box's own edges
|
||||||
var previous: Vector2 = points[-1]
|
# never coincide with the hull's and the only cut is the horizontal one.
|
||||||
var previous_inside: bool = previous.y >= fill_y
|
var box := PackedVector2Array([
|
||||||
for current in points:
|
Vector2(left - 8.0, fill_y), Vector2(right + 8.0, fill_y),
|
||||||
var current_inside: bool = current.y >= fill_y
|
Vector2(right + 8.0, bottom + 8.0), Vector2(left - 8.0, bottom + 8.0),
|
||||||
if current_inside != previous_inside:
|
])
|
||||||
var amount: float = (fill_y - previous.y) / (current.y - previous.y)
|
var pieces: Array = []
|
||||||
clipped.append(previous.lerp(current, amount))
|
for piece in Geometry2D.intersect_polygons(points, box):
|
||||||
if current_inside:
|
var drawable: PackedVector2Array = _drawable(piece)
|
||||||
clipped.append(current)
|
if not drawable.is_empty():
|
||||||
previous = current
|
pieces.append(drawable)
|
||||||
previous_inside = current_inside
|
return pieces
|
||||||
return clipped
|
|
||||||
|
|
||||||
|
# Last guard before the renderer. The clipper is sound where the old walk
|
||||||
|
# was not, but it can still hand back a vertex repeated at a corner, or a
|
||||||
|
# sliver of a puddle at the very start of a fill - neither is worth drawing
|
||||||
|
# and both are the shapes triangulation objects to.
|
||||||
|
func _drawable(points: PackedVector2Array) -> PackedVector2Array:
|
||||||
|
var welded := PackedVector2Array()
|
||||||
|
for point in points:
|
||||||
|
if welded.is_empty() \
|
||||||
|
or welded[welded.size() - 1].distance_to(point) > WELD_DISTANCE:
|
||||||
|
welded.append(point)
|
||||||
|
# The polygon is implicitly closed, so the last point meeting the first is
|
||||||
|
# the same duplication seen from the other end.
|
||||||
|
while welded.size() >= 2 \
|
||||||
|
and welded[0].distance_to(welded[welded.size() - 1]) <= WELD_DISTANCE:
|
||||||
|
welded.remove_at(welded.size() - 1)
|
||||||
|
if welded.size() < 3 or _area(welded) < MIN_FILL_AREA:
|
||||||
|
return PackedVector2Array()
|
||||||
|
return welded
|
||||||
|
|
||||||
|
|
||||||
|
# Shoelace, unsigned: the clip preserves the hull's winding, but which way a
|
||||||
|
# hull winds is the definition's business and not worth depending on here.
|
||||||
|
func _area(points: PackedVector2Array) -> float:
|
||||||
|
var total: float = 0.0
|
||||||
|
var previous: Vector2 = points[points.size() - 1]
|
||||||
|
for point in points:
|
||||||
|
total += previous.x * point.y - point.x * previous.y
|
||||||
|
previous = point
|
||||||
|
return absf(total) * 0.5
|
||||||
|
|
||||||
|
|
||||||
func _draw() -> void:
|
func _draw() -> void:
|
||||||
|
|
@ -75,9 +129,8 @@ func _draw() -> void:
|
||||||
ship.core_color, 0.75)
|
ship.core_color, 0.75)
|
||||||
return
|
return
|
||||||
draw_colored_polygon(hull, Color(_cooling_color(), 0.58))
|
draw_colored_polygon(hull, Color(_cooling_color(), 0.58))
|
||||||
var filled: PackedVector2Array = _filled_hull(hull, cooldown_progress())
|
for piece in _filled_hull(hull, cooldown_progress()):
|
||||||
if filled.size() >= 3:
|
draw_colored_polygon(piece, Color(ready_color, 0.68))
|
||||||
draw_colored_polygon(filled, Color(ready_color, 0.68))
|
|
||||||
|
|
||||||
|
|
||||||
func _ready_color() -> Color: return Color.WHITE
|
func _ready_color() -> Color: return Color.WHITE
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue