mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-10 16:23:44 +08:00
Fit RoadScore panel to comma four and preserve gesture visibility
This commit is contained in:
+4
-2
@@ -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.
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user