Fit RoadScore panel to comma four and preserve gesture visibility

This commit is contained in:
firestar5683
2026-09-18 22:44:13 -07:00
parent e8eae47837
commit f1b79537b8
5 changed files with 43 additions and 25 deletions
+4 -2
View File
@@ -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.
+3 -1
View File
@@ -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')
+22 -18
View File
@@ -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
+6
View File
@@ -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'])
+8 -4
View File
@@ -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)