mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-08-31 13:13:44 +08:00
Aethergauge Pretty
This commit is contained in:
@@ -11,6 +11,11 @@ from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.starpilot_border import _csc_state, _intensity, _glow_color
|
||||
from openpilot.selfdrive.ui.lib.starpilot_status import get_border_color
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.widget_style import (
|
||||
CONTROL_BORDER,
|
||||
CONTROL_BORDER_WIDTH,
|
||||
draw_control_card,
|
||||
)
|
||||
|
||||
|
||||
# --- Scale factor (single knob for all pixel-space dimensions) ---
|
||||
@@ -35,6 +40,31 @@ ROAD_THICKNESS = 4.0 * SCALE
|
||||
ROAD_HALF_SIZE = 40.0 * SCALE
|
||||
ROAD_EDGE_INSET = 2.0 * SCALE
|
||||
FILL_ALPHA = 90
|
||||
ROAD_CONTOUR_WIDTH = 2.0 * SCALE
|
||||
|
||||
# The gauge overlays an unconstrained camera image. It shares the visible
|
||||
# control-card frame used by Set Speed and MAP, then owns two contained
|
||||
# viewports: animated road graphics above, glanceable status metrics below.
|
||||
# Nothing may draw outside the card.
|
||||
AETHER_CONTENT_INSET_X = max(5.0 * SCALE, CONTROL_BORDER_WIDTH / 2.0 + 3.0 * SCALE)
|
||||
AETHER_ROAD_TOP_INSET = max(5.0 * SCALE, CONTROL_BORDER_WIDTH / 2.0 + 3.0 * SCALE)
|
||||
AETHER_ROAD_VIEWPORT_HEIGHT = 75.0 * SCALE
|
||||
AETHER_ROAD_TO_CRADLE_GAP = 2.0 * SCALE
|
||||
AETHER_CRADLE_TOP = 83.0 * SCALE
|
||||
AETHER_CRADLE_BOTTOM_INSET = max(2.0 * SCALE, CONTROL_BORDER_WIDTH / 2.0 + 2.0 * SCALE)
|
||||
|
||||
AETHER_LABEL_SIZE = int(16.0 * SCALE)
|
||||
AETHER_LABEL_SLOT_HEIGHT = AETHER_LABEL_SIZE
|
||||
AETHER_VALUE_GAP = 2.0 * SCALE
|
||||
AETHER_VALUE_MAX_SIZE = int(40.0 * SCALE)
|
||||
AETHER_VALUE_MIN_SIZE = int(24.0 * SCALE)
|
||||
AETHER_LEAD_MAX_SIZE = int(32.0 * SCALE)
|
||||
AETHER_LEAD_MIN_SIZE = int(18.0 * SCALE)
|
||||
AETHER_REDUCTION_SIZE = int(16.0 * SCALE)
|
||||
AETHER_REDUCTION_GAP = 4.0 * SCALE
|
||||
AETHER_ACCENT_GAP = 2.0 * SCALE
|
||||
AETHER_UNIT_SIZE = int(18.0 * SCALE)
|
||||
AETHER_UNIT_GAP = 2.0 * SCALE
|
||||
|
||||
STOP_SNAP_THRESHOLD = 0.5
|
||||
STOP_LERP_RATE = 0.25
|
||||
@@ -51,7 +81,10 @@ LEAD_STOPPED_SPEED_THRESHOLD = 1.0
|
||||
COLOR_FORCE_STOP = rl.Color(255, 30, 60, 255)
|
||||
COLOR_LEAD_STOPPED = rl.Color(255, 60, 60, 255)
|
||||
COLOR_LEAD_SLOWER = rl.Color(255, 191, 0, 255)
|
||||
COLOR_SHADOW = rl.Color(0, 0, 0, 100)
|
||||
COLOR_CONTOUR = rl.Color(2, 6, 9, 235)
|
||||
COLOR_AETHER_CARD = rl.Color(9, 14, 18, 226)
|
||||
COLOR_PRIMARY_TEXT = rl.Color(245, 250, 252, 255)
|
||||
COLOR_SECONDARY_TEXT = rl.Color(225, 235, 240, 235)
|
||||
COLOR_STOP_SIGN_OUTLINE = rl.Color(255, 255, 255, 255)
|
||||
COLOR_STOP_LINE_GLOW = rl.Color(255, 30, 60, 255)
|
||||
COLOR_STOP_LINE_CORE = rl.Color(255, 200, 200, 255)
|
||||
@@ -150,12 +183,119 @@ def _get_perspective_offset(t: float, data: 'AetherGaugeData | None') -> float:
|
||||
max_offset_top = math.tanh(path_y_far * PERSPECTIVE_GAIN) * PERSPECTIVE_MAX_OFFSET
|
||||
return max_offset_top * (t ** PERSPECTIVE_EXPONENT)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AetherGaugeLayout:
|
||||
card: rl.Rectangle
|
||||
road: rl.Rectangle
|
||||
cradle: rl.Rectangle
|
||||
|
||||
|
||||
def _snapped_rect(x: float, y: float, width: float, height: float) -> rl.Rectangle:
|
||||
return rl.Rectangle(
|
||||
int(round(x)), int(round(y)), max(1, int(round(width))), max(1, int(round(height))),
|
||||
)
|
||||
|
||||
|
||||
def _aether_layout(rect: rl.Rectangle) -> AetherGaugeLayout:
|
||||
"""Return the one geometry contract used by every AetherGauge primitive."""
|
||||
# Match the Set Speed and MAP frame exactly; internal viewports provide the
|
||||
# clearance needed by the denser AetherGauge road and metric visuals.
|
||||
card = _snapped_rect(rect.x, rect.y, rect.width, rect.height)
|
||||
|
||||
cradle_top = int(round(card.y + AETHER_CRADLE_TOP))
|
||||
cradle_bottom = int(round(card.y + card.height - AETHER_CRADLE_BOTTOM_INSET))
|
||||
road_y = int(round(card.y + AETHER_ROAD_TOP_INSET))
|
||||
road_height = min(
|
||||
int(round(AETHER_ROAD_VIEWPORT_HEIGHT)),
|
||||
max(1, cradle_top - road_y - int(round(AETHER_ROAD_TO_CRADLE_GAP))),
|
||||
)
|
||||
content_x = int(round(card.x + AETHER_CONTENT_INSET_X))
|
||||
content_width = max(1, int(round(card.width - 2.0 * AETHER_CONTENT_INSET_X)))
|
||||
|
||||
return AetherGaugeLayout(
|
||||
card=card,
|
||||
road=_snapped_rect(content_x, road_y, content_width, road_height),
|
||||
cradle=_snapped_rect(content_x, cradle_top, content_width, max(1, cradle_bottom - cradle_top)),
|
||||
)
|
||||
|
||||
|
||||
def _draw_aether_card(layout: AetherGaugeLayout, alpha: float = 1.0) -> None:
|
||||
"""Draw the shared control frame with AetherGauge's deeper contrast fill."""
|
||||
draw_control_card(
|
||||
layout.card,
|
||||
fill=_fade(COLOR_AETHER_CARD, alpha),
|
||||
border=_fade(CONTROL_BORDER, alpha),
|
||||
)
|
||||
|
||||
|
||||
def _text_outline_px(size: int) -> int:
|
||||
return max(1, int(round(size / (28.0 * SCALE))))
|
||||
|
||||
|
||||
def _draw_text_with_shadow(font: rl.Font, text: str, pos: rl.Vector2, size: int, color: rl.Color, alpha: float = 1.0):
|
||||
for dx, dy in ((-1, -1), (1, -1), (-1, 1), (1, 1)):
|
||||
rl.draw_text_ex(font, text, rl.Vector2(pos.x + dx, pos.y + dy), size, 0, _fade(rl.BLACK, alpha))
|
||||
outline_px = _text_outline_px(size)
|
||||
for dx, dy in (
|
||||
(-outline_px, 0), (outline_px, 0), (0, -outline_px), (0, outline_px),
|
||||
(-outline_px, -outline_px), (outline_px, -outline_px),
|
||||
(-outline_px, outline_px), (outline_px, outline_px),
|
||||
):
|
||||
rl.draw_text_ex(font, text, rl.Vector2(pos.x + dx, pos.y + dy), size, 0, _fade(COLOR_CONTOUR, alpha))
|
||||
rl.draw_text_ex(font, text, pos, size, 0, _fade(color, alpha))
|
||||
|
||||
|
||||
def _fit_text_size(font: rl.Font, text: str, max_size: int, min_size: int,
|
||||
max_width: float, max_height: float) -> tuple[int, rl.Vector2]:
|
||||
"""Measure down to a guaranteed-safe text size for the supplied content box."""
|
||||
for size in range(max_size, min_size - 1, -1):
|
||||
text_size = measure_text_cached(font, text, size)
|
||||
outline = _text_outline_px(size)
|
||||
if text_size.x + 2 * outline <= max_width and text_size.y + 2 * outline <= max_height:
|
||||
return size, text_size
|
||||
|
||||
# Keep pathological future values contained even if they exceed the intended
|
||||
# type scale. Normal gauge data never reaches this fallback.
|
||||
for size in range(min_size - 1, 0, -1):
|
||||
text_size = measure_text_cached(font, text, size)
|
||||
outline = _text_outline_px(size)
|
||||
if text_size.x + 2 * outline <= max_width and text_size.y + 2 * outline <= max_height:
|
||||
return size, text_size
|
||||
|
||||
return 1, measure_text_cached(font, text, 1)
|
||||
|
||||
|
||||
def _fit_numeric_line(font_bold: rl.Font, font_medium: rl.Font, value: str, reduction: str,
|
||||
max_width: float, max_height: float) -> 'tuple[int, rl.Vector2, rl.Vector2 | None, bool]':
|
||||
"""Fit the primary value and optional reduction inline, or reflow the reduction."""
|
||||
reduction_size = measure_text_cached(font_medium, reduction, AETHER_REDUCTION_SIZE) if reduction else None
|
||||
|
||||
for value_font_size in range(AETHER_VALUE_MAX_SIZE, 0, -1):
|
||||
value_size = measure_text_cached(font_bold, value, value_font_size)
|
||||
value_outline = _text_outline_px(value_font_size)
|
||||
if value_size.y + 2 * value_outline > max_height:
|
||||
continue
|
||||
|
||||
if reduction_size is None:
|
||||
if value_size.x + 2 * value_outline <= max_width:
|
||||
return value_font_size, value_size, None, True
|
||||
continue
|
||||
|
||||
reduction_outline = _text_outline_px(AETHER_REDUCTION_SIZE)
|
||||
inline_width = value_size.x + AETHER_REDUCTION_GAP + reduction_size.x + 2 * max(value_outline, reduction_outline)
|
||||
if inline_width <= max_width:
|
||||
return value_font_size, value_size, reduction_size, True
|
||||
|
||||
if value_font_size == AETHER_VALUE_MIN_SIZE:
|
||||
break
|
||||
|
||||
# The normal source values fit inline. This fallback keeps a future long
|
||||
# value contained by using the reserved label row for the reduction.
|
||||
value_font_size, value_size = _fit_text_size(
|
||||
font_bold, value, AETHER_VALUE_MIN_SIZE, 1, max_width, max_height,
|
||||
)
|
||||
return value_font_size, value_size, reduction_size, False
|
||||
|
||||
|
||||
# --- Data model ---
|
||||
|
||||
class IndicatorType(Enum):
|
||||
@@ -178,6 +318,14 @@ class AetherGaugeData:
|
||||
is_numeric: bool = False
|
||||
|
||||
|
||||
def _state_label(data: AetherGaugeData) -> str:
|
||||
return {
|
||||
IndicatorType.FORCE_STOP: "STOP",
|
||||
IndicatorType.STOP_LIGHT: "RED LIGHT",
|
||||
IndicatorType.LEAD: "LEAD",
|
||||
}.get(data.indicator_type, "")
|
||||
|
||||
|
||||
# --- Source functions (replaces class-based sources) ---
|
||||
|
||||
def _build_curve_gauge_data(curvature: float, target_speed: float, v_cruise: float) -> AetherGaugeData:
|
||||
@@ -460,16 +608,13 @@ class AetherGauge:
|
||||
if not data:
|
||||
return
|
||||
|
||||
if cx is None or bottom is None:
|
||||
if cx is None:
|
||||
base_cx = rect.x + rect.width / 2
|
||||
cy_speed = rect.y + 180 * SCALE
|
||||
speed_text = str(round(current_speed))
|
||||
speed_text_size = measure_text_cached(font_bold, speed_text, int(176 * SCALE))
|
||||
icx = base_cx - speed_text_size.x / 2 - 70.0 * SCALE
|
||||
icy = cy_speed - 39.5 * SCALE
|
||||
else:
|
||||
icx = cx
|
||||
icy = bottom - ROAD_HALF_SIZE
|
||||
|
||||
if data.indicator_type != self._last_indicator_type:
|
||||
self._dist_filter.x = data.indicator_value
|
||||
@@ -497,60 +642,71 @@ class AetherGauge:
|
||||
data.unit = unit
|
||||
|
||||
if data.indicator_type in (IndicatorType.ROAD_CURVE, IndicatorType.FORCE_STOP, IndicatorType.LEAD, IndicatorType.STOP_LIGHT):
|
||||
self._render_unified_road(rect, icx, icy, data, font_bold, font_medium, alpha)
|
||||
self._render_unified_road(rect, icx, data, font_bold, font_medium, alpha)
|
||||
|
||||
def _render_unified_road(self, rect, icx, icy, data, font_bold, font_medium, alpha=1.0):
|
||||
bottom = icy + ROAD_HALF_SIZE
|
||||
def _render_unified_road(self, rect, icx, data, font_bold, font_medium, alpha=1.0):
|
||||
layout = _aether_layout(rect)
|
||||
_draw_aether_card(layout, alpha)
|
||||
bottom = layout.road.y + layout.road.height
|
||||
road_icy = bottom - ROAD_HALF_SIZE
|
||||
|
||||
if data.indicator_type in (IndicatorType.FORCE_STOP, IndicatorType.STOP_LIGHT):
|
||||
distance = 15.0
|
||||
else:
|
||||
distance = data.indicator_value
|
||||
|
||||
points_left = []
|
||||
points_right = []
|
||||
self._current_road_h = self._road_h_filter.update(_get_road_height(data))
|
||||
road_h = self._current_road_h
|
||||
# Custom widgets render after AugmentedRoadView releases its content
|
||||
# scissor. Keep every animated primitive inside the road viewport here.
|
||||
rl.begin_scissor_mode(
|
||||
int(layout.road.x), int(layout.road.y), int(layout.road.width), int(layout.road.height),
|
||||
)
|
||||
try:
|
||||
points_left = []
|
||||
points_right = []
|
||||
self._current_road_h = min(self._road_h_filter.update(_get_road_height(data)), layout.road.height)
|
||||
road_h = self._current_road_h
|
||||
|
||||
for i in range(ROAD_SEGMENTS + 1):
|
||||
t = i / ROAD_SEGMENTS
|
||||
offset = _get_perspective_offset(t, data)
|
||||
cx_t = icx + offset
|
||||
y_t = bottom - t * road_h
|
||||
w_t = ROAD_W_BOTTOM - t * (ROAD_W_BOTTOM - ROAD_W_TOP)
|
||||
for i in range(ROAD_SEGMENTS + 1):
|
||||
t = i / ROAD_SEGMENTS
|
||||
offset = _get_perspective_offset(t, data)
|
||||
cx_t = icx + offset
|
||||
y_t = bottom - t * road_h
|
||||
w_t = ROAD_W_BOTTOM - t * (ROAD_W_BOTTOM - ROAD_W_TOP)
|
||||
|
||||
points_left.append(rl.Vector2(cx_t - w_t, y_t))
|
||||
points_right.append(rl.Vector2(cx_t + w_t, y_t))
|
||||
points_left.append(rl.Vector2(cx_t - w_t, y_t))
|
||||
points_right.append(rl.Vector2(cx_t + w_t, y_t))
|
||||
|
||||
fill_color = _fade(_with_alpha(data.color, FILL_ALPHA), alpha)
|
||||
for i in range(ROAD_SEGMENTS):
|
||||
t = i / ROAD_SEGMENTS
|
||||
stroke = ROAD_THICKNESS * (1.0 - 0.6 * t)
|
||||
rl.draw_triangle(points_left[i], points_right[i], points_left[i+1], fill_color)
|
||||
rl.draw_triangle(points_right[i], points_right[i+1], points_left[i+1], fill_color)
|
||||
rl.draw_line_ex(points_left[i], points_left[i+1], stroke, _fade(COLOR_SHADOW, alpha))
|
||||
rl.draw_line_ex(points_right[i], points_right[i+1], stroke, _fade(COLOR_SHADOW, alpha))
|
||||
rl.draw_line_ex(points_left[i], points_left[i+1], stroke, _fade(data.color, alpha))
|
||||
rl.draw_line_ex(points_right[i], points_right[i+1], stroke, _fade(data.color, alpha))
|
||||
fill_color = _fade(_with_alpha(data.color, FILL_ALPHA), alpha)
|
||||
for i in range(ROAD_SEGMENTS):
|
||||
t = i / ROAD_SEGMENTS
|
||||
stroke = ROAD_THICKNESS * (1.0 - 0.6 * t)
|
||||
rl.draw_triangle(points_left[i], points_right[i], points_left[i+1], fill_color)
|
||||
rl.draw_triangle(points_right[i], points_right[i+1], points_left[i+1], fill_color)
|
||||
rl.draw_line_ex(points_left[i], points_left[i+1], stroke + ROAD_CONTOUR_WIDTH, _fade(COLOR_CONTOUR, alpha))
|
||||
rl.draw_line_ex(points_right[i], points_right[i+1], stroke + ROAD_CONTOUR_WIDTH, _fade(COLOR_CONTOUR, alpha))
|
||||
rl.draw_line_ex(points_left[i], points_left[i+1], stroke, _fade(data.color, alpha))
|
||||
rl.draw_line_ex(points_right[i], points_right[i+1], stroke, _fade(data.color, alpha))
|
||||
|
||||
it = data.indicator_type
|
||||
it = data.indicator_type
|
||||
|
||||
if it in (IndicatorType.STOP_LIGHT, IndicatorType.FORCE_STOP):
|
||||
self._draw_stop_line(icx, bottom, distance, data, alpha)
|
||||
if it in (IndicatorType.STOP_LIGHT, IndicatorType.FORCE_STOP):
|
||||
self._draw_stop_line(icx, bottom, distance, data, alpha)
|
||||
|
||||
if it == IndicatorType.LEAD:
|
||||
self._draw_lead_car(icx, bottom, data, alpha)
|
||||
if it == IndicatorType.LEAD:
|
||||
self._draw_lead_car(icx, bottom, data, alpha)
|
||||
|
||||
if it in (IndicatorType.ROAD_CURVE, IndicatorType.FORCE_STOP, IndicatorType.STOP_LIGHT):
|
||||
self._draw_standard_chevrons(icx, bottom, distance, data, alpha)
|
||||
if it in (IndicatorType.ROAD_CURVE, IndicatorType.FORCE_STOP, IndicatorType.STOP_LIGHT):
|
||||
self._draw_standard_chevrons(icx, bottom, distance, data, alpha)
|
||||
|
||||
if it == IndicatorType.STOP_LIGHT:
|
||||
self._draw_traffic_light(icx, icy, distance, data, alpha)
|
||||
if it == IndicatorType.STOP_LIGHT:
|
||||
self._draw_traffic_light(icx, road_icy, distance, data, alpha)
|
||||
|
||||
if it == IndicatorType.FORCE_STOP:
|
||||
self._draw_approaching_stop_sign(icx, icy, bottom, distance, data, font_bold, alpha)
|
||||
if it == IndicatorType.FORCE_STOP:
|
||||
self._draw_approaching_stop_sign(icx, road_icy, bottom, distance, data, font_bold, alpha)
|
||||
finally:
|
||||
rl.end_scissor_mode()
|
||||
|
||||
self._draw_mini_cradle(icx, bottom, data, font_bold, font_medium, alpha)
|
||||
self._draw_mini_cradle(layout.cradle, data, font_bold, font_medium, alpha)
|
||||
|
||||
def _draw_stop_line(self, icx, bottom, distance, data, alpha=1.0):
|
||||
t, cx_line, cy_line = _road_xy(distance, icx, bottom, data, self._current_road_h)
|
||||
@@ -562,6 +718,10 @@ class AetherGauge:
|
||||
fade = 1.0 - t * 0.5
|
||||
glow_a = int(150 * fade)
|
||||
|
||||
rl.draw_line_ex(
|
||||
p_left, p_right, max(3.0 * SCALE, 7.0 * SCALE * fade) + ROAD_CONTOUR_WIDTH,
|
||||
_fade(COLOR_CONTOUR, alpha),
|
||||
)
|
||||
rl.draw_line_ex(p_left, p_right, max(3.0 * SCALE, 7.0 * SCALE * fade), _fade(_with_alpha(COLOR_STOP_LINE_GLOW, glow_a), alpha))
|
||||
rl.draw_line_ex(p_left, p_right, max(1.5 * SCALE, 3.5 * SCALE * fade), _fade(COLOR_STOP_LINE_CORE, alpha))
|
||||
|
||||
@@ -642,7 +802,10 @@ class AetherGauge:
|
||||
|
||||
chev_a = max(0, min(255, int(data.color.a * (1.0 - t / t_lead) * math.sin(t / t_lead * math.pi))))
|
||||
c_color = _fade(_with_alpha(data.color, chev_a), alpha)
|
||||
c_contour = _fade(_with_alpha(COLOR_CONTOUR, int(chev_a * 0.9)), alpha)
|
||||
|
||||
rl.draw_line_ex(rl.Vector2(lx, cy_t - chevron_w * 0.5), rl.Vector2(cx_t, cy_t), chevron_thick + ROAD_CONTOUR_WIDTH, c_contour)
|
||||
rl.draw_line_ex(rl.Vector2(rx, cy_t - chevron_w * 0.5), rl.Vector2(cx_t, cy_t), chevron_thick + ROAD_CONTOUR_WIDTH, c_contour)
|
||||
rl.draw_line_ex(rl.Vector2(lx, cy_t - chevron_w * 0.5), rl.Vector2(cx_t, cy_t), chevron_thick, c_color)
|
||||
rl.draw_line_ex(rl.Vector2(rx, cy_t - chevron_w * 0.5), rl.Vector2(cx_t, cy_t), chevron_thick, c_color)
|
||||
|
||||
@@ -690,7 +853,10 @@ class AetherGauge:
|
||||
chev_a = max(0, min(255, int(data.color.a * alpha_factor)))
|
||||
chev_color = _fade(_with_alpha(data.color, chev_a), alpha)
|
||||
chev_shadow = _fade(rl.Color(0, 0, 0, int(chev_a * 0.5)), alpha)
|
||||
chev_contour = _fade(_with_alpha(COLOR_CONTOUR, int(chev_a * 0.9)), alpha)
|
||||
|
||||
rl.draw_line_ex(rl.Vector2(lx, ly), rl.Vector2(cx_t, cy_t), chevron_thick + ROAD_CONTOUR_WIDTH, chev_contour)
|
||||
rl.draw_line_ex(rl.Vector2(rx, ry), rl.Vector2(cx_t, cy_t), chevron_thick + ROAD_CONTOUR_WIDTH, chev_contour)
|
||||
rl.draw_line_ex(rl.Vector2(lx, ly + 1.5), rl.Vector2(cx_t, cy_t + 1.5), chevron_thick, chev_shadow)
|
||||
rl.draw_line_ex(rl.Vector2(rx, ry + 1.5), rl.Vector2(cx_t, cy_t + 1.5), chevron_thick, chev_shadow)
|
||||
rl.draw_line_ex(rl.Vector2(lx, ly), rl.Vector2(cx_t, cy_t), chevron_thick, chev_color)
|
||||
@@ -761,18 +927,53 @@ class AetherGauge:
|
||||
stop_txt_size = measure_text_cached(font_bold, "STOP", stop_font_size)
|
||||
rl.draw_text_ex(font_bold, "STOP", rl.Vector2(cx_stop - stop_txt_size.x / 2, y_sign - stop_txt_size.y / 2), stop_font_size, 0, _fade(rl.WHITE, alpha))
|
||||
|
||||
def _draw_mini_cradle(self, cx, bottom, data, font_bold, font_medium, alpha=1.0):
|
||||
def _draw_mini_cradle(self, cradle, data, font_bold, font_medium, alpha=1.0):
|
||||
if not data.text:
|
||||
return
|
||||
|
||||
if data.is_numeric:
|
||||
val_size = measure_text_cached(font_bold, data.text, int(50 * SCALE))
|
||||
val_pos = rl.Vector2(int(cx - val_size.x / 2), int(bottom + 6 * SCALE))
|
||||
_draw_text_with_shadow(font_bold, data.text, val_pos, int(50 * SCALE), data.color, alpha)
|
||||
label = _state_label(data)
|
||||
metric_top = int(round(cradle.y + AETHER_LABEL_SLOT_HEIGHT + AETHER_VALUE_GAP))
|
||||
cradle_right = cradle.x + cradle.width
|
||||
|
||||
accent_y = int(val_pos.y + val_size.y + 2 * SCALE)
|
||||
accent_w = int(val_size.x + 16 * SCALE)
|
||||
accent_x = int(cx - accent_w / 2)
|
||||
if data.is_numeric:
|
||||
unit_size = measure_text_cached(font_medium, data.unit, AETHER_UNIT_SIZE) if data.unit else None
|
||||
footer_height = AETHER_ACCENT_GAP
|
||||
if unit_size is not None:
|
||||
footer_height += AETHER_UNIT_GAP + unit_size.y
|
||||
max_metric_height = max(1.0, cradle.y + cradle.height - metric_top - footer_height)
|
||||
max_metric_width = max(1.0, cradle.width - 2 * _text_outline_px(AETHER_VALUE_MAX_SIZE))
|
||||
value_font_size, value_size, reduction_size, reduction_inline = _fit_numeric_line(
|
||||
font_bold, font_medium, data.text, data.reduction_text, max_metric_width, max_metric_height,
|
||||
)
|
||||
value_outline = _text_outline_px(value_font_size)
|
||||
reduction_outline = _text_outline_px(AETHER_REDUCTION_SIZE) if reduction_size is not None else 0
|
||||
|
||||
group_width = value_size.x
|
||||
if reduction_size is not None and reduction_inline:
|
||||
group_width += AETHER_REDUCTION_GAP + reduction_size.x
|
||||
value_x = int(round(cradle.x + (cradle.width - group_width) / 2))
|
||||
value_pos = rl.Vector2(value_x, metric_top)
|
||||
|
||||
label_max_width = cradle.width - 2 * _text_outline_px(AETHER_LABEL_SIZE)
|
||||
if label and reduction_size is not None and not reduction_inline:
|
||||
label_max_width -= reduction_size.x + AETHER_REDUCTION_GAP + reduction_outline
|
||||
if label:
|
||||
label_font_size, label_size = _fit_text_size(
|
||||
font_medium, label, AETHER_LABEL_SIZE, 1, max(1.0, label_max_width), AETHER_LABEL_SLOT_HEIGHT,
|
||||
)
|
||||
label_x = cradle.x + value_outline if reduction_size is not None and not reduction_inline else cradle.x + (cradle.width - label_size.x) / 2
|
||||
label_y = cradle.y + (AETHER_LABEL_SLOT_HEIGHT - label_size.y) / 2
|
||||
_draw_text_with_shadow(
|
||||
font_medium, label, rl.Vector2(int(round(label_x)), int(round(label_y))),
|
||||
label_font_size, COLOR_PRIMARY_TEXT, alpha,
|
||||
)
|
||||
|
||||
_draw_text_with_shadow(font_bold, data.text, value_pos, value_font_size, COLOR_PRIMARY_TEXT, alpha)
|
||||
|
||||
accent_y = int(round(value_pos.y + value_size.y + AETHER_ACCENT_GAP))
|
||||
accent_w = min(max_metric_width, value_size.x + 16 * SCALE)
|
||||
accent_x = int(round(value_pos.x + value_size.x / 2 - accent_w / 2))
|
||||
accent_x = max(int(round(cradle.x + value_outline)), min(accent_x, int(round(cradle_right - value_outline - accent_w))))
|
||||
rl.draw_line_ex(
|
||||
rl.Vector2(accent_x, accent_y),
|
||||
rl.Vector2(accent_x + accent_w, accent_y),
|
||||
@@ -780,16 +981,42 @@ class AetherGauge:
|
||||
_fade(_with_alpha(data.color, 160), alpha),
|
||||
)
|
||||
|
||||
if data.reduction_text:
|
||||
red_size = measure_text_cached(font_medium, data.reduction_text, int(22 * SCALE))
|
||||
red_pos = rl.Vector2(int(cx + val_size.x / 2 + 6 * SCALE), int(val_pos.y + val_size.y / 2 - red_size.y / 2))
|
||||
_draw_text_with_shadow(font_medium, data.reduction_text, red_pos, int(22 * SCALE), COLOR_REDUCTION, alpha)
|
||||
if reduction_size is not None:
|
||||
if reduction_inline:
|
||||
reduction_x = int(round(value_pos.x + value_size.x + AETHER_REDUCTION_GAP))
|
||||
reduction_y = int(round(value_pos.y + (value_size.y - reduction_size.y) / 2))
|
||||
else:
|
||||
reduction_x = int(round(cradle_right - reduction_size.x - reduction_outline))
|
||||
reduction_y = int(round(cradle.y + (AETHER_LABEL_SLOT_HEIGHT - reduction_size.y) / 2))
|
||||
_draw_text_with_shadow(
|
||||
font_medium, data.reduction_text, rl.Vector2(reduction_x, reduction_y),
|
||||
AETHER_REDUCTION_SIZE, COLOR_REDUCTION, alpha,
|
||||
)
|
||||
|
||||
if data.unit:
|
||||
unit_size = measure_text_cached(font_medium, data.unit, int(20 * SCALE))
|
||||
unit_pos = rl.Vector2(int(cx - unit_size.x / 2), int(accent_y + 3 * SCALE))
|
||||
_draw_text_with_shadow(font_medium, data.unit, unit_pos, int(20 * SCALE), rl.Color(255, 255, 255, 180), alpha)
|
||||
if unit_size is not None:
|
||||
unit_pos = rl.Vector2(
|
||||
int(round(cradle.x + (cradle.width - unit_size.x) / 2)),
|
||||
int(round(accent_y + AETHER_UNIT_GAP)),
|
||||
)
|
||||
_draw_text_with_shadow(font_medium, data.unit, unit_pos, AETHER_UNIT_SIZE, COLOR_SECONDARY_TEXT, alpha)
|
||||
else:
|
||||
val_size = measure_text_cached(font_bold, data.text, int(32 * SCALE))
|
||||
val_pos = rl.Vector2(int(cx - val_size.x / 2), int(bottom + 10 * SCALE))
|
||||
_draw_text_with_shadow(font_bold, data.text, val_pos, int(32 * SCALE), data.color, alpha)
|
||||
if label:
|
||||
label_font_size, label_size = _fit_text_size(
|
||||
font_medium, label, AETHER_LABEL_SIZE, 1,
|
||||
cradle.width - 2 * _text_outline_px(AETHER_LABEL_SIZE), AETHER_LABEL_SLOT_HEIGHT,
|
||||
)
|
||||
label_pos = rl.Vector2(
|
||||
int(round(cradle.x + (cradle.width - label_size.x) / 2)),
|
||||
int(round(cradle.y + (AETHER_LABEL_SLOT_HEIGHT - label_size.y) / 2)),
|
||||
)
|
||||
_draw_text_with_shadow(font_medium, label, label_pos, label_font_size, COLOR_PRIMARY_TEXT, alpha)
|
||||
|
||||
lead_font_size, lead_size = _fit_text_size(
|
||||
font_bold, data.text, AETHER_LEAD_MAX_SIZE, AETHER_LEAD_MIN_SIZE,
|
||||
cradle.width - 2 * _text_outline_px(AETHER_LEAD_MAX_SIZE),
|
||||
cradle.y + cradle.height - metric_top,
|
||||
)
|
||||
lead_pos = rl.Vector2(
|
||||
int(round(cradle.x + (cradle.width - lead_size.x) / 2)), metric_top,
|
||||
)
|
||||
_draw_text_with_shadow(font_bold, data.text, lead_pos, lead_font_size, COLOR_PRIMARY_TEXT, alpha)
|
||||
|
||||
@@ -2,7 +2,7 @@ import pyray as rl
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.widgets.base import LayoutWidget
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.aethergauge import AetherGauge, _fade
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.aethergauge import AetherGauge
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.widget_style import CONTROL_WIDTH
|
||||
|
||||
class AetherGaugeWidget(LayoutWidget):
|
||||
@@ -29,12 +29,10 @@ class AetherGaugeWidget(LayoutWidget):
|
||||
return
|
||||
|
||||
cx = rect.x + rect.width / 2
|
||||
# Set the road bottom to rect.y + 145, leaving 115px for the text cradle underneath
|
||||
bottom = rect.y + 145.0
|
||||
# AetherGauge derives its bounded road and metric viewports from rect.
|
||||
self._aethergauge.render(
|
||||
rect, self._font_bold, self._font_medium,
|
||||
current_speed=self.hud_renderer.speed,
|
||||
cx=cx,
|
||||
bottom=bottom,
|
||||
alpha=alpha,
|
||||
)
|
||||
|
||||
@@ -31,16 +31,20 @@ mock_ui_state = types.SimpleNamespace(
|
||||
},
|
||||
)
|
||||
from openpilot.selfdrive.ui.onroad.starpilot import aethergauge
|
||||
from openpilot.selfdrive.ui.onroad.starpilot import widget_style
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.aethergauge import (
|
||||
AetherGauge,
|
||||
AetherGaugeData,
|
||||
IndicatorType,
|
||||
_aether_layout,
|
||||
_cem_curvature_data,
|
||||
_draw_aether_card,
|
||||
_is_cem_curvature,
|
||||
_is_curve_speed,
|
||||
_is_lead,
|
||||
_is_stop_light,
|
||||
_lead_data,
|
||||
_state_label,
|
||||
)
|
||||
from openpilot.starpilot.common.experimental_state import CEStatus
|
||||
|
||||
@@ -295,7 +299,7 @@ def test_monotonic_ratchet_clamp_prevents_upward_bounce(monkeypatch):
|
||||
|
||||
rendered_data = []
|
||||
gauge = AetherGauge()
|
||||
monkeypatch.setattr(gauge, "_render_unified_road", lambda rect, cx, cy, data, fb, fm, alpha: rendered_data.append(data))
|
||||
monkeypatch.setattr(gauge, "_render_unified_road", lambda rect, cx, data, fb, fm, alpha: rendered_data.append(data))
|
||||
|
||||
# Initial frame at 30m
|
||||
_set_plan(redLight=True, forcingStopLength=30.0)
|
||||
@@ -307,3 +311,137 @@ def test_monotonic_ratchet_clamp_prevents_upward_bounce(monkeypatch):
|
||||
gauge.render(None, None, None, current_speed=10.0, cx=100.0, bottom=200.0)
|
||||
# Ratchet clamp must prevent the display number from increasing above 30m!
|
||||
assert int(rendered_data[-1].text) <= 30
|
||||
|
||||
|
||||
def test_aether_card_reuses_the_shared_control_frame(monkeypatch):
|
||||
triangles = []
|
||||
fills = []
|
||||
keylines = []
|
||||
|
||||
monkeypatch.setattr(aethergauge.rl, "draw_triangle", lambda *args: triangles.append(args))
|
||||
monkeypatch.setattr(aethergauge.rl, "draw_rectangle_rounded", lambda *args: fills.append(args))
|
||||
monkeypatch.setattr(aethergauge.rl, "draw_rectangle_rounded_lines_ex", lambda *args: keylines.append(args))
|
||||
|
||||
rect = aethergauge.rl.Rectangle(10, 20, 176, 260)
|
||||
layout = _aether_layout(rect)
|
||||
_draw_aether_card(layout, 0.5)
|
||||
|
||||
assert not triangles
|
||||
assert len(fills) == 1
|
||||
assert len(keylines) == 1
|
||||
|
||||
card, roundness, segments, fill = fills[0]
|
||||
keyline_card, keyline_roundness, keyline_segments, border_width, border = keylines[0]
|
||||
assert card.x == rect.x
|
||||
assert card.y == rect.y
|
||||
assert card.width == rect.width
|
||||
assert card.height == rect.height
|
||||
assert keyline_card.x == card.x
|
||||
assert keyline_card.y == card.y
|
||||
assert keyline_card.width == card.width
|
||||
assert keyline_card.height == card.height
|
||||
assert roundness == keyline_roundness == widget_style.CONTROL_ROUNDNESS
|
||||
assert segments == keyline_segments == widget_style.CONTROL_SEGMENTS
|
||||
assert border_width == widget_style.CONTROL_BORDER_WIDTH
|
||||
assert fill.a == int(aethergauge.COLOR_AETHER_CARD.a * 0.5)
|
||||
assert (border.r, border.g, border.b, border.a) == (
|
||||
widget_style.CONTROL_BORDER.r,
|
||||
widget_style.CONTROL_BORDER.g,
|
||||
widget_style.CONTROL_BORDER.b,
|
||||
int(widget_style.CONTROL_BORDER.a * 0.5),
|
||||
)
|
||||
|
||||
|
||||
def test_aether_layout_keeps_road_and_cradle_inside_one_card():
|
||||
layout = _aether_layout(aethergauge.rl.Rectangle(10, 20, 176, 260))
|
||||
card_right = layout.card.x + layout.card.width
|
||||
card_bottom = layout.card.y + layout.card.height
|
||||
inner_border = widget_style.CONTROL_BORDER_WIDTH / 2.0
|
||||
|
||||
for viewport in (layout.road, layout.cradle):
|
||||
assert layout.card.x <= viewport.x
|
||||
assert viewport.x + viewport.width <= card_right
|
||||
assert layout.card.y <= viewport.y
|
||||
assert viewport.y + viewport.height <= card_bottom
|
||||
assert layout.road.x >= layout.card.x + inner_border
|
||||
assert layout.road.y >= layout.card.y + inner_border
|
||||
assert layout.cradle.y + layout.cradle.height <= card_bottom - inner_border
|
||||
assert layout.road.y + layout.road.height < layout.cradle.y
|
||||
|
||||
|
||||
def _fake_text_size(_, text, size):
|
||||
return aethergauge.rl.Vector2(max(1, len(text) * size * 0.5), size)
|
||||
|
||||
|
||||
def _assert_text_calls_fit_card(text_calls, card):
|
||||
card_right = card.x + card.width
|
||||
card_bottom = card.y + card.height
|
||||
for text, pos, size in text_calls:
|
||||
measured = _fake_text_size(None, text, size)
|
||||
assert card.x <= pos.x
|
||||
assert pos.x + measured.x <= card_right
|
||||
assert card.y <= pos.y
|
||||
assert pos.y + measured.y <= card_bottom
|
||||
|
||||
|
||||
@pytest.mark.parametrize("data", [
|
||||
AetherGaugeData(
|
||||
text="120", unit="km/h", color=aethergauge.COLOR_CEM_SPEED,
|
||||
indicator_type=IndicatorType.ROAD_CURVE, reduction_text="-20", is_numeric=True,
|
||||
),
|
||||
AetherGaugeData(
|
||||
text="999", unit="ft", color=aethergauge.COLOR_FORCE_STOP,
|
||||
indicator_type=IndicatorType.STOP_LIGHT, is_numeric=True,
|
||||
),
|
||||
AetherGaugeData(
|
||||
text="STOPPED", color=aethergauge.COLOR_LEAD_STOPPED,
|
||||
indicator_type=IndicatorType.LEAD,
|
||||
),
|
||||
])
|
||||
def test_aether_cradle_fits_text_within_card(monkeypatch, data):
|
||||
text_calls = []
|
||||
monkeypatch.setattr(aethergauge, "measure_text_cached", _fake_text_size)
|
||||
monkeypatch.setattr(
|
||||
aethergauge.rl, "draw_text_ex",
|
||||
lambda _, text, pos, size, __, ___: text_calls.append((text, pos, size)),
|
||||
)
|
||||
monkeypatch.setattr(aethergauge.rl, "draw_line_ex", lambda *args: None)
|
||||
|
||||
layout = _aether_layout(aethergauge.rl.Rectangle(10, 20, 176, 260))
|
||||
AetherGauge()._draw_mini_cradle(layout.cradle, data, object(), object())
|
||||
|
||||
_assert_text_calls_fit_card(text_calls, layout.card)
|
||||
|
||||
|
||||
def test_aether_road_render_scissors_the_animated_viewport(monkeypatch):
|
||||
scissor_calls = []
|
||||
events = []
|
||||
monkeypatch.setattr(aethergauge, "measure_text_cached", _fake_text_size)
|
||||
monkeypatch.setattr(aethergauge.rl, "begin_scissor_mode", lambda *args: scissor_calls.append(args))
|
||||
monkeypatch.setattr(aethergauge.rl, "end_scissor_mode", lambda: events.append("end_scissor"))
|
||||
monkeypatch.setattr(aethergauge.rl, "draw_rectangle_rounded", lambda *args: None)
|
||||
monkeypatch.setattr(aethergauge.rl, "draw_rectangle_rounded_lines_ex", lambda *args: None)
|
||||
monkeypatch.setattr(aethergauge.rl, "draw_triangle", lambda *args: None)
|
||||
monkeypatch.setattr(aethergauge.rl, "draw_line_ex", lambda *args: None)
|
||||
monkeypatch.setattr(aethergauge.rl, "draw_text_ex", lambda *args: events.append("text"))
|
||||
monkeypatch.setattr(aethergauge.rl, "get_frame_time", lambda: 0.0)
|
||||
|
||||
rect = aethergauge.rl.Rectangle(10, 20, 176, 260)
|
||||
layout = _aether_layout(rect)
|
||||
data = AetherGaugeData(
|
||||
text="23", unit="mph", color=aethergauge.COLOR_CEM_SPEED,
|
||||
indicator_type=IndicatorType.ROAD_CURVE, indicator_value=0.005, is_numeric=True,
|
||||
)
|
||||
AetherGauge()._render_unified_road(rect, 98.0, data, object(), object())
|
||||
|
||||
assert scissor_calls == [(
|
||||
int(layout.road.x), int(layout.road.y), int(layout.road.width), int(layout.road.height),
|
||||
)]
|
||||
assert events.index("end_scissor") < events.index("text")
|
||||
|
||||
|
||||
def test_state_labels_make_urgent_and_lead_states_explicit():
|
||||
assert _state_label(AetherGaugeData(text="", indicator_type=IndicatorType.FORCE_STOP)) == "STOP"
|
||||
assert _state_label(AetherGaugeData(text="", indicator_type=IndicatorType.STOP_LIGHT)) == "RED LIGHT"
|
||||
assert _state_label(AetherGaugeData(text="", indicator_type=IndicatorType.LEAD)) == "LEAD"
|
||||
assert _state_label(AetherGaugeData(text="", indicator_type=IndicatorType.ROAD_CURVE)) == ""
|
||||
|
||||
Reference in New Issue
Block a user