@tool extends Control @export_enum("Attitude", "Heading", "Acceleration", "Vertical acceleration") var instrument: int = 0 ## Shares the circle radius with another instrument. @export var radius_source: Control var attitude := Vector3.ZERO var acceleration := Vector3.ZERO var available := false var full_scale := 5.0 func _ready() -> void: resized.connect(queue_redraw) theme_changed.connect(queue_redraw) if is_instance_valid(radius_source): radius_source.resized.connect(queue_redraw) func _text(at: Vector2, value: String, font_size: int = 16, color: Variant = null) -> void: draw_string(get_theme_default_font(), at, value, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, _ink() if color == null else color) func _line(a: Vector2, b: Vector2, color: Variant = null, width: float = 1.0) -> void: draw_line(a, b, _role("instrument_grid") if color == null else color, width, true) func _draw() -> void: match instrument: 0: _draw_attitude(available) 1: _draw_heading() 2: _draw_acceleration() 3: _draw_vertical() func _horizon_half(center: Vector2, radius: float, normal: Vector2, displacement: float) -> PackedVector2Array: var polygon := PackedVector2Array() var previous := Vector2(radius, 0) var previous_distance := previous.dot(normal) - displacement for step in range(1, 129): var point := Vector2.from_angle(step * TAU / 128.0) * radius var distance := point.dot(normal) - displacement if (distance >= 0) != (previous_distance >= 0): polygon.append(center + previous.lerp(point, previous_distance / (previous_distance - distance))) if distance >= 0: polygon.append(center + point) previous = point previous_distance = distance return polygon func _draw_attitude(available: bool) -> void: var center := circle_center() var radius := circle_radius() draw_circle(center, radius, _role("horizon_sky") if available else _role("instrument_empty")) if available: var rotation := deg_to_rad(-attitude.x) var normal := Vector2.DOWN.rotated(rotation) var direction := Vector2.RIGHT.rotated(rotation) var displacement := attitude.y * radius / 60.0 var ground := _horizon_half(center, radius, normal, displacement) if ground.size() >= 3: draw_colored_polygon(ground, _role("horizon_ground")) if absf(displacement) < radius: var half_chord := sqrt(radius * radius - displacement * displacement) _line(center + normal * displacement - direction * half_chord, center + normal * displacement + direction * half_chord, _ink(), 3) for pitch_mark in range(-90, 91, 10): var y: float = (attitude.y - pitch_mark) * radius / 60.0 if pitch_mark != 0 and absf(y) < radius - 25: var p := center + normal * y var width := 26.0 if pitch_mark % 20 == 0 else 15.0 _line(p - direction * width, p + direction * width, _ink(), 2) if pitch_mark % 20 == 0: _text(p + direction * (width + 7) + Vector2(0, 6), str(absi(pitch_mark)), 18) for bank in [-60, -30, 0, 30, 60]: var radial := Vector2.UP.rotated(deg_to_rad(bank)) _line(center + radial * (radius - 10), center + radial * (radius - 1), _ink(), 2) var pointer := Vector2.UP.rotated(rotation) draw_colored_polygon(PackedVector2Array([center + pointer * (radius - 17), center + pointer.rotated(0.07) * (radius - 29), center + pointer.rotated(-0.07) * (radius - 29)]), _role("instrument_accent")) draw_arc(center, radius, 0, TAU, 128, _role("instrument_grid"), 2, true) _line(center + Vector2(-47, 0), center + Vector2(-14, 0), _role("instrument_accent"), 4) _line(center + Vector2(14, 0), center + Vector2(47, 0), _role("instrument_accent"), 4) _line(center + Vector2(-14, 0), center + Vector2(-14, 8), _role("instrument_accent"), 3) _line(center + Vector2(14, 0), center + Vector2(14, 8), _role("instrument_accent"), 3) draw_circle(center, 4, _role("instrument_accent")) func _heading_tick_x(degrees: float) -> float: return size.x / 2 + (degrees - attitude.z) * size.x / 100.0 func _draw_heading() -> void: if available: var first_tick := int(floor((attitude.z - 50.0) / 10.0)) * 10 for degrees in range(first_tick, first_tick + 120, 10): var x := _heading_tick_x(degrees) if x < 18 or x > size.x - 18: continue _line(Vector2(x, 7), Vector2(x, 16), _ink(), 2) if degrees % 20 == 0: _text(Vector2(x - 17, 36), "%03d" % int(fposmod(degrees, 360)), 18, _ink()) _line(Vector2(size.x / 2, 0), Vector2(size.x / 2, 20), _role("instrument_accent"), 3) func _draw_acceleration() -> void: var radius := circle_radius() var center := circle_center() for fraction in [0.5, 1.0]: draw_arc(center, radius * fraction, 0, TAU, 96, _role("instrument_grid"), 2, true) _line(center - Vector2(radius, 0), center + Vector2(radius, 0), _role("instrument_axis")) _line(center - Vector2(0, radius), center + Vector2(0, radius), _role("instrument_axis")) _text(center + Vector2(-14, -radius - 8), "+X", 18) _text(center + Vector2(radius + 2, 6), "+Y", 18) if available: var planar := Vector2(acceleration.y, -acceleration.x) / maxf(full_scale, 0.1) var endpoint := center + planar.limit_length(1.0) * radius _line(center, endpoint, _role("instrument_accent"), 4) draw_circle(endpoint, 7, _role("instrument_accent")) func _draw_vertical() -> void: var center := circle_center() var radius := circle_radius() _line(center - Vector2(0, radius), center + Vector2(0, radius), _role("instrument_axis"), 2) for fraction in [-1.0, -0.5, 0.0, 0.5, 1.0]: var y: float = center.y + fraction * radius _line(Vector2(center.x - 6, y), Vector2(center.x + 6, y), _ink(), 2) if available: var y := center.y + clampf(acceleration.z / maxf(full_scale, 0.1), -1, 1) * radius _line(Vector2(center.x - 10, y), Vector2(center.x + 10, y), _role("instrument_accent"), 5) func circle_radius() -> float: if is_instance_valid(radius_source): return radius_source.circle_radius() return maxf(1.0, minf(size.x - 24, size.y - 44) / 2) func circle_center() -> Vector2: return size / 2 + Vector2(0, 10) func _ink() -> Color: return get_theme_color("font_color", "Label") func _role(name: StringName) -> Color: return get_theme_color(name, "SurfaceSide")