From f1b79537b80b053a7abd83e59e395ef547cf6726 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Fri, 18 Sep 2026 22:44:13 -0700 Subject: [PATCH] Fit RoadScore panel to comma four and preserve gesture visibility --- roadscore/DEMO.md | 6 +++-- roadscore/prototype/normal_onroad.py | 4 ++- roadscore/prototype/overlay_view.py | 40 +++++++++++++++------------- roadscore/tests/test_overlay_view.py | 6 +++++ roadscore/tools/preview_overlay.py | 12 ++++++--- 5 files changed, 43 insertions(+), 25 deletions(-) diff --git a/roadscore/DEMO.md b/roadscore/DEMO.md index e8e74874c7..5b77648301 100644 --- a/roadscore/DEMO.md +++ b/roadscore/DEMO.md @@ -6,7 +6,7 @@ The conductor receives only road messages already delivered by replay. It reques ## Reading the overlay -The compact lower-left panel leaves the existing camera, lane and path display in place. Its rows show RoadScore's activity, profile, section, compute backend and buffered music. +The lower-left panel is 94–112 logical pixels tall on the comma four's 536 × 240 canvas, with width limited to 55% of the screen. The existing camera, lane and path display stays in place. Its rows show RoadScore's activity, profile, section, compute backend and buffered music. | Display | Meaning | | --- | --- | @@ -38,6 +38,8 @@ python3 -m unittest discover -s roadscore/tests -v python3 roadscore/tools/preview_overlay.py --output roadscore/results/ui-polish/overlay-states.png ``` -The preview renders six synthetic states with the repository's Inter bitmap font atlas: preparation, ready, generating, accepted-music hold, failed composer and stored score. It uses CPU image drawing and does not open a window, contact a device, start a worker, run replay or open an audio stream. It is a layout review, not a native-rendering or hardware-validation claim. The output stays in ignored `results/`. +The preview renders six full 536 × 240 synthetic canvases with the repository's Inter bitmap font atlas: preparation, ready, generating, accepted-music hold, failed composer and stored score. It uses CPU image drawing and does not open a window, contact a device, start a worker, run replay or open an audio stream. It is a layout review, not a native-rendering or hardware-validation claim. The output stays in ignored `results/`. + +Generation timing and road gestures have separate space in the footer, so an active job cannot hide the event. The startup label uses the selected Prism/Aurora profile; stored playback is labeled as such from preparation onward. The active native overlay is `prototype/overlay.py`; the pure presentation helper is `prototype/overlay_view.py`. `prototype/index.html` and `prototype/native_display.py` are older excerpt-specific bench interfaces, not the normal onroad demo. The historical style selector is not an ACE profile selector. Keep those older entrypoints distinct when presenting the current demo. diff --git a/roadscore/prototype/normal_onroad.py b/roadscore/prototype/normal_onroad.py index 880bece018..f538e73d76 100644 --- a/roadscore/prototype/normal_onroad.py +++ b/roadscore/prototype/normal_onroad.py @@ -27,7 +27,9 @@ env['ROADSCORE_COMPOSER']=a.composer env['ROADSCORE_ACE_PROFILE']=a.profile env['ROADSCORE_OVERLAY_CAPTURE']=str(out/'overlay.png') env['ROADSCORE_STATUS_FILE']=str(out/'roadscore_status.json') -(out/'roadscore_status.json').write_text(json.dumps({'readiness':'Preparing','style':settings.style})) +initial_display={'readiness':'Preparing','composer':a.composer,'profile':a.profile if a.composer=='ace' else None,'style':a.profile.title() if a.composer=='ace' else settings.style} +if a.replay:initial_display.update(style='Stored score',section='ARCHIVED SCORE',compute='none') +(out/'roadscore_status.json').write_text(json.dumps(initial_display)) env['ROADSCORE_OVERLAY']='1' if settings.overlay else '0' env['ROADSCORE_FORCE_MUTE']='0' if a.audible else '1' env['ROADSCORE_ORIGIN_FILE']=str(out/'replay_origin.json') diff --git a/roadscore/prototype/overlay_view.py b/roadscore/prototype/overlay_view.py index 331fc5121d..8651596449 100644 --- a/roadscore/prototype/overlay_view.py +++ b/roadscore/prototype/overlay_view.py @@ -40,7 +40,7 @@ def overlay_view(state): elif degraded: note = 'Quality check / reserve in use' elif state.get('job_inflight'): - note = 'New passage' + (f' / {elapsed:.1f}s elapsed' if elapsed is not None else '') + note = f'Job {elapsed:.1f}s elapsed' if elapsed is not None else 'New passage in progress' event = '' if state.get('turn_signal_music'): event = 'Signal percussion' @@ -50,10 +50,8 @@ def overlay_view(state): event = 'Queued ' + display_text(state['gesture_queued'][0].get('kind')).replace('_', ' ') elif (lead := seconds(state.get('lead'))) is not None and lead > 0: event = f'Curve in {lead:.1f}s' - if event: - note = f'{note} / {event}' if note else event return dict(profile=display_text(profile), section=section, backend=backend, activity=activity, - buffered=seconds(state.get('buffered')), note=note, + buffered=seconds(state.get('buffered')), note=note, event=event, ready=readiness == 'READY' and not degraded, stored=stored) @@ -68,8 +66,8 @@ def fit_text(text, max_width, measure): def draw_panel(rl, font, state, screen_width, screen_height): view = overlay_view(state) - width = min(340, screen_width - 24) - height = 146 if view['note'] else 126 + width = min(320, screen_width * .55, screen_width - 24) + height = 112 if view['note'] or view['event'] else 94 x, y = 12, max(12, screen_height - height - 12) color = {'READY': (123, 229, 193), 'GENERATING': (131, 192, 255), 'DEGRADED': (255, 197, 112), 'PREPARING': (180, 190, 204)}[view['activity']] @@ -86,20 +84,26 @@ def draw_panel(rl, font, state, screen_width, screen_height): rl.draw_rectangle_rounded(rl.Rectangle(x + width - badge_width - 10, y + 10, badge_width, 22), .4, 8, rl.Color(*color, 28)) text(view['activity'], width - badge_width - 2, 15, 11, accent) text('RoadScore', 12, 12, 17, available=width - badge_width - 34) - text(view['profile'], 12, 39, 17) - text(view['section'], 12, 62, 12, muted) + text(view['profile'].upper(), 12, 35, 15) + text(view['section'], 12, 55, 11, muted) + buffer = view['buffered'] + label = 'BUFFER ' + ('--' if buffer is None else f'{buffer:.0f}s') + label_width = rl.measure_text_ex(font, label, 11, 0).x + buffer_left = width - label_width - 12 + text(label, buffer_left, 75, 11, accent) left, right = view['backend'] - text(left, 12, 83, 11, muted) + text(left, 12, 75, 11, muted, available=buffer_left - 24) if right: offset = 12 + rl.measure_text_ex(font, left, 11, 0).x # A geometric middle dot avoids missing-glyph boxes in the native font. - rl.draw_circle(int(x + offset + 7), int(y + 89), 1.5, muted) - text(right, offset + 15, 83, 11, muted) - buffer = view['buffered'] - text('BUFFER', 12, 105, 11, muted) - label = '--' if buffer is None else f'{buffer:.0f}s' - label_width = rl.measure_text_ex(font, label, 13, 0).x - text(label, width - label_width - 12, 103, 13, accent) - if view['note']: - text(view['note'], 12, 126, 11, accent) + rl.draw_circle(int(x + offset + 7), int(y + 81), 1.5, muted) + text(right, offset + 15, 75, 11, muted, available=buffer_left - offset - 27) + if view['note'] and view['event']: + # Give road gestures their own space so job timing cannot push them offscreen. + event_width = min((width - 36) / 2, rl.measure_text_ex(font, view['event'], 10, 0).x) + event_left = width - event_width - 12 + text(view['note'], 12, 95, 10, accent, available=event_left - 24) + text(view['event'], event_left, 95, 10, muted, available=event_width) + elif view['note'] or view['event']: + text(view['note'] or view['event'], 12, 95, 10, accent) return view diff --git a/roadscore/tests/test_overlay_view.py b/roadscore/tests/test_overlay_view.py index 67f6187299..f1049be3b0 100644 --- a/roadscore/tests/test_overlay_view.py +++ b/roadscore/tests/test_overlay_view.py @@ -22,6 +22,12 @@ class OverlayTests(unittest.TestCase): self.assertTrue(view['ready']) self.assertIn('12.3s', view['note']) + def test_gesture_remains_separate_from_generation_timing(self): + view = overlay_view(dict(readiness='READY', job_inflight=True, generation_elapsed_seconds=123.4, + turn_signal_music=True)) + self.assertEqual(view['event'], 'Signal percussion') + self.assertIn('123.4s', view['note']) + def test_missing_buffer_is_not_zero(self): for value in (None, float('nan'), float('inf'), '12', True): self.assertIsNone(overlay_view({'buffered': value})['buffered']) diff --git a/roadscore/tools/preview_overlay.py b/roadscore/tools/preview_overlay.py index 3a55d92c24..c9d76bf4a4 100644 --- a/roadscore/tools/preview_overlay.py +++ b/roadscore/tools/preview_overlay.py @@ -65,11 +65,15 @@ def main(): dict(base, readiness='DEGRADED', holding_accepted_music=True, job_inflight=True, buffered=18), dict(base, worker_failed=True, buffered=0), dict(readiness='READY', style='Stored score', section='ARCHIVED SCORE', compute='none', buffered=92)] - board = Image.new('RGBA', (728, 564), '#080e16') + # Match the comma four UI's logical canvas, not a cropped panel. + width, height = 536, 240 + board = Image.new('RGBA', (width * 2, height * 3), '#080e16') for index, state in enumerate(cases): - panel = Image.new('RGBA', (364, 188), '#080e16') - draw_panel(Canvas(panel, ROOT / 'selfdrive/assets/fonts/Inter-Medium.fnt'), None, state, 364, 176) - board.paste(panel, ((index % 2) * 364, (index // 2) * 188)) + panel = Image.new('RGBA', (width, height), '#080e16') + canvas = Canvas(panel, ROOT / 'selfdrive/assets/fonts/Inter-Medium.fnt') + canvas.draw_text_ex(None, 'SYNTHETIC UI / 536 x 240', canvas.Vector2(12, 12), 11, 0, (120, 135, 150, 255)) + draw_panel(canvas, None, state, width, height) + board.paste(panel, ((index % 2) * width, (index // 2) * height)) args.output.parent.mkdir(parents=True, exist_ok=True) board.convert('RGB').save(args.output) print(args.output)