From 6eba716ff80098356d242676bee8d3bc514fef86 Mon Sep 17 00:00:00 2001 From: firestarsdog <229254897+firestarsdog@users.noreply.github.com> Date: Sat, 8 Aug 2026 04:47:00 -0400 Subject: [PATCH] Biscuit tweaks --- .../ui/onroad/starpilot/slc_speed_limit.py | 259 +++++++++++------- selfdrive/ui/onroad/starpilot/widget_style.py | 12 +- 2 files changed, 166 insertions(+), 105 deletions(-) diff --git a/selfdrive/ui/onroad/starpilot/slc_speed_limit.py b/selfdrive/ui/onroad/starpilot/slc_speed_limit.py index eb61348d6..382b27d4d 100644 --- a/selfdrive/ui/onroad/starpilot/slc_speed_limit.py +++ b/selfdrive/ui/onroad/starpilot/slc_speed_limit.py @@ -3,12 +3,12 @@ from typing import Optional import pyray as rl from openpilot.common.constants import CV -from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.selfdrive.ui.onroad.starpilot.widget_style import ( - CONTROL_BG, CONTROL_BORDER_WIDTH, SLC_HEIGHT, + CONTROL_BG, CONTROL_BORDER, CONTROL_BORDER_WIDTH, CONTROL_ROUNDNESS, CONTROL_SEGMENTS, SLC_HEIGHT, draw_control_card, roundness_for, ) @@ -24,19 +24,21 @@ RED_RING_WIDTH = 20 # Pending sign blink cadence — 1s period, 50% duty cycle. PENDING_BLINK_MS = 500 -# Source display metadata: title, abbreviation, raw-value key (display order). +# Source display metadata: source name, main label, value key, bubble label, icon. SOURCE_DEFS = [ - ("Dashboard", "Dash", "dashboard_sl", "Dashboard", "dashboard"), - ("Map Data", "Maps", "map_sl", "Map", "map"), - ("Vision", "Vision", "vision_sl", "Camera", "camera"), - ("Mapbox", "Mapbox", "mapbox_sl", "Mapbox", "map"), - ("Upcoming", "Next", "next_sl", "Navigation", "navigation"), + ("Dashboard", "Dash", "dashboard_sl", "Dashboard", "dashboard"), + ("Map Data", "MAP", "map_sl", "Map Data", "map"), + ("Vision", "VISION", "vision_sl", "Vision", "camera"), + ("Mapbox", "MBOX", "mapbox_sl", "Mapbox", "map"), + ("Upcoming", "NEXT", "next_sl", "Next", "next"), ] # Fonts FONT_LABEL = 30 -FONT_SPEED = 78 -FONT_OFFSET = 30 +FONT_SOURCE = 40 # Set Speed MAX label size. +FONT_SPEED = 90 # Set Speed value size. +FONT_OFFSET = 29 # Compact offset text. +OFFSET_CHIP_SEGMENTS = 8 # Capsule curve segments. FONT_EU_LARGE = 70 FONT_EU_SMALL = 60 FONT_EU_OFFSET = 40 @@ -202,6 +204,17 @@ def _active_source_label(state: dict) -> str: return _ACTIVE_SOURCE_LABELS.get(source, source.upper()) +def _source_label_color(alpha: int) -> rl.Color: + """Match Set Speed's MAX label color.""" + if ui_state.status == UIStatus.ENGAGED: + base = rl.Color(128, 216, 166, 255) + elif ui_state.status in (UIStatus.DISENGAGED, UIStatus.OVERRIDE): + base = rl.Color(145, 155, 149, 255) + else: + base = rl.Color(166, 166, 166, 255) + return _speed_limit_pulse_color(base, alpha) + + # ── US MUTCD Sign ───────────────────────────────────────────────────── def _draw_offset_chip(rect: rl.Rectangle, offset_str: str, alpha: int) -> None: @@ -209,18 +222,19 @@ def _draw_offset_chip(rect: rl.Rectangle, offset_str: str, alpha: int) -> None: font = _get_semi_bold() text_size = measure_text_cached(font, offset_str, FONT_OFFSET) chip_w = max(64.0, text_size.x + 24.0) - chip_h = 38.0 + chip_h = 36.0 chip_rect = rl.Rectangle( rect.x + (rect.width - chip_w) / 2, - rect.y + rect.height - chip_h - 14, + rect.y + rect.height - chip_h - 10, chip_w, chip_h, ) - chip_border = rl.Color(255, 255, 255, alpha) + chip_border = rl.Color(CONTROL_BORDER.r, CONTROL_BORDER.g, CONTROL_BORDER.b, + min(alpha, CONTROL_BORDER.a)) chip_fill = rl.Color(0, 0, 0, min(120, alpha)) roundness = roundness_for(chip_rect, 18) - rl.draw_rectangle_rounded(chip_rect, roundness, 12, chip_fill) - rl.draw_rectangle_rounded_lines_ex(chip_rect, roundness, 12, 2, chip_border) + rl.draw_rectangle_rounded(chip_rect, roundness, OFFSET_CHIP_SEGMENTS, chip_fill) + rl.draw_rectangle_rounded_lines_ex(chip_rect, roundness, OFFSET_CHIP_SEGMENTS, 2, chip_border) rl.draw_text_ex( font, offset_str, @@ -240,15 +254,16 @@ def _draw_us_sign(x: float, y: float, sign_width: float, sign_height: float, visible frame geometry with Set Speed. Border and text colors continue to use the existing Vision pulse and pending blink behavior. """ - # Pending: border blinks white <-> red. Active: border is white. + # Pending: blink white/red. Active: shared blue-grey. if pending: blink_on = int(rl.get_time() * 1000) % 1000 < PENDING_BLINK_MS base_border = rl.Color(255, 255, 255, alpha) if blink_on else rl.Color(201, 34, 49, alpha) else: - base_border = rl.Color(255, 255, 255, alpha) + base_border = rl.Color(CONTROL_BORDER.r, CONTROL_BORDER.g, CONTROL_BORDER.b, + min(alpha, CONTROL_BORDER.a)) # Compose the blink base with the active vision pulse (no-op outside window). - border_color = _speed_limit_pulse_color(base_border, alpha) + border_color = _speed_limit_pulse_color(base_border, base_border.a) # White value text reads on the translucent road background. text_color = _speed_limit_pulse_color(rl.Color(255, 255, 255, 255), alpha) @@ -271,21 +286,21 @@ def _draw_us_sign(x: float, y: float, sign_width: float, sign_height: float, rl.draw_text_ex(font_bold, speed_text, rl.Vector2(cx - speed_size.x / 2, y + 85), FONT_SPEED - 6, 0, text_color) elif show_offset: # Offset ON: source at the top, speed below it, and the offset in a chip. - source_size = measure_text_cached(font_semi, source_label, FONT_LABEL) - source_color = _speed_limit_pulse_color(_WHITE, alpha) - rl.draw_text_ex(font_semi, source_label, rl.Vector2(cx - source_size.x / 2, y + 20), FONT_LABEL, 0, source_color) + source_size = measure_text_cached(font_semi, source_label, FONT_SOURCE) + source_color = _source_label_color(alpha) + rl.draw_text_ex(font_semi, source_label, rl.Vector2(cx - source_size.x / 2, y + 8), FONT_SOURCE, 0, source_color) speed_size = measure_text_cached(font_bold, speed_text, FONT_SPEED) - rl.draw_text_ex(font_bold, speed_text, rl.Vector2(cx - speed_size.x / 2, y + 54), FONT_SPEED, 0, text_color) + rl.draw_text_ex(font_bold, speed_text, rl.Vector2(cx - speed_size.x / 2, y + 44), FONT_SPEED, 0, text_color) _draw_offset_chip(card_rect, offset_str, alpha) else: - # Offset OFF: source at the top, speed centered in the remaining space. - source_size = measure_text_cached(font_semi, source_label, FONT_LABEL) - source_color = _speed_limit_pulse_color(_WHITE, alpha) - rl.draw_text_ex(font_semi, source_label, rl.Vector2(cx - source_size.x / 2, y + 20), FONT_LABEL, 0, source_color) + # Offset OFF: match Set Speed typography. + source_size = measure_text_cached(font_semi, source_label, FONT_SOURCE) + source_color = _source_label_color(alpha) + rl.draw_text_ex(font_semi, source_label, rl.Vector2(cx - source_size.x / 2, y + 27), FONT_SOURCE, 0, source_color) speed_size = measure_text_cached(font_bold, speed_text, FONT_SPEED) - rl.draw_text_ex(font_bold, speed_text, rl.Vector2(cx - speed_size.x / 2, y + 78), FONT_SPEED, 0, text_color) + rl.draw_text_ex(font_bold, speed_text, rl.Vector2(cx - speed_size.x / 2, y + 77), FONT_SPEED, 0, text_color) # ── EU Vienna Sign ──────────────────────────────────────────────────── @@ -385,37 +400,24 @@ def _draw_sign(state: dict, rect: rl.Rectangle, *, pending: bool = False): # ── Sources Bubble (expandable overlay) ──────────────────────────────── -_SOURCE_PANEL_WIDTH = 300 -_SOURCE_PANEL_GAP = 28 -_SOURCE_PANEL_PAD_X = 18 +# Fixed width fits the longest source label and a three-digit speed at 26px. +_SOURCE_PANEL_WIDTH = 248 +_SOURCE_PANEL_GAP = 20 +_SOURCE_PANEL_PAD_X = 9 _SOURCE_PANEL_PAD_Y = 4 _SOURCE_PANEL_BG = rl.Color(0, 0, 0, 145) +_SOURCE_PANEL_BORDER = rl.Color(196, 205, 208, 72) _SOURCE_DIVIDER = rl.Color(196, 205, 208, 70) +_SOURCE_ACTIVE_BAR = rl.Color(CONTROL_BORDER.r, CONTROL_BORDER.g, CONTROL_BORDER.b, 230) _SOURCE_ICON_MUTED = rl.Color(196, 205, 208, 190) _SOURCE_LABEL = rl.Color(255, 255, 255, 215) -_SOURCE_FONT = 34 -_SOURCE_MIN_FONT = 22 -_SOURCE_TEXT_GAP = 12 -_SOURCE_ICON_SIZE = 30 - - -def _fit_sources_row(font, label: str, value_text: str, row_h: float, available_w: float): - font_size = min(_SOURCE_FONT, max(_SOURCE_MIN_FONT, int(row_h * 0.82))) - label_size = measure_text_cached(font, label, font_size) - value_size = measure_text_cached(font, value_text, font_size) - needed_w = label_size.x + value_size.x + _SOURCE_TEXT_GAP - - if needed_w > available_w: - font_size = max(_SOURCE_MIN_FONT, int(font_size * (available_w / needed_w))) - label_size = measure_text_cached(font, label, font_size) - value_size = measure_text_cached(font, value_text, font_size) - while label_size.x + value_size.x + _SOURCE_TEXT_GAP > available_w and font_size > _SOURCE_MIN_FONT: - font_size -= 1 - label_size = measure_text_cached(font, label, font_size) - value_size = measure_text_cached(font, value_text, font_size) - - fits = label_size.x + value_size.x + _SOURCE_TEXT_GAP <= available_w - return font_size, value_size, fits +_SOURCE_FONT = 26 +_SOURCE_ICON_TEXT_GAP = 8 +_SOURCE_ICON_SIZE = 28 +_SOURCE_ACTIVE_BAR_WIDTH = 5.0 +_SOURCE_ACTIVE_BAR_HEIGHT = 32.0 +_SOURCE_ACTIVE_BAR_X = 2.0 +_SOURCE_ACTIVE_BAR_ROW_INSET = 6.0 def _draw_source_icon(icon_key: str, x: float, y: float, size: float, color: rl.Color) -> None: @@ -425,58 +427,106 @@ def _draw_source_icon(icon_key: str, x: float, y: float, size: float, color: rl. stroke = max(2.0, size / 12.0) if icon_key == "map": + map_stroke = max(2.0, size * 0.075) left = x + size * 0.12 + fold_left = x + size * 0.37 + fold_right = x + size * 0.63 right = x + size * 0.88 - top = y + size * 0.18 - bottom = y + size * 0.82 - fold = size * 0.25 - rl.draw_line_ex(rl.Vector2(left, top), rl.Vector2(left, bottom), stroke, color) - rl.draw_line_ex(rl.Vector2(left, top), rl.Vector2(left + fold, top + size * 0.12), stroke, color) - rl.draw_line_ex(rl.Vector2(left + fold, top + size * 0.12), rl.Vector2(left + fold, bottom + size * 0.12), stroke, color) - rl.draw_line_ex(rl.Vector2(left + fold, top + size * 0.12), rl.Vector2(left + fold * 2.0, top), stroke, color) - rl.draw_line_ex(rl.Vector2(left + fold * 2.0, top), rl.Vector2(left + fold * 2.0, bottom), stroke, color) - rl.draw_line_ex(rl.Vector2(left + fold * 2.0, top), rl.Vector2(right, top + size * 0.12), stroke, color) - rl.draw_line_ex(rl.Vector2(right, top + size * 0.12), rl.Vector2(right, bottom + size * 0.12), stroke, color) + top = y + size * 0.20 + top_low = y + size * 0.27 + bottom = y + size * 0.80 + bottom_low = y + size * 0.73 + outline = [ + rl.Vector2(left, top), + rl.Vector2(fold_left, top_low), + rl.Vector2(fold_right, top), + rl.Vector2(right, top_low), + rl.Vector2(right, bottom), + rl.Vector2(fold_right, bottom_low), + rl.Vector2(fold_left, bottom), + rl.Vector2(left, bottom_low), + ] + for index, point in enumerate(outline): + rl.draw_line_ex(point, outline[(index + 1) % len(outline)], map_stroke, color) + for point in outline: + rl.draw_circle_v(point, map_stroke / 2, color) + rl.draw_line_ex(outline[1], outline[6], map_stroke, color) + rl.draw_line_ex(outline[2], outline[5], map_stroke, color) elif icon_key == "camera": - body = rl.Rectangle(x + size * 0.08, y + size * 0.28, size * 0.84, size * 0.54) - rl.draw_rectangle_rounded(body, 0.25, 8, color) - lens = rl.Vector2(cx, y + size * 0.55) - rl.draw_circle_v(lens, size * 0.17, _SOURCE_PANEL_BG) - rl.draw_circle_lines(int(lens.x), int(lens.y), size * 0.17, color) + body = rl.Rectangle(x + size * 0.09, y + size * 0.29, size * 0.82, size * 0.52) + rl.draw_rectangle_rounded(body, 0.20, 8, color) + lens = rl.Vector2(cx, y + size * 0.54) + lens_outer = size * 0.17 + rl.draw_circle_v(lens, lens_outer, _SOURCE_PANEL_BG) + rl.draw_ring(lens, size * 0.105, lens_outer, 0, 360, max(24, int(size * 0.25)), color) rl.draw_rectangle_rounded( - rl.Rectangle(x + size * 0.28, y + size * 0.16, size * 0.24, size * 0.17), - 0.25, 6, color, + rl.Rectangle(x + size * 0.30, y + size * 0.18, size * 0.23, size * 0.15), + 0.18, 8, color, ) + elif icon_key == "next": + arrow_stroke = max(2.0, size * 0.08) + arrow_tip = rl.Vector2(x + size * 0.88, cy) + rl.draw_line_ex(rl.Vector2(x + size * 0.10, cy), arrow_tip, arrow_stroke, color) + for endpoint in ( + rl.Vector2(x + size * 0.60, y + size * 0.18), + rl.Vector2(x + size * 0.60, y + size * 0.82), + ): + rl.draw_line_ex(arrow_tip, endpoint, arrow_stroke, color) + rl.draw_circle_v(arrow_tip, arrow_stroke / 2, color) elif icon_key == "navigation": - head = rl.Vector2(x + size * 0.82, y + size * 0.16) - left = rl.Vector2(x + size * 0.16, y + size * 0.78) - right = rl.Vector2(x + size * 0.64, y + size * 0.84) - rl.draw_triangle(left, head, right, color) - rl.draw_line_ex(left, head, stroke, color) - rl.draw_line_ex(head, right, stroke, color) + pin_center = rl.Vector2(cx, y + size * 0.36) + pin_radius = size * 0.22 + rl.draw_circle_v(pin_center, pin_radius, color) + rl.draw_triangle( + rl.Vector2(cx - pin_radius * 0.82, y + size * 0.40), + rl.Vector2(cx + pin_radius * 0.82, y + size * 0.40), + rl.Vector2(cx, y + size * 0.86), + color, + ) + rl.draw_circle_v(pin_center, size * 0.09, _SOURCE_PANEL_BG) else: # Dashboard / fallback - rl.draw_ring(rl.Vector2(cx, cy + size * 0.10), size * 0.27, size * 0.34, 200, 340, 24, color) + dashboard_scale = 1.22 + pivot = rl.Vector2(cx, cy + size * 0.17) + inner_radius = size * 0.27 * dashboard_scale + outer_radius = size * 0.34 * dashboard_scale + ring_segments = max(24, int(size * 0.25)) + rl.draw_ring(pivot, inner_radius, outer_radius, 190, 350, ring_segments, color) + cap_radius = (outer_radius - inner_radius) / 2 + for angle in (190, 350): + radians = math.radians(angle) + rl.draw_circle_v( + rl.Vector2( + pivot.x + math.cos(radians) * (inner_radius + cap_radius), + pivot.y + math.sin(radians) * (inner_radius + cap_radius), + ), + cap_radius, + color, + ) + needle_angle = math.radians(-48) + needle_length = inner_radius + stroke * 0.15 rl.draw_line_ex( - rl.Vector2(cx, cy + size * 0.10), - rl.Vector2(cx + size * 0.18, cy - size * 0.12), + pivot, + rl.Vector2( + pivot.x + math.cos(needle_angle) * needle_length, + pivot.y + math.sin(needle_angle) * needle_length, + ), stroke, color, ) - rl.draw_circle_v(rl.Vector2(cx, cy + size * 0.10), stroke, color) + rl.draw_circle_v(pivot, max(2.0, size * 0.06 * dashboard_scale), color) def _draw_sources_bubble(state: dict, sign_rect: rl.Rectangle): """Draw the expanded source list attached to the SLC card.""" - font_bold = _get_bold() font_semi = _get_semi_bold() active_source = state['speed_limit_source'] rows = [] - for title, abbrev, value_key, panel_label, icon_key in SOURCE_DEFS: + for title, _abbrev, value_key, panel_label, icon_key in SOURCE_DEFS: value = state[value_key] - if value == 0: + if value == 0 and active_source != title: continue - rows.append((title, abbrev, panel_label, icon_key, value, active_source == title)) + rows.append((panel_label, icon_key, value, active_source == title)) if not rows: return @@ -487,15 +537,17 @@ def _draw_sources_bubble(state: dict, sign_rect: rl.Rectangle): _SOURCE_PANEL_WIDTH, sign_rect.height, ) - rl.draw_rectangle_rounded(panel_rect, roundness_for(panel_rect), 16, _SOURCE_PANEL_BG) + rl.draw_rectangle_rounded(panel_rect, CONTROL_ROUNDNESS, CONTROL_SEGMENTS, _SOURCE_PANEL_BG) + rl.draw_rectangle_rounded_lines_ex( + panel_rect, CONTROL_ROUNDNESS, CONTROL_SEGMENTS, 1, _SOURCE_PANEL_BORDER, + ) row_h = (panel_rect.height - 2 * _SOURCE_PANEL_PAD_Y) / len(rows) content_left = panel_rect.x + _SOURCE_PANEL_PAD_X content_right = panel_rect.x + panel_rect.width - _SOURCE_PANEL_PAD_X - label_left = content_left + _SOURCE_ICON_SIZE + _SOURCE_TEXT_GAP - available_w = content_right - label_left + label_left = content_left + _SOURCE_ICON_SIZE + _SOURCE_ICON_TEXT_GAP - for index, (title, abbrev, panel_label, icon_key, value, is_active) in enumerate(rows): + for index, (panel_label, icon_key, value, is_active) in enumerate(rows): row_y = panel_rect.y + _SOURCE_PANEL_PAD_Y + index * row_h if index: divider_y = row_y @@ -506,26 +558,33 @@ def _draw_sources_bubble(state: dict, sign_rect: rl.Rectangle): _SOURCE_DIVIDER, ) - text_font = font_bold if is_active else font_semi + text_font = font_semi label_text = panel_label - value_text = str(int(round(value))) - font_size, value_size, fits = _fit_sources_row( - text_font, label_text, value_text, row_h, available_w - ) - if not fits: - label_text = abbrev - font_size, value_size, _ = _fit_sources_row( - text_font, label_text, value_text, row_h, available_w - ) + value_text = "\u2013" if value <= 0 else str(int(round(value))) + font_size = _SOURCE_FONT + value_size = measure_text_cached(text_font, value_text, font_size) - baseline_y = row_y + (row_h - font_size) / 2 + baseline_y = row_y + (row_h - value_size.y) / 2 icon_y = row_y + (row_h - _SOURCE_ICON_SIZE) / 2 + if is_active: + active_bar_height = min( + _SOURCE_ACTIVE_BAR_HEIGHT, + max(10.0, row_h - 2 * _SOURCE_ACTIVE_BAR_ROW_INSET), + ) + active_bar_rect = rl.Rectangle( + panel_rect.x + _SOURCE_ACTIVE_BAR_X, + row_y + (row_h - active_bar_height) / 2, + _SOURCE_ACTIVE_BAR_WIDTH, + active_bar_height, + ) + rl.draw_rectangle_rounded(active_bar_rect, 0.5, 4, _SOURCE_ACTIVE_BAR) + icon_color = _WHITE if is_active else _SOURCE_ICON_MUTED _draw_source_icon(icon_key, content_left, icon_y, _SOURCE_ICON_SIZE, icon_color) label_pos = rl.Vector2(label_left, baseline_y) value_pos = rl.Vector2(content_right - value_size.x, baseline_y) - label_color = _SOURCE_LABEL + label_color = _WHITE if is_active else _SOURCE_LABEL value_color = _WHITE if is_active else _SOURCE_LABEL rl.draw_text_ex(text_font, label_text, label_pos, font_size, 0, label_color) rl.draw_text_ex(text_font, value_text, value_pos, font_size, 0, value_color) diff --git a/selfdrive/ui/onroad/starpilot/widget_style.py b/selfdrive/ui/onroad/starpilot/widget_style.py index ab963a3b9..9588b1aa5 100644 --- a/selfdrive/ui/onroad/starpilot/widget_style.py +++ b/selfdrive/ui/onroad/starpilot/widget_style.py @@ -7,10 +7,12 @@ import pyray as rl # here prevents the Set Speed and SLC implementations from drifting apart. CONTROL_WIDTH = 176 SET_SPEED_HEIGHT = 196 -SLC_HEIGHT = 216 -CONTROL_RADIUS = 16 -CONTROL_SEGMENTS = 16 -CONTROL_BORDER_WIDTH = 3 +SLC_HEIGHT = SET_SPEED_HEIGHT +# Stable Raylib shape settings for scaled rendering. +CONTROL_RADIUS = 31 +CONTROL_ROUNDNESS = 0.35 +CONTROL_SEGMENTS = 10 +CONTROL_BORDER_WIDTH = 6 CONTROL_BG = rl.Color(0, 0, 0, 166) CONTROL_BORDER = rl.Color(196, 205, 208, 180) # The layout manager has historically anchored the left controls at x + 146. @@ -27,6 +29,6 @@ def draw_control_card(rect: rl.Rectangle, *, fill: rl.Color = CONTROL_BG, border: rl.Color = CONTROL_BORDER, border_width: float = CONTROL_BORDER_WIDTH) -> None: """Draw the common translucent rounded card used by left-hand controls.""" - roundness = roundness_for(rect) + roundness = CONTROL_ROUNDNESS rl.draw_rectangle_rounded(rect, roundness, CONTROL_SEGMENTS, fill) rl.draw_rectangle_rounded_lines_ex(rect, roundness, CONTROL_SEGMENTS, border_width, border)