mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-12 07:23:45 +08:00
Correct sign size/sign size + offset sizes
This commit is contained in:
@@ -28,6 +28,7 @@ from openpilot.starpilot.ui.onroad_dm import draw_monitor_art, monitor_bounds, m
|
||||
from openpilot.starpilot.ui.onroad_stopped_timer import PREVIEW_DURATION_S
|
||||
from openpilot.starpilot.ui.layout_preview_sidebar import SIDEBAR_WIDTH, render_sidebar
|
||||
from openpilot.starpilot.ui.presentation import BitmapFonts, FontRole, Profile, default_font_directory
|
||||
from openpilot.starpilot.speed_limits.speed_domain import OffsetBand, OffsetSchedule
|
||||
from openpilot.starpilot.ui.rainbow_path import RainbowPath
|
||||
from openpilot.starpilot.ui.road_colors import edge_gradient, lane_color, path_mode, solid_gradient
|
||||
from openpilot.starpilot.ui.onroad_customization import ROAD_COLORS
|
||||
@@ -78,6 +79,7 @@ def sample_state(scene: str, document: dict) -> OnroadState:
|
||||
from openpilot.starpilot.ui.weather import WeatherReading
|
||||
state = OnroadState(weather=WeatherReading(21, 2, True), engaged=lateral or longitudinal, camera_available=False,
|
||||
speed_mps=0.0 if scene in ("cem_lead", "stopped") else 22.0, cruise_kph=88.0,
|
||||
slc_offset_schedule=OffsetSchedule((OffsetBand(0., None, 2.),)),
|
||||
stopped_duration_s=PREVIEW_DURATION_S if scene == "stopped" else None,
|
||||
speed_limit=SpeedLimitObservation(kind=ObservationKind.VALID, source="dashboard",
|
||||
speed_limit_mps=22.0 if pending else 20.0,
|
||||
|
||||
@@ -23,6 +23,7 @@ from openpilot.starpilot.ui.traffic_status import TrafficDisplay
|
||||
from openpilot.starpilot.ui.developer_preview import OnroadVisualPreview
|
||||
from openpilot.starpilot.ui.navigation_state import NavigationDisplay
|
||||
from openpilot.starpilot.ui.live_developer_sidebar import DeveloperMetric
|
||||
from openpilot.starpilot.speed_limits.speed_domain import OffsetSchedule
|
||||
|
||||
|
||||
|
||||
@@ -244,6 +245,7 @@ class OnroadState:
|
||||
weather: WeatherReading | None = None
|
||||
ford_rear_fog_on: bool = False
|
||||
ford_rear_fog_source_ns: int = 0
|
||||
slc_offset_schedule: OffsetSchedule | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.speed_mps is not None and (not math.isfinite(self.speed_mps) or self.speed_mps < 0):
|
||||
|
||||
@@ -55,6 +55,8 @@ from openpilot.starpilot.ui.settings_state import Destination, DestinationAvaila
|
||||
from openpilot.starpilot.ui.shell import ShellMode, ShellSnapshot
|
||||
from openpilot.starpilot.ui.software_state import DownloadLabel, SoftwareRequest, SoftwareState
|
||||
from openpilot.starpilot.ui.toggles_state import Personality, ToggleKey, TogglesState
|
||||
from openpilot.starpilot.saved_source import read_saved
|
||||
from openpilot.starpilot.speed_limits import offset_document
|
||||
|
||||
|
||||
SLC_SIGN_HOLD_NS = 150_000_000
|
||||
@@ -314,6 +316,7 @@ class RuntimeSnapshotAdapter:
|
||||
self._customization_value = default_document()
|
||||
self._appearance_value = OnroadAppearance()
|
||||
self._appearance_read_ns: int | None = None
|
||||
self._slc_offset_schedule = None
|
||||
# A second screen (Android Auto) swaps in its own display choices before anything uses them.
|
||||
self.appearance_override: Callable[[OnroadAppearance], OnroadAppearance] | None = None
|
||||
self._last_mode: ShellMode | None = None
|
||||
@@ -442,6 +445,12 @@ class RuntimeSnapshotAdapter:
|
||||
self._appearance_value = onroad_appearance(self.ui_state.params)
|
||||
self._navigation_enabled = _flag(self.ui_state.params, "NavigationEnabled")
|
||||
self._show_curve_status = _flag(self.ui_state.params, "ShowCSCStatus")
|
||||
try:
|
||||
raw, readable = read_saved(self.ui_state.params, "SLCOffsetSchedule", offset_document.MAX_DOCUMENT_BYTES)
|
||||
offsets = offset_document.decode(raw) if readable and raw is not None else None
|
||||
self._slc_offset_schedule = offsets.schedule() if isinstance(offsets, offset_document.OffsetDocument) else None
|
||||
except (AttributeError, OSError, TypeError, ValueError, OverflowError):
|
||||
self._slc_offset_schedule = None
|
||||
if self.appearance_override is not None:
|
||||
self._appearance_value = self.appearance_override(self._appearance_value)
|
||||
self._appearance_read_ns = now_ns
|
||||
@@ -720,7 +729,7 @@ class RuntimeSnapshotAdapter:
|
||||
engaged=display_lat_active or display_long_active or longitudinal_overridden, camera_available=started,
|
||||
speed_mps=raw_speed, cruise_kph=cruise, speed_limit=observation,
|
||||
appearance=self._appearance_value, customization=self._customization_value,
|
||||
alert=alert, metric=bool(ui.is_metric),
|
||||
alert=alert, metric=bool(ui.is_metric), slc_offset_schedule=self._slc_offset_schedule,
|
||||
experimental_available=bool(ui.has_longitudinal_control),
|
||||
experimental_enabled=observed_experimental,
|
||||
personality=list(Personality).index(persona),
|
||||
|
||||
@@ -49,26 +49,28 @@ class SpeedLimitShapes:
|
||||
if image.data is not None:
|
||||
rl.unload_image(image)
|
||||
|
||||
def _draw(self, name, x, y, color, rotation=0, *, scale=None):
|
||||
def _draw(self, name, x, y, color, rotation=0, *, scale=None, width_scale=1):
|
||||
self._load()
|
||||
source = self.sources[name]
|
||||
origin = self.origins[name] if scale is None else rl.Vector2(source.width * scale / SPRITE_DENSITIES.get(name, 1) / 2,
|
||||
source.height * scale / SPRITE_DENSITIES.get(name, 1) / 2)
|
||||
scale = (self.scale if scale is None else scale) / SPRITE_DENSITIES.get(name, 1)
|
||||
rl.draw_texture_pro(self.texture, source, rl.Rectangle(x, y, source.width * scale, source.height * scale),
|
||||
if width_scale != 1:
|
||||
origin = rl.Vector2(origin.x * width_scale, origin.y)
|
||||
rl.draw_texture_pro(self.texture, source, rl.Rectangle(x, y, source.width * scale * width_scale, source.height * scale),
|
||||
origin, rotation, color)
|
||||
|
||||
def circle(self, x, y, color):
|
||||
self._outlined('ring', x, y, color)
|
||||
|
||||
def _outlined(self, name, x, y, color, *, scale=None):
|
||||
def _outlined(self, name, x, y, color, *, scale=None, width_scale=1):
|
||||
radius = self.scale if scale is None else scale
|
||||
for dx, dy in ((-radius, 0), (radius, 0), (0, -radius), (0, radius)):
|
||||
self._draw(name, x + dx, y + dy, rl.Color(6, 14, 10, 230), scale=scale)
|
||||
self._draw(name, x, y, color, scale=scale)
|
||||
self._draw(name, x + dx, y + dy, rl.Color(6, 14, 10, 230), scale=scale, width_scale=width_scale)
|
||||
self._draw(name, x, y, color, scale=scale, width_scale=width_scale)
|
||||
|
||||
def outline(self, x, y, color, *, scale=None):
|
||||
self._outlined('outline', x, y, color, scale=scale)
|
||||
def outline(self, x, y, color, *, scale=None, width_scale=1):
|
||||
self._outlined('outline', x, y, color, scale=scale, width_scale=width_scale)
|
||||
|
||||
def button(self, x, y, color, *, profile, scale, pressed=False):
|
||||
self._draw(f'button_{profile}' + ('_pressed' if pressed else ''), x, y, color, scale=scale)
|
||||
|
||||
@@ -312,12 +312,17 @@ class SpeedLimitWidget:
|
||||
return
|
||||
|
||||
color = white
|
||||
# Clamping the controller target does not configure an offset for this sign.
|
||||
offset_text = shown.offset_text if vienna or shown.offset_configured else None
|
||||
if vienna:
|
||||
x, top = x + 5 * scale, top + 5 * scale
|
||||
center_x = 59
|
||||
self.shapes.circle(x + 59 * scale, top + sign_center_y * scale, self.pulse.color(rl.Color(201, 34, 49, 255), now))
|
||||
else:
|
||||
self.shapes.outline(x + center_x * scale, top + sign_center_y * scale, IN_USE if shown.limit_in_use else white)
|
||||
# Keep 4:5 proportions and the original text sizes inside the existing tap area.
|
||||
sign_scale = (160 if offset_text else 150) / SIGN_HEIGHT
|
||||
self.shapes.outline(x + center_x * scale, top + (SIGN_HEIGHT * sign_scale / 2 - 9) * scale, IN_USE if shown.limit_in_use else white,
|
||||
scale=scale * sign_scale, width_scale=SIGN_HEIGHT * 4 / 5 / SIGN_WIDTH)
|
||||
|
||||
heading_size = HEADING_SIZE if not vienna else 18
|
||||
if vienna and self.profile == Profile.LARGE and not shown.pending:
|
||||
@@ -327,8 +332,8 @@ class SpeedLimitWidget:
|
||||
heading_size *= 108 / (right - left)
|
||||
left, right, a, b = ink(heading, FontRole.SEMI_BOLD, heading_size)
|
||||
# Lift the Vienna stack to keep the offset clear of the bottom rim.
|
||||
lift = (12 if shown.limit_in_use else 7 if shown.offset_text else 2) if vienna else 0
|
||||
header_y = 47 - lift if vienna else HEADING_ROW
|
||||
lift = (12 if shown.limit_in_use else 7 if offset_text else 2) if vienna else 0
|
||||
header_y = 47 - lift if vienna else 21 if offset_text else 26
|
||||
arrow_scale = 1.
|
||||
if vienna:
|
||||
# Fit the heading and arrow inside the circle at the top of their ink.
|
||||
@@ -352,15 +357,15 @@ class SpeedLimitWidget:
|
||||
role = FontRole.BOLD if vienna else FontRole.SET_SPEED
|
||||
size = 58 if vienna else NUMBER_SIZE
|
||||
offset_size = 30 if vienna else 27
|
||||
number_center = (73 if shown.offset_text else 80) - lift if vienna else NUMBER_ROW
|
||||
number_center = (73 if offset_text else 80) - lift if vienna else 76 if offset_text else 90
|
||||
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 - lift 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)
|
||||
if offset_text:
|
||||
left, right, a, b = ink(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:
|
||||
@@ -374,12 +379,12 @@ class SpeedLimitWidget:
|
||||
centered(shown.posted_text, role, size, number_top, color)
|
||||
else:
|
||||
draw_speed_row(self.fonts, shown.posted_text, role, size * self.text_scale, x + center_x * scale,
|
||||
top + NUMBER_ROW * scale, color, '0123456789')
|
||||
if shown.offset_text:
|
||||
top + number_center * scale, color, '0123456789')
|
||||
if offset_text:
|
||||
if not vienna:
|
||||
left, right, a, b = ink(shown.offset_text, FontRole.MEDIUM, offset_size)
|
||||
left, right, a, b = ink(offset_text, FontRole.MEDIUM, offset_size)
|
||||
offset_size *= min(1, 108 / (right - left), 24 / (b - a))
|
||||
offset_top = 105
|
||||
centered(shown.offset_text, FontRole.MEDIUM, offset_size, offset_top, color if vienna else secondary)
|
||||
offset_top = 120
|
||||
centered(offset_text, FontRole.MEDIUM, offset_size, offset_top, color if vienna else secondary)
|
||||
if shown.limit_in_use:
|
||||
centered('IN USE', FontRole.SEMI_BOLD, 15 if vienna else 22, 98 if vienna else 117, IN_USE)
|
||||
|
||||
@@ -26,7 +26,7 @@ def historical_outline():
|
||||
@pytest.mark.parametrize('profile', list(Profile))
|
||||
@pytest.mark.parametrize('offset', [False, True])
|
||||
@pytest.mark.parametrize('vienna', [False, True])
|
||||
def test_both_sign_styles_keep_their_height_with_or_without_offset(profile, offset, vienna, historical_outline):
|
||||
def test_sign_layout_and_tap_area_stay_fixed_with_or_without_offset(profile, offset, vienna, historical_outline):
|
||||
current = state(show_slc_offset=offset)
|
||||
current = replace(current, appearance=replace(current.appearance, use_vienna_sign=vienna))
|
||||
widget = SpeedLimitWidget(fonts(profile), profile)
|
||||
@@ -46,8 +46,10 @@ def test_both_sign_styles_keep_their_height_with_or_without_offset(profile, offs
|
||||
else:
|
||||
historical_outline.assert_not_called()
|
||||
widget.shapes.outline.assert_called_once()
|
||||
assert widget.shapes.outline.call_args.args[:2] == (bounds[0]+12+SIGN_WIDTH/2*scale, bounds[1]+12+(SIGN_HEIGHT/2-7)*scale)
|
||||
assert widget.shapes.outline.call_args.kwargs == {}
|
||||
sign_scale = (160 if offset else 150) / SIGN_HEIGHT
|
||||
assert widget.shapes.outline.call_args.args[:2] == (bounds[0]+12+SIGN_WIDTH/2*scale,
|
||||
bounds[1]+12+(SIGN_HEIGHT*sign_scale/2-9)*scale)
|
||||
assert widget.shapes.outline.call_args.kwargs == {'scale': scale * sign_scale, 'width_scale': .9}
|
||||
assert ('+5' in [call.args[0] for call in foreground_calls(widget.fonts.draw)]) == offset
|
||||
|
||||
|
||||
@@ -103,3 +105,84 @@ def test_small_rectangular_no_offset_regression(historical_outline):
|
||||
assert bounds is not None and bounds[3] - bounds[1] == SIGN_HEIGHT + 20
|
||||
historical_outline.assert_not_called()
|
||||
widget.shapes.outline.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('profile', list(Profile))
|
||||
@pytest.mark.parametrize('metric', (False, True))
|
||||
@pytest.mark.parametrize('posted_mps', (1 / 3.6, 20., 40., 160 / 3.6))
|
||||
@pytest.mark.parametrize('configured_offset', (0, 4, -4))
|
||||
def test_only_the_displayed_ranges_configured_offset_expands_the_sign(profile, metric, posted_mps, configured_offset):
|
||||
from openpilot.starpilot.speed_limits.speed_domain import OffsetBand, OffsetSchedule, adjusted_limit
|
||||
factor = 3.6 if metric else 2.2369362921
|
||||
bands = ((0., 10.), (10., 40.), (40., None))
|
||||
schedule = OffsetSchedule(tuple(OffsetBand(lower, upper,
|
||||
(configured_offset if lower <= posted_mps and (upper is None or posted_mps < upper) else 7) / factor)
|
||||
for lower, upper in bands))
|
||||
target = adjusted_limit(posted_mps, schedule)
|
||||
assert target is not None
|
||||
current = state(metric=metric, speed_mps=5., slc_offset_schedule=schedule)
|
||||
current = replace(current, speed_limit=SpeedLimitObservation(ObservationKind.VALID, 'vision', posted_mps,
|
||||
offset_mps=target - posted_mps, presentation_adjusted_limit_mps=target, accepted_speed_limit_mps=15.))
|
||||
shown = resolve_unified_speed(current)
|
||||
assert shown.offset_configured == (configured_offset != 0)
|
||||
assert shown.limit_text == str(round(target * factor))
|
||||
if configured_offset == 0 and posted_mps in (1 / 3.6, 160 / 3.6):
|
||||
assert shown.offset_text is not None # The effective adjustment is clamping, not a configured offset.
|
||||
widget = SpeedLimitWidget(fonts(profile), profile)
|
||||
widget.shapes = Mock()
|
||||
widget.render(current)
|
||||
visible_offset = configured_offset != 0 and shown.offset_text is not None
|
||||
assert widget.shapes.outline.call_args.kwargs['scale'] == pytest.approx(widget.scale * (160 if visible_offset else 150) / SIGN_HEIGHT)
|
||||
assert [call.args[0] for call in foreground_calls(widget.fonts.draw)] == (
|
||||
['VISION', shown.posted_text] + ([shown.offset_text] if visible_offset else []))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('profile', list(Profile))
|
||||
def test_mutcd_preserves_text_sizes_and_fits_both_sizes_with_real_fonts(profile):
|
||||
from itertools import product
|
||||
from openpilot.starpilot.speed_limits.speed_domain import OffsetBand, OffsetSchedule
|
||||
from openpilot.starpilot.ui.presentation import BitmapFonts, FontRole, default_font_directory
|
||||
owns_context = not rl.is_window_ready()
|
||||
if owns_context:
|
||||
rl.init_window(128, 128, 'MUTCD spacing')
|
||||
try:
|
||||
with BitmapFonts(profile, default_font_directory()) as font:
|
||||
widget = SpeedLimitWidget(font, profile)
|
||||
for value, adjustment, stage in product((65, 120, 199), (0, 4, 10, -10), ('live', 'pending', 'retained')):
|
||||
current = state(metric=True, slc_offset_schedule=OffsetSchedule((OffsetBand(0., None, adjustment / 3.6),)))
|
||||
current = replace(current, speed_mps=(value + adjustment + 2) / 3.6,
|
||||
speed_limit=SpeedLimitObservation(ObservationKind.VALID, 'vision', value / 3.6,
|
||||
offset_mps=adjustment / 3.6, action_enabled=False,
|
||||
pending_speed_limit_mps=value / 3.6 if stage == 'pending' else None))
|
||||
if stage == 'retained':
|
||||
current = replace(current, speed_limit=replace(current.speed_limit, kind=ObservationKind.STALE, speed_limit_mps=None,
|
||||
last_received_speed_limit_mps=value / 3.6, last_received_adjusted_limit_mps=(value + adjustment) / 3.6,
|
||||
last_received_source='vision', session_id='drive'))
|
||||
with patch.object(font, 'draw') as drawn, patch.object(widget.shapes, 'outline') as outline:
|
||||
widget.render(current)
|
||||
scale = widget.scale * (160 if adjustment else 150) / SIGN_HEIGHT
|
||||
cx, cy = outline.call_args.args[:2]
|
||||
half_width, half_height = (SIGN_WIDTH / 2 - 4) * .9 * scale, (SIGN_HEIGHT / 2 - 4) * scale
|
||||
boxes = []
|
||||
for call in foreground_calls(drawn):
|
||||
text, role, size, x, y, _ = call.args
|
||||
left, right = font.horizontal_ink(text, role, size)
|
||||
top, bottom = font.vertical_ink(text, role, size)
|
||||
if text.startswith('LAST '):
|
||||
assert right - left == pytest.approx(108 * widget.scale)
|
||||
else:
|
||||
assert size == pytest.approx({FontRole.SEMI_BOLD: 26, FontRole.SET_SPEED: 70, FontRole.MEDIUM: 27}[role] * widget.text_scale)
|
||||
boxes.append((x + left, y + top, x + right, y + bottom))
|
||||
assert cx - half_width + 1 <= min(box[0] for box in boxes)
|
||||
assert max(box[2] for box in boxes) <= cx + half_width - 1
|
||||
assert cy - half_height + 1 <= boxes[0][1]
|
||||
assert boxes[-1][3] <= cy + half_height - 1
|
||||
assert all(a[3] + 4 * widget.scale <= b[1] for a, b in zip(boxes, boxes[1:], strict=False))
|
||||
bounds = speed_limit_bounds(profile, current)
|
||||
outer_width, outer_height = (SIGN_WIDTH / 2 - 2) * .9 * scale + scale, (SIGN_HEIGHT / 2 - 2) * scale + scale
|
||||
assert bounds[0] <= cx - outer_width and cx + outer_width <= bounds[2]
|
||||
assert bounds[1] <= cy - outer_height and cy + outer_height <= bounds[3]
|
||||
widget.close()
|
||||
finally:
|
||||
if owns_context:
|
||||
rl.close_window()
|
||||
|
||||
@@ -619,9 +619,9 @@ def test_compact_us_sign_keeps_camera_visible_at_saved_position(speed):
|
||||
hud._speed_limit_sign(state)
|
||||
fill.assert_not_called()
|
||||
outline.assert_called_once()
|
||||
assert outline.call_args.args[:2] == (264, 95)
|
||||
assert outline.call_args.args[:2] == (264, 96)
|
||||
assert [call.args[0] for call in foreground_calls(fonts.draw)] == ['SPEED', str(speed)]
|
||||
assert foreground_calls(fonts.draw)[0].args[3:5] == (259, 38)
|
||||
assert foreground_calls(fonts.draw)[0].args[3:5] == (259, 47)
|
||||
|
||||
|
||||
def test_compact_drag_keeps_animation_state_and_separate_origins():
|
||||
|
||||
@@ -100,6 +100,43 @@ def ui_fake():
|
||||
has_longitudinal_control=True, CP=None, recording_audio=False)
|
||||
|
||||
|
||||
def test_slc_offset_schedule_uses_cached_validated_settings(tmp_path):
|
||||
from pathlib import Path
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.starpilot.saved_source import read_saved
|
||||
from openpilot.starpilot.speed_limits import offset_document as od
|
||||
from openpilot.starpilot.ui.unified_speed_presentation import resolve_unified_speed
|
||||
ui = ui_fake()
|
||||
ui.params = Params(str(tmp_path))
|
||||
document = od.OffsetDocument((0., 10., 15., 20., 25., 30., 35., 40.), (7., 7., 7., 0., 7., 7., 7.))
|
||||
ui.params.put('SLCOffsetSchedule', od.to_value(document), block=True)
|
||||
adapter = RuntimeSnapshotAdapter(ui)
|
||||
with patch('openpilot.starpilot.ui.runtime_snapshot.read_saved', wraps=read_saved) as reads:
|
||||
first = adapter.build(ShellMode.ONROAD, now_ns=NOW).onroad
|
||||
assert first.slc_offset_schedule == document.schedule()
|
||||
assert not resolve_unified_speed(first).offset_configured
|
||||
document = od.OffsetDocument(document.bounds_mps, (7.,) * 7)
|
||||
ui.params.put('SLCOffsetSchedule', od.to_value(document), block=True)
|
||||
cached = adapter.build(ShellMode.ONROAD, now_ns=NOW + 50_000_000).onroad
|
||||
assert cached.slc_offset_schedule is first.slc_offset_schedule
|
||||
reads.assert_called_once()
|
||||
adapter.invalidate_appearance()
|
||||
updated = adapter.build(ShellMode.ONROAD, now_ns=NOW + 100_000_000).onroad
|
||||
assert updated.slc_offset_schedule == document.schedule()
|
||||
assert resolve_unified_speed(updated).offset_configured
|
||||
assert reads.call_count == 2
|
||||
path = Path(ui.params.get_param_path('SLCOffsetSchedule'))
|
||||
for raw in (b'{bad', od.encode(od.NEEDS_REVIEW), b'x' * (od.MAX_DOCUMENT_BYTES + 1), None):
|
||||
if raw is None:
|
||||
path.unlink()
|
||||
else:
|
||||
path.write_bytes(raw)
|
||||
adapter.invalidate_appearance()
|
||||
invalid = adapter.build(ShellMode.ONROAD, now_ns=NOW + 100_000_000).onroad
|
||||
assert invalid.slc_offset_schedule is None
|
||||
assert not resolve_unified_speed(invalid).offset_configured
|
||||
|
||||
|
||||
class TestRuntimeSnapshot(unittest.TestCase):
|
||||
|
||||
def test_home_uses_shared_statistics_source_without_onroad_polling(self):
|
||||
|
||||
@@ -78,6 +78,21 @@ def test_bad_atlas_dimensions_release_image_without_uploading(gpu):
|
||||
gpu['load_texture_from_image'].assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('scale', (1., 8 / 9))
|
||||
def test_narrow_outline_reuses_the_atlas_and_keeps_its_center(gpu, scale):
|
||||
owner = shapes.SpeedLimitShapes()
|
||||
owner.outline(64, 65, shapes.rl.WHITE, scale=scale, width_scale=.9)
|
||||
assert gpu['draw_texture_pro'].call_count == 5
|
||||
_, source, destination, origin, _, _ = gpu['draw_texture_pro'].call_args.args
|
||||
assert (source.width, source.height) == (136, 152)
|
||||
assert (destination.width, destination.height) == pytest.approx((136 * scale * .9, 152 * scale))
|
||||
assert (origin.x, origin.y) == pytest.approx((destination.width / 2, destination.height / 2))
|
||||
owner.outline(64, 65, shapes.rl.WHITE)
|
||||
assert gpu['draw_texture_pro'].call_args.args[3].x == 68
|
||||
gpu['load_texture_from_image'].assert_called_once()
|
||||
owner.close()
|
||||
|
||||
|
||||
def test_large_sign_scale_changes_destination_and_rotation_origin_only(gpu):
|
||||
owner = shapes.SpeedLimitShapes(1.25)
|
||||
owner.arrow('compact', 345.75, 111.25, -1, shapes.rl.BLACK)
|
||||
|
||||
@@ -33,12 +33,14 @@ def drawing(monkeypatch):
|
||||
|
||||
|
||||
def state(**changes):
|
||||
from openpilot.starpilot.speed_limits.speed_domain import OffsetBand, OffsetSchedule
|
||||
observation = SpeedLimitObservation(ObservationKind.VALID, 'map', 50 / 3.6,
|
||||
accepted_speed_limit_mps=40 / 3.6, accepted_adjusted_limit_mps=45 / 3.6,
|
||||
pending_speed_limit_mps=50 / 3.6, pending_adjusted_limit_mps=55 / 3.6,
|
||||
presentation_adjusted_limit_mps=55 / 3.6, accepted_source='dashboard', action_enabled=True,
|
||||
session_id='drive', presentation_id=2, decision_id=1, pending_since_ns=10_000_000_000)
|
||||
return replace(OnroadState(True, False, 45 / 3.6, 100., observation, metric=True, longitudinal_active=True,
|
||||
slc_offset_schedule=OffsetSchedule((OffsetBand(0., None, 5 / 3.6),)),
|
||||
observed_ns=10_000_000_000), **changes)
|
||||
|
||||
|
||||
|
||||
@@ -105,6 +105,21 @@ def test_hidden_max_and_offset_preferences_keep_the_latest_sign():
|
||||
assert (shown.mode, shown.posted_text, shown.offset_text) == ('limit_only', '108', None)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('stage,expected', [('live', True), ('confirmation', False), ('retained', False), ('last_received', True)])
|
||||
def test_configured_offset_follows_the_resolved_posted_limit(stage, expected):
|
||||
from openpilot.starpilot.speed_limits.speed_domain import OffsetBand, OffsetSchedule
|
||||
current = state(slc_offset_schedule=OffsetSchedule((OffsetBand(0., 25., 0.), OffsetBand(25., None, 2.))))
|
||||
if stage == 'confirmation':
|
||||
current = replace(current, speed_limit=replace(current.speed_limit, pending_speed_limit_mps=20., pending_adjusted_limit_mps=20.))
|
||||
elif stage in ('retained', 'last_received'):
|
||||
current = replace(current, speed_limit=replace(current.speed_limit, kind=ObservationKind.STALE, speed_limit_mps=None,
|
||||
accepted_adjusted_limit_mps=20., last_received_speed_limit_mps=30., last_received_adjusted_limit_mps=32.,
|
||||
effective_cluster_target_mps=20. if stage == 'retained' else None, effective_cap_mps=20.))
|
||||
shown = resolve_unified_speed(current, confirmation=stage == 'confirmation')
|
||||
assert shown.posted_text == ('108' if expected else '72')
|
||||
assert shown.offset_configured == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('pending,adjusted,presentation,offset,expected', [
|
||||
(30., 33., 32., 2., '119'),
|
||||
(30., None, 32., 2., '115'),
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
from dataclasses import dataclass
|
||||
import math
|
||||
|
||||
from openpilot.starpilot.speed_limits.speed_domain import offset_for_limit
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class UnifiedSpeedPresentation:
|
||||
@@ -23,6 +25,7 @@ class UnifiedSpeedPresentation:
|
||||
limit_in_use: bool = False
|
||||
set_limited: bool = False
|
||||
show_last: bool = False
|
||||
offset_configured: bool = False
|
||||
|
||||
@property
|
||||
def max_status(self) -> str:
|
||||
@@ -159,4 +162,6 @@ def resolve_unified_speed(state, *, confirmation=False) -> UnifiedSpeedPresentat
|
||||
obs.override_basis != 'pedal' and
|
||||
positive(obs.effective_cluster_target_mps) else None,
|
||||
speed(active_target) if slc_limiting and positive(active_target) else None,
|
||||
posted_retained, limit_in_use, set_limited, show_last)
|
||||
posted_retained, limit_in_use, set_limited, show_last,
|
||||
offset_configured=state.slc_offset_schedule is not None and
|
||||
offset_for_limit(posted, state.slc_offset_schedule) not in (None, 0.))
|
||||
|
||||
Reference in New Issue
Block a user