Biscuit tweaks

This commit is contained in:
firestarsdog
2026-08-08 04:47:00 -04:00
parent 42d3cf42d8
commit 6eba716ff8
2 changed files with 166 additions and 105 deletions
+159 -100
View File
@@ -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)
@@ -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)