Place RoadScore badge between native speed groups

This commit is contained in:
firestar5683
2026-09-20 10:35:01 -07:00
parent 0dec20423e
commit 99bc1e75ea
2 changed files with 59 additions and 17 deletions
+29 -13
View File
@@ -142,11 +142,14 @@ class EventPresentation:
def hud_bounds(screen_width, screen_height):
"""Accessory slot below DM/speed, left of the native speed-limit sign."""
width = min(286, screen_width - 64 - 144 - 16)
if width < 230 or screen_height < 224:
"""Stable top-middle slot between native MAX speed and the speed-limit sign."""
# Mici's MAX speed group occupies the leftmost 162px, not the centre.
# Reserve the 64px sidebar and the sign's 144px right-hand footprint.
left, right = 170, screen_width - 64 - 144 - 10
width = min(150, right - left)
if width < 148 or screen_height < 224:
return None
return (16, 88, width, 60)
return (left + (right - left - width) / 2, 10, width, 132)
def startup_bounds(screen_width, screen_height, footer_right=188):
@@ -168,7 +171,7 @@ def draw_panel(rl, font, state, screen_width, screen_height, emphasis_font=None,
'DEGRADED': (255, 197, 112), 'PREPARING': (196, 204, 214)}[view['activity']]), 255)
muted = rl.Color(235, 239, 243, 255)
section = view['section'].removeprefix('INTENT: ').split(' > ')[0].title()
if section in ('Archived Score', 'Waiting For Score'):
if section in ('Archived Score', 'Waiting For Score', 'Prepared Prism'):
section = ''
identity = 'Stored' if view['stored'] else view['profile']
subtitle = identity + (' / ' + section if section else '')
@@ -185,21 +188,34 @@ def draw_panel(rl, font, state, screen_width, screen_height, emphasis_font=None,
elif view['activity'] == 'PREPARING':
subtitle = 'Preparing music' if identity == 'Preparing' else identity + ' / Preparing'
# Match the native steering wheel's 50px identity, with fixed text anchors.
rl.draw_circle(int(x + 25), int(y + 29), 25, rl.Color(0, 0, 0, 150))
icon_x, icon_y = (x, y + 4) if startup else (x + (width - 50) / 2, y)
rl.draw_circle(int(icon_x + 25), int(icon_y + 25), 25, rl.Color(0, 0, 0, 150))
icon_color = accent if view['activity'] == 'DEGRADED' else rl.WHITE
rl.draw_circle(int(x + 14), int(y + 38), 5, icon_color)
rl.draw_circle(int(x + 33), int(y + 34), 5, icon_color)
rl.draw_circle(int(icon_x + 14), int(icon_y + 34), 5, icon_color)
rl.draw_circle(int(icon_x + 33), int(icon_y + 30), 5, icon_color)
for left, top, w, h in ((17, 14, 3, 24), (36, 10, 3, 24), (17, 10, 22, 4)):
rl.draw_rectangle_rounded(rl.Rectangle(x + left, y + top, w, h), .2, 4, icon_color)
rl.draw_rectangle_rounded(rl.Rectangle(icon_x + left, icon_y + top - 4, w, h), .2, 4, icon_color)
def text(label, top, size, face, tint, available, left=64):
label = fit_text(label, available, lambda value: rl.measure_text_ex(face, value, size, 0).x)
# Native HUD text uses local shadows; avoid an opaque rectangle over the road.
for dx, dy in ((-1, 0), (1, 0), (0, -1), (0, 1), (1, 2)):
rl.draw_text_ex(face, label, rl.Vector2(x + left + dx, y + top + dy), size, 0, rl.Color(0, 0, 0, 210))
rl.draw_text_ex(face, label, rl.Vector2(x + left, y + top), size, 0, tint)
text(title, 5, 20, title_font, accent if view['activity'] == 'DEGRADED' else rl.WHITE, 118)
reserve = '' if view['stored'] or view['buffered'] is None else f"{int(view['buffered'])}s buffer"
if reserve:
text(reserve, 11, 12, font, accent, width - 190, left=190)
text(subtitle, 33, 16, font, muted, width - 64)
if startup:
text(title, 5, 20, title_font, accent if view['activity'] == 'DEGRADED' else rl.WHITE, 118)
if reserve:
text(reserve, 11, 12, font, accent, width - 190, left=190)
text(subtitle, 33, 16, font, muted, width - 64)
else:
if view['event_state'] == 'active':
subtitle = subtitle.replace('Simulated ', 'Demo: ')
def centered(label, top, size, face, tint):
label = fit_text(label, width, lambda value: rl.measure_text_ex(face, value, size, 0).x)
left = (width - rl.measure_text_ex(face, label, size, 0).x) / 2
text(label, top, size, face, tint, width, left=left)
centered(title, 56, 20, title_font, accent if view['activity'] == 'DEGRADED' else rl.WHITE)
centered(subtitle, 82, 16, font, muted)
if reserve:
centered(reserve, 108, 12, font, accent)
return view
+30 -4
View File
@@ -1,10 +1,11 @@
"""Presentation contracts only; these tests never import the runtime or device tools."""
import sys
import unittest
from types import SimpleNamespace as NS
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'prototype'))
from overlay_view import display_text, fit_text, overlay_view, hud_bounds, startup_bounds, EventPresentation
from overlay_view import display_text, fit_text, overlay_view, hud_bounds, startup_bounds, EventPresentation, draw_panel
class OverlayTests(unittest.TestCase):
@@ -72,12 +73,13 @@ class OverlayTests(unittest.TestCase):
self.assertEqual(startup_bounds(536, 240, 256), (294, 172, 230, 60))
self.assertIsNone(startup_bounds(536, 240, 306))
self.assertIsNone(startup_bounds(320, 240))
self.assertEqual(hud_bounds(536, 240), (16, 88, 286, 60))
self.assertEqual(hud_bounds(536, 240), (170, 10, 148, 132))
def test_native_slot_clears_speed_sign_driver_and_steering(self):
x, y, width, height = hud_bounds(536, 240)
# Measured native screenshot + native widget dimensions, with safety margins.
occupied = [(8, 4, 72, 72), (160, 0, 158, 76), (328, 16, 120, 144),
# Native MAX is left aligned and extends through y=145, including its fade.
# The regression was caused by assuming this large group was centred.
occupied = [(0, 0, 162, 145), (328, 16, 120, 144),
(0, 160, 80, 80), (472, 0, 64, 240)]
for ox, oy, ow, oh in occupied:
self.assertTrue(x + width <= ox or ox + ow <= x or y + height <= oy or oy + oh <= y)
@@ -85,6 +87,30 @@ class OverlayTests(unittest.TestCase):
self.assertLessEqual(y + height, 240)
self.assertIsNone(hud_bounds(320, 240))
def test_stacked_badge_keeps_native_sized_icon_and_labels_inside_slot(self):
class Canvas:
WHITE = (255, 255, 255, 255)
Color = staticmethod(lambda *value: value)
Vector2 = staticmethod(lambda x,y: NS(x=x,y=y))
Rectangle = staticmethod(lambda x,y,w,h: (x,y,w,h))
def __init__(self): self.texts=[];self.circles=[]
def measure_text_ex(self,font,text,size,spacing): return NS(x=len(text)*size*.56)
def draw_text_ex(self,font,text,pos,size,spacing,color): self.texts.append((text,pos,size))
def draw_circle(self,x,y,radius,color): self.circles.append((x,y,radius))
def draw_rectangle_rounded(self,*args): pass
canvas=Canvas()
state=dict(readiness='READY',compute='prepared-core',profile='prism',section='PREPARED PRISM',buffered=296)
draw_panel(canvas,None,state,536,240)
x,y,w,h=hud_bounds(536,240)
self.assertEqual(canvas.circles[0][2],25)
self.assertTrue(any(text=='RoadScore' and size==20 for text,_,size in canvas.texts))
self.assertTrue(any(text=='Prism' and size==16 for text,_,size in canvas.texts))
for text,pos,size in canvas.texts:
self.assertGreaterEqual(pos.x,x-1)
self.assertLessEqual(pos.x+canvas.measure_text_ex(None,text,size,0).x,x+w+1)
self.assertGreaterEqual(pos.y,y)
self.assertLessEqual(pos.y+size,y+h)
def test_recent_linger_is_explicit_and_new_active_cue_is_immediate(self):
presenter = EventPresentation()
active = overlay_view(dict(gesture_active=['turn_signal'], readiness='READY'))