mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 05:23:41 +08:00
271 lines
14 KiB
Python
271 lines
14 KiB
Python
"""The latest limit, its attention animation and override mark on both screens."""
|
|
|
|
import time
|
|
from typing import cast
|
|
|
|
import pyray as rl
|
|
|
|
from openpilot.starpilot.ui.large_speed_geometry import LARGE_SIGN_SCALE, CONFIRMATION_SCALE, CONFIRMATION_PADDING
|
|
from openpilot.starpilot.ui.onroad_state import OnroadState, manual_slc_confirmation, speed_limit_bounds, slc_controls
|
|
from openpilot.starpilot.ui.presentation import BitmapFonts, FontRole, Profile
|
|
from openpilot.starpilot.ui.speed_limit_pulse import SpeedLimitPulse
|
|
from openpilot.starpilot.ui.speed_limit_shapes import SpeedLimitShapes
|
|
from openpilot.starpilot.ui.unified_speed_presentation import displayed_adjusted_limit_mps, resolve_unified_speed
|
|
|
|
|
|
INK = rl.Color(241, 246, 243, 255)
|
|
MUTED = rl.Color(190, 208, 202, 255)
|
|
SOURCE_LABELS = {'dashboard': 'DASH', 'map': 'OSM', 'vision': 'VISION', 'mapbox': 'NAV'}
|
|
|
|
|
|
def draw_text(fonts, text, role, size, x, y, color=INK):
|
|
if isinstance(color, (tuple, list)):
|
|
color = rl.Color(*color)
|
|
radius = 1.15 if fonts.profile == Profile.LARGE else 1.
|
|
contour = rl.Color(6, 14, 10, round(color.a * .90))
|
|
for dx, dy in ((-radius, 0), (radius, 0), (0, -radius), (0, radius)):
|
|
fonts.draw(text, role, size, x + dx, y + dy, contour)
|
|
fonts.draw(text, role, size, x, y + (1.5 if fonts.profile == Profile.LARGE else 1.2),
|
|
rl.Color(7, 16, 12, round(color.a * .32)))
|
|
fonts.draw(text, role, size, x, y, color)
|
|
|
|
|
|
def draw_line(profile, start, end, stroke, color):
|
|
radius = 1.4 if profile == Profile.LARGE else 1.
|
|
rl.draw_line_ex(start, end, stroke + 2 * radius, rl.Color(6, 14, 10, round(color.a * .87)))
|
|
rl.draw_line_ex(start, end, stroke, color)
|
|
|
|
|
|
def draw_ink(fonts, text, role, size, x, top, color=INK):
|
|
ink_top, ink_bottom = fonts.vertical_ink(text, role, size)
|
|
draw_text(fonts, text, role, size, x, top - ink_top, color)
|
|
return fonts.measure(text, role, size).width, ink_bottom - ink_top
|
|
|
|
|
|
def draw_caption(fonts, label, x, top, color=MUTED, *, unit=None, size=None, centered=False):
|
|
large = fonts.profile == Profile.LARGE
|
|
label_size = size or (28 if large else 24)
|
|
unit_size, gap = (28, 12) if large else (20, 8)
|
|
if centered:
|
|
width = fonts.measure(label, FontRole.MEDIUM, label_size).width
|
|
if unit:
|
|
width += gap + fonts.measure(unit, FontRole.NORMAL, unit_size).width
|
|
x -= width / 2
|
|
width, height = draw_ink(fonts, label, FontRole.MEDIUM, label_size, x, top, color)
|
|
if unit:
|
|
unit_top, _ = fonts.vertical_ink(unit, FontRole.NORMAL, unit_size)
|
|
# Exclude the p descender when aligning mph to the caption baseline.
|
|
_, baseline = fonts.vertical_ink('mh' if unit == 'mph' else unit, FontRole.NORMAL, unit_size)
|
|
unit_width, _ = draw_ink(fonts, unit, FontRole.NORMAL, unit_size, x + width + gap,
|
|
top + height - (baseline - unit_top), color)
|
|
width += gap + unit_width
|
|
return width, height
|
|
|
|
|
|
class SpeedLimitWidget:
|
|
def __init__(self, fonts: BitmapFonts, profile: Profile):
|
|
self.fonts = fonts
|
|
self.profile = profile
|
|
self.scale = LARGE_SIGN_SCALE if profile == Profile.LARGE else 1
|
|
self.text_scale = self.scale * Profile.COMPACT.font_scale / profile.font_scale
|
|
self.pulse = SpeedLimitPulse()
|
|
self.shapes = SpeedLimitShapes(self.scale, headless_context=getattr(fonts, '_headless_context', False))
|
|
|
|
def close(self):
|
|
self.shapes.close()
|
|
|
|
def render(self, state: OnroadState, *, defer_confirmation=False) -> None:
|
|
bounds = speed_limit_bounds(self.profile, state)
|
|
now = state.observed_ns / 1e9 if state.observed_ns else time.monotonic()
|
|
self.pulse.update(state, now, visible=bounds is not None)
|
|
if bounds is None:
|
|
return
|
|
if manual_slc_confirmation(self.profile, state):
|
|
if not defer_confirmation:
|
|
self.render_confirmation(state)
|
|
return
|
|
self._sign(state, bounds, now)
|
|
|
|
def render_confirmation(self, state: OnroadState, pressed=None):
|
|
bounds = speed_limit_bounds(self.profile, state)
|
|
if bounds is None or not manual_slc_confirmation(self.profile, state):
|
|
return
|
|
now = state.observed_ns / 1e9 if state.observed_ns else time.monotonic()
|
|
self.pulse.update(state, now)
|
|
scale = CONFIRMATION_SCALE[self.profile]
|
|
x, y, right, _ = bounds
|
|
width = (right - x) / scale
|
|
shown = resolve_unified_speed(state)
|
|
source = SOURCE_LABELS.get(shown.source, 'SPEED')
|
|
white = rl.Color(242, 246, 244, 255)
|
|
secondary = rl.Color(209, 222, 218, 255)
|
|
|
|
def text_size(size):
|
|
return size * scale * Profile.COMPACT.font_scale / self.profile.font_scale
|
|
|
|
def ink_width(text, size, role=FontRole.MEDIUM):
|
|
left, right = self.fonts.horizontal_ink(text, role, text_size(size))
|
|
return (right - left) / scale
|
|
|
|
def text(label, size, cx, cy, color=white, role=FontRole.MEDIUM):
|
|
size = text_size(size)
|
|
left, right = self.fonts.horizontal_ink(label, role, size)
|
|
top, bottom = self.fonts.vertical_ink(label, role, size)
|
|
self.fonts.draw(label, role, size, x + cx * scale - (left + right) / 2,
|
|
y + cy * scale - (top + bottom) / 2, color)
|
|
|
|
def rounded(left, top, w, h, color, radius=8):
|
|
rect = rl.Rectangle(x + left * scale, y + top * scale, w * scale, h * scale)
|
|
rl.draw_rectangle_rounded(rect, 2 * radius / min(w, h), 12, color)
|
|
|
|
if state.slc_prompt_open:
|
|
self.shapes.confirmation(bounds, opened=True)
|
|
target_size = 40
|
|
nw, uw = ink_width(shown.proposal_text, target_size, FontRole.BOLD), ink_width(shown.unit, 20)
|
|
context = source + (f' {shown.posted_text} {shown.offset_text}' if shown.offset_text else '')
|
|
context_size = 20 * min(1, (width - 24 - nw - 6 - uw - 16) / max(1, ink_width(context, 20)))
|
|
context_width = ink_width(context, context_size)
|
|
start = (width - nw - 6 - uw - 16 - context_width) / 2
|
|
text(shown.proposal_text, target_size, start + nw / 2, 24, white, FontRole.BOLD)
|
|
nt, nb = self.fonts.vertical_ink(shown.proposal_text, FontRole.BOLD, text_size(target_size))
|
|
ut, ub = self.fonts.vertical_ink(shown.unit, FontRole.MEDIUM, text_size(20))
|
|
text(shown.unit, 20, start + nw + 6 + uw / 2, 24 + (nb - nt - ub + ut) / scale / 2, secondary)
|
|
text(context, context_size, start + nw + 6 + uw + 16 + context_width / 2, 24, rl.Color(203, 197, 215, 255))
|
|
for control, (fill, border, ink) in zip(slc_controls(self.profile, state), (
|
|
((35, 74, 57), (106, 153, 127), (224, 243, 232)),
|
|
((77, 46, 57), (173, 122, 134), (248, 229, 234))), strict=True):
|
|
left = (control.bounds[0] - x) / scale
|
|
top = (control.bounds[1] - y) / scale
|
|
w, h = (control.bounds[2] - control.bounds[0]) / scale, (control.bounds[3] - control.bounds[1]) / scale
|
|
# Keep the full touch area around the shorter, visually balanced face.
|
|
face_top, face_height = top + CONFIRMATION_PADDING, h - 2 * CONFIRMATION_PADDING
|
|
rounded(left, face_top, w, face_height, rl.Color(*border, 255), 16)
|
|
color = tuple(min(255, channel + 14) for channel in fill) if pressed == control.request else fill
|
|
rounded(left + 1.5, face_top + 1.5, w - 3, face_height - 3, rl.Color(*color, 255), 14.5)
|
|
text(control.label, 37, left + w / 2, top + h / 2, rl.Color(*ink, 255))
|
|
return
|
|
|
|
self.shapes.confirmation(bounds, strength=self.pulse.confirmation_strength(now))
|
|
source_color = rl.Color(197, 212, 208, 255)
|
|
label_width = ink_width(source, 22, FontRole.SEMI_BOLD)
|
|
target = displayed_adjusted_limit_mps(state.speed_limit)
|
|
direction = (-1 if target > state.speed_mps else 1) if target is not None and state.speed_mps is not None and target != state.speed_mps else 0
|
|
start = (width - label_width - (19.2 if direction else 0)) / 2
|
|
text(source, 22, start + label_width / 2, 22, source_color, FontRole.SEMI_BOLD)
|
|
if direction:
|
|
self.shapes.arrow('confirmation', x + (start + label_width + 12.6) * scale, y + 22.5 * scale,
|
|
direction, source_color, scale=1.1 * scale)
|
|
nw = ink_width(shown.posted_text, 64, FontRole.BOLD)
|
|
ow = ink_width(shown.offset_text, 32) if shown.offset_text else 0
|
|
start = (width - nw - (10 + ow if ow else 0)) / 2
|
|
# Align the posted value and offset at their visible lower edge.
|
|
number_top, number_bottom = self.fonts.vertical_ink(shown.posted_text, FontRole.BOLD, text_size(64))
|
|
text(shown.posted_text, 64, start + nw / 2, 84 - (number_bottom - number_top) / scale / 2, white, FontRole.BOLD)
|
|
if ow:
|
|
offset_text = cast(str, shown.offset_text)
|
|
top, bottom = self.fonts.vertical_ink(offset_text, FontRole.MEDIUM, text_size(32))
|
|
text(offset_text, 32, start + nw + 10 + ow / 2, 84 - (bottom - top) / scale / 2, secondary)
|
|
caption = ink_width('New Limit', 30, FontRole.SEMI_BOLD)
|
|
cx = (width - caption - 10) / 2
|
|
accent = rl.Color(214, 188, 240, 255)
|
|
text('New Limit', 30, cx + caption / 2, 104, accent, FontRole.SEMI_BOLD)
|
|
ax, ay = x + (cx + caption + 8) * scale, y + 104 * scale
|
|
rl.draw_line_ex(rl.Vector2(ax - 2 * scale, ay - 4 * scale), rl.Vector2(ax + 2 * scale, ay), 1.5 * scale, accent)
|
|
rl.draw_line_ex(rl.Vector2(ax + 2 * scale, ay), rl.Vector2(ax - 2 * scale, ay + 4 * scale), 1.5 * scale, accent)
|
|
if state.slc_wheel_confirmation_available:
|
|
accept, reject = ink_width('Accept', 26), ink_width('Reject', 26)
|
|
start = (width - 18 - 4 - accept - 10 - 18 - 4 - reject) / 2
|
|
for left, plus in ((start, True), (start + 18 + 4 + accept + 10, False)):
|
|
self.shapes.wheel_key(plus, x + (left + 9) * scale, y + 135.5 * scale, scale=scale)
|
|
hint_ink = rl.Color(210, 216, 216, 255)
|
|
text('Accept', 26, start + 22 + accept / 2, 135.5, hint_ink)
|
|
text('Reject', 26, start + 18 + 4 + accept + 10 + 22 + reject / 2, 135.5, hint_ink)
|
|
|
|
def _sign(self, state: OnroadState, bounds, now: float) -> None:
|
|
shown, obs = resolve_unified_speed(state), state.speed_limit
|
|
heading = 'LAST' if str(obs.kind) != 'valid' else SOURCE_LABELS.get(shown.source, 'SPEED')
|
|
x, top = bounds[0] + 12, bounds[1] + 12
|
|
scale = self.scale
|
|
pending = shown.pending and not obs.automatic_adoption_pending
|
|
target = displayed_adjusted_limit_mps(obs) if pending else None
|
|
arrow_direction = 0
|
|
if target is not None and state.speed_mps is not None and target != state.speed_mps:
|
|
arrow_direction = -1 if target > state.speed_mps else 1
|
|
white = self.pulse.color(rl.Color(244, 247, 245, 255), now)
|
|
secondary = self.pulse.color(rl.Color(223, 232, 227, 255), now)
|
|
|
|
def ink(text, role, size):
|
|
return tuple(value / scale for value in (*self.fonts.horizontal_ink(text, role, size * self.text_scale),
|
|
*self.fonts.vertical_ink(text, role, size * self.text_scale)))
|
|
|
|
def centered(text, role, size, ink_top, color, center_x=59):
|
|
left, right, a, b = ink(text, role, size)
|
|
# Snap only the final draw, after fitting in the shared 118 x 132 geometry.
|
|
origin = round(x + (center_x - (left + right) / 2) * scale)
|
|
y = round(top + (ink_top - a) * scale)
|
|
self.fonts.draw(text, role, size * self.text_scale, origin, y, color)
|
|
return origin + left * scale, y + a * scale, origin + right * scale, y + b * scale
|
|
|
|
def circle_fits(left, right, a, b):
|
|
return all((px - 59) ** 2 + (py - 67) ** 2 <= 46 ** 2
|
|
for px in (left, right) for py in (a, b))
|
|
|
|
vienna = state.appearance.use_vienna_sign
|
|
large_steady = self.profile == Profile.LARGE and not vienna and not shown.pending
|
|
color = rl.Color(0, 0, 0, 255) if vienna else white
|
|
if vienna:
|
|
self.shapes.circle(x + 59 * scale, top + 67 * scale, self.pulse.color(rl.Color(201, 34, 49, 255), now))
|
|
else:
|
|
self.shapes.outline(x + 59 * scale, top + 66 * scale, white)
|
|
|
|
heading_size = 28 / self.text_scale if large_steady else 18
|
|
left, right, a, b = ink(heading, FontRole.SEMI_BOLD, heading_size)
|
|
if large_steady and right - left > 96:
|
|
heading_size *= 96 / (right - left)
|
|
left, right, a, b = ink(heading, FontRole.SEMI_BOLD, heading_size)
|
|
heading_width = right - left
|
|
header_y = 47 if vienna else 21 if large_steady else 26
|
|
# The small arrow's visible ink is 12 x 16, with a six-pixel label gap.
|
|
header_left = 59 - (heading_width + (18 if arrow_direction else 0)) / 2
|
|
heading_color = self.pulse.color(MUTED, now) if large_steady else color
|
|
centered(heading, FontRole.SEMI_BOLD, heading_size, header_y - (b - a) / 2, heading_color, header_left + heading_width / 2)
|
|
if arrow_direction:
|
|
self.shapes.arrow('compact', round(x + (header_left + heading_width + 12) * scale),
|
|
round(top + header_y * scale), arrow_direction, color)
|
|
|
|
role = FontRole.SET_SPEED if large_steady else FontRole.BOLD
|
|
size = 77.92 / self.text_scale if large_steady else 58 if vienna else 64
|
|
offset_size = 30 if vienna else 27
|
|
number_center = (73 if shown.offset_text else 80) if vienna else (67 if shown.offset_text else 81)
|
|
while True:
|
|
left, right, a, b = ink(shown.posted_text, role, size)
|
|
width, height = right - left, b - a
|
|
number_top = number_center - height / 2
|
|
if vienna:
|
|
fits = number_top >= 57 and circle_fits(59 - width / 2, 59 + width / 2, number_top, number_top + height)
|
|
if shown.offset_text:
|
|
left, right, a, b = ink(shown.offset_text, FontRole.MEDIUM, offset_size)
|
|
offset_top = number_top + height + 4
|
|
fits = fits and circle_fits(59 - (right - left) / 2, 59 + (right - left) / 2, offset_top, offset_top + b - a)
|
|
else:
|
|
fits = width <= 96 and height <= 54
|
|
if fits:
|
|
break
|
|
size *= .95
|
|
if vienna:
|
|
offset_size *= .95
|
|
number = centered(shown.posted_text, role, size, number_top, color)
|
|
if shown.offset_text:
|
|
if not vienna:
|
|
left, right, a, b = ink(shown.offset_text, FontRole.MEDIUM, offset_size)
|
|
offset_size *= min(1, 96 / (right - left), 24 / (b - a))
|
|
offset_top = 97
|
|
centered(shown.offset_text, FontRole.MEDIUM, offset_size, offset_top, color if vienna else secondary)
|
|
if obs.driver_override_active:
|
|
color = rl.Color(197, 163, 255, 240)
|
|
left, top, right, bottom = number
|
|
for start, end in ((rl.Vector2(left + 3 * scale, top + 3 * scale), rl.Vector2(right - 3 * scale, bottom - 3 * scale)),
|
|
(rl.Vector2(right - 3 * scale, top + 3 * scale), rl.Vector2(left + 3 * scale, bottom - 3 * scale))):
|
|
rl.draw_line_ex(start, end, 4.5 * scale, rl.Color(6, 14, 10, 220))
|
|
rl.draw_line_ex(start, end, 2.5 * scale, color)
|