From 99bc1e75ea6459e3e75953cee487b31223529fd9 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Sun, 20 Sep 2026 10:35:01 -0700 Subject: [PATCH] Place RoadScore badge between native speed groups --- roadscore/prototype/overlay_view.py | 42 +++++++++++++++++++--------- roadscore/tests/test_overlay_view.py | 34 +++++++++++++++++++--- 2 files changed, 59 insertions(+), 17 deletions(-) diff --git a/roadscore/prototype/overlay_view.py b/roadscore/prototype/overlay_view.py index 051e425c44..f7012ab614 100644 --- a/roadscore/prototype/overlay_view.py +++ b/roadscore/prototype/overlay_view.py @@ -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 diff --git a/roadscore/tests/test_overlay_view.py b/roadscore/tests/test_overlay_view.py index 6fcfbbb6b5..09fc13bd6b 100644 --- a/roadscore/tests/test_overlay_view.py +++ b/roadscore/tests/test_overlay_view.py @@ -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'))