mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-09 22:13:43 +08:00
475 lines
24 KiB
Python
475 lines
24 KiB
Python
"""Parked onroad layout preview using the same native view as the driving UI."""
|
|
|
|
from pathlib import Path
|
|
import json
|
|
import math
|
|
import struct
|
|
import time
|
|
from openpilot.common.filter_simple import FirstOrderFilter
|
|
import zlib
|
|
|
|
import numpy as np
|
|
from dataclasses import replace
|
|
|
|
import pyray as rl
|
|
|
|
from openpilot.starpilot.ui.layout_preview_transport import SCENES
|
|
from openpilot.starpilot.ui.developer_preview import OnroadVisualPreview
|
|
from openpilot.starpilot.ui.onroad_customization import CAMERA_WIDGETS, PROFILES, RUNTIME_PREVIEW_WIDGETS, placement, validate_document, widget_size
|
|
from openpilot.starpilot.ui.onroad_widget_style import draw_widget_frame
|
|
from openpilot.starpilot.curve_speed.status import CurveObservation
|
|
from openpilot.starpilot.ui.onroad_state import ConditionalLead
|
|
from openpilot.starpilot.ui.onroad_state import AlertSize, ObservationKind, OnroadAlert, OnroadState, SourceReading, SpeedLimitObservation
|
|
from openpilot.starpilot.ui.appearance_preferences import CameraViewChoice, OnroadAppearance
|
|
from openpilot.starpilot.ui.onroad_dm import monitor_visible
|
|
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.ui.wheel_feedback import WheelFeedback
|
|
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
|
|
from openpilot.system.ui.lib.shader_polygon import Gradient, draw_polygon
|
|
|
|
|
|
CEM_SCENE_REASONS = {"cem_stop_light": "STOP LIGHT", "cem_lead": "LEAD", "cem_curve": "CURVE"}
|
|
IDLE_SECONDS = 30.0
|
|
ASSET_DIRECTORY = Path(__file__).parents[2] / "selfdrive/assets"
|
|
|
|
|
|
def render_sample_road(rect: rl.Rectangle, state: OnroadState, profile: Profile) -> None:
|
|
style = state.customization["roadColors"][str(profile)]
|
|
|
|
def points(values):
|
|
return np.array([(rect.x + x * rect.width, rect.y + y * rect.height) for x, y in values], dtype=np.float32)
|
|
|
|
draw_polygon(rect, points([(0, 1), (.4, .45), (.55, .45), (.83, 1)]), rl.Color(48, 57, 67, 255))
|
|
if profile == Profile.LARGE and "pathEdge" in style:
|
|
for strip in ([(.18, 1), (.44, .45), (.447, .45), (.231, 1)],
|
|
[(.639, 1), (.503, .45), (.51, .45), (.69, 1)]):
|
|
draw_polygon(rect, points(strip), gradient=edge_gradient(style["pathEdge"]))
|
|
path = [(.231, 1), (.447, .45), (.503, .45), (.639, 1)]
|
|
else:
|
|
path = [(.18, 1), (.44, .45), (.51, .45), (.69, 1)]
|
|
mode = path_mode(style)
|
|
gradient = (RainbowPath().gradient() if mode == "rainbow" else
|
|
solid_gradient(style.get("path", ROAD_COLORS["path"])) if mode == "color" else
|
|
Gradient((0, 1), (0, 0), [rl.Color(13, 248, 122, 102), rl.Color(114, 255, 92, 89),
|
|
rl.Color(114, 255, 92, 0)], [0, .5, 1]))
|
|
draw_polygon(rect, points(path), gradient=gradient)
|
|
for index, (bottom, top) in enumerate(((.16, .43), (.71, .52), (.02, .38), (.85, .57))):
|
|
adjacent = profile == Profile.COMPACT and index < 2
|
|
default = rl.Color(0, 255, 64, 178) if adjacent else rl.Color(255, 255, 255, 178)
|
|
line = [(bottom - .002, 1), (top - .0005, .45), (top + .0005, .45), (bottom + .002, 1)]
|
|
draw_polygon(rect, points(line), lane_color(style, adjacent, default, .7))
|
|
|
|
|
|
def sample_state(scene: str, document: dict) -> OnroadState:
|
|
if scene not in SCENES:
|
|
raise ValueError("Unknown layout preview scene")
|
|
lateral = scene in ("engaged", "aol", "experimental", "braking", "stopped", "slc_pending") or scene in CEM_SCENE_REASONS
|
|
longitudinal = scene in ("engaged", "long_only", "experimental", "braking", "stopped", "slc_pending") or scene in CEM_SCENE_REASONS
|
|
longitudinal = longitudinal or scene in ('slc_max', 'slc_equal', 'slc_pedal', 'slc_retained')
|
|
state = OnroadState(engaged=lateral or longitudinal, camera_available=False,
|
|
speed_mps=0.0 if scene in ("cem_lead", "stopped") else 22.0, cruise_kph=88.0,
|
|
stopped_duration_s=PREVIEW_DURATION_S if scene == "stopped" else None,
|
|
speed_limit=SpeedLimitObservation(kind=ObservationKind.VALID, source="dashboard", speed_limit_mps=20.0,
|
|
accepted_speed_limit_mps=20.0, accepted_source='dashboard',
|
|
accepted_adjusted_limit_mps=22.0, effective_cluster_target_mps=22.0,
|
|
offset_mps=2.0, limiting_max_set=True,
|
|
pending_adjusted_limit_mps=24.0 if scene == 'slc_pending' else None,
|
|
presentation_adjusted_limit_mps=24.0 if scene == 'slc_pending' else 22.0,
|
|
pending_speed_limit_mps=22.0 if scene == "slc_pending" else None,
|
|
session_id="preview",
|
|
source_readings=(SourceReading('dashboard', True, 'valid', 20),
|
|
SourceReading('map', True, 'valid', 20),
|
|
SourceReading('vision', True, 'unknown')),
|
|
decision_id=1, presentation_id=1, action_enabled=longitudinal),
|
|
alert=OnroadAlert(), appearance=OnroadAppearance(wheel_pedal_feedback=True,
|
|
show_speed_limit_sign=True),
|
|
wheel_feedback=WheelFeedback(brake_pressed=scene == "braking"),
|
|
metric=False, experimental_available=True,
|
|
experimental_enabled=scene == "experimental" or scene in CEM_SCENE_REASONS, lateral_active=lateral,
|
|
longitudinal_active=longitudinal, stock_cruise_active=False,
|
|
slc_system_long_available=longitudinal, torque_utilization=0.78 if lateral else 0.0,
|
|
customization=document,
|
|
visual_preview=OnroadVisualPreview(cem_reason=CEM_SCENE_REASONS[scene]) if scene in CEM_SCENE_REASONS else None)
|
|
|
|
if scene == 'cem_lead':
|
|
state = replace(state, conditional_lead=ConditionalLead(12.0, 0.0))
|
|
elif scene == 'cem_curve':
|
|
state = replace(state, curve=CurveObservation(False, None, None, False, False, False, 0.0, 2.0, 0.0,
|
|
'preview', 'idle', 'none', False, False, 0.005))
|
|
observation = state.speed_limit
|
|
if scene == 'slc_max':
|
|
state = replace(state, cruise_kph=65)
|
|
observation = replace(observation, limiting_max_set=False)
|
|
elif scene == 'slc_equal':
|
|
state = replace(state, cruise_kph=79.2)
|
|
elif scene in ('slc_pedal', 'slc_retained'):
|
|
observation = replace(observation, driver_override_active=True,
|
|
override_basis='pedal' if scene == 'slc_pedal' else 'persistent',
|
|
retained_override=scene == 'slc_retained')
|
|
elif scene == 'slc_unavailable':
|
|
observation = SpeedLimitObservation(kind=ObservationKind.STALE)
|
|
elif scene == 'slc_disengaged':
|
|
observation = replace(observation, action_enabled=True, effective_cluster_target_mps=None)
|
|
elif scene == 'slc_display':
|
|
observation = replace(observation, action_enabled=False, effective_cluster_target_mps=None)
|
|
return replace(state, speed_limit=observation)
|
|
|
|
|
|
def _chunk(kind: bytes, data: bytes) -> bytes:
|
|
payload = kind + data
|
|
return struct.pack(">I", len(data)) + payload + struct.pack(">I", zlib.crc32(payload) & 0xffffffff)
|
|
|
|
|
|
def _straight_rgba(pixels: bytes) -> bytes:
|
|
"""PNG uses straight alpha; the transparent GL scratch target stores premultiplied RGB."""
|
|
values = np.frombuffer(pixels, dtype=np.uint8).reshape(-1, 4).astype(np.uint16)
|
|
alpha = values[:, 3:4]
|
|
values[:, :3] = np.where(alpha > 0, np.minimum(255, (values[:, :3] * 255 + alpha // 2) // np.maximum(alpha, 1)), 0)
|
|
return values.astype(np.uint8).tobytes()
|
|
|
|
|
|
def _png_rgba(width: int, height: int, pixels: bytes, *, widget_art: dict | None = None) -> bytes:
|
|
if len(pixels) != width * height * 4:
|
|
raise ValueError("Invalid layout preview pixel buffer")
|
|
stride = width * 4
|
|
rows = b"".join(b"\0" + pixels[y * stride:(y + 1) * stride] for y in range(height))
|
|
return (b"\x89PNG\r\n\x1a\n" + _chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0)) +
|
|
(_chunk(b"tEXt", b"starpilot-widget-art\0" + json.dumps(widget_art, separators=(",", ":"), allow_nan=False).encode())
|
|
if widget_art is not None else b"") +
|
|
_chunk(b"IDAT", zlib.compress(rows, 3)) + _chunk(b"IEND", b""))
|
|
|
|
|
|
class _DriverMonitorArt:
|
|
def __init__(self, profile: Profile):
|
|
from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
|
|
|
|
self.profile = profile
|
|
self.textures: dict[str, rl.Texture] = {}
|
|
self.renderer = DriverStateRenderer.__new__(DriverStateRenderer)
|
|
self.renderer._lines = False
|
|
self.renderer._is_active = True
|
|
self.renderer._force_active = False
|
|
self.renderer._awareness_unfull = False
|
|
self.renderer._fade_filter = FirstOrderFilter(1.0, 0.05, 1 / 60)
|
|
self.renderer._rotation_filter = FirstOrderFilter(90.0, 0.05, 1 / 60)
|
|
self.renderer._color_fade_filter = FirstOrderFilter(1.0, 0.05, 1 / 60)
|
|
directory = ASSET_DIRECTORY / "icons_mici/onroad/driver_monitoring"
|
|
try:
|
|
for name in ("dm_background", "dm_person", "dm_cone"):
|
|
image = rl.load_image(str(directory / f"{name}.png"))
|
|
try:
|
|
size = 128 if profile == Profile.LARGE else 60
|
|
if name != "dm_background":
|
|
size = round(52 / 60 * size)
|
|
rl.image_resize(image, size, size)
|
|
texture = rl.load_texture_from_image(image)
|
|
finally:
|
|
rl.unload_image(image)
|
|
if not texture.id:
|
|
raise RuntimeError(f"Unable to load driver monitoring artwork: {name}")
|
|
self.textures[name] = texture
|
|
self.renderer._dm_background = self.textures["dm_background"]
|
|
self.renderer._dm_person = self.textures["dm_person"]
|
|
self.renderer._dm_cone = self.textures["dm_cone"]
|
|
except Exception:
|
|
self.close()
|
|
raise
|
|
|
|
def render(self, _content: rl.Rectangle, state: OnroadState, *, top_icons: bool = False) -> None:
|
|
position = placement(state.customization, self.profile, "driver_monitor")
|
|
if not monitor_visible(self.profile, state, fresh=True, onroad=True, top_icons=top_icons):
|
|
return
|
|
size = 128 if self.profile == Profile.LARGE else 60
|
|
x = position["x"] + (192 - size) / 2 if self.profile == Profile.LARGE else position["x"]
|
|
y = position["y"] + (192 - size) / 2 if self.profile == Profile.LARGE else position["y"]
|
|
self.renderer._rect = rl.Rectangle(x, y, size, size)
|
|
widget = PROFILES[str(self.profile)]["widgets"]["driver_monitor"]
|
|
draw_widget_frame(rl.Rectangle(position["x"], position["y"], widget["width"], widget["height"]),
|
|
state.customization, self.profile, "driver_monitor")
|
|
self.renderer._render(self.renderer._rect)
|
|
|
|
def close(self) -> None:
|
|
if rl.is_window_ready():
|
|
for texture in self.textures.values():
|
|
rl.unload_texture(texture)
|
|
self.textures.clear()
|
|
|
|
|
|
class _Canvas:
|
|
def __init__(self, profile: Profile, viewport=None):
|
|
from openpilot.starpilot.ui.onroad import OnroadView
|
|
|
|
self.profile = profile
|
|
self.viewport = viewport
|
|
self.fonts: BitmapFonts | None = None
|
|
self.view: OnroadView | None = None
|
|
self.monitor: _DriverMonitorArt | None = None
|
|
self.target: rl.RenderTexture | None = None
|
|
self.map = None
|
|
try:
|
|
self.fonts = BitmapFonts(profile, default_font_directory())
|
|
layers = {'background_layer': self._render_background}
|
|
if viewport is None:
|
|
self.view = OnroadView(self.fonts, ASSET_DIRECTORY, background_layer=layers['background_layer'])
|
|
else:
|
|
from openpilot.starpilot.system.android_auto.projection_onroad import ProjectionOnroad
|
|
self.view = ProjectionOnroad.create_view(OnroadView, self.fonts, viewport=viewport, **layers)
|
|
from openpilot.starpilot.system.android_auto.projection_exit import ProjectionExit
|
|
self.view.projection_exit_layer = ProjectionExit(rl).render
|
|
from openpilot.starpilot.ui.onroad_map import MapOverlay, SampleTileReader
|
|
self.map = MapOverlay(reader=SampleTileReader(), fonts=self.fonts)
|
|
self.view.map_layer = self._render_map
|
|
self.monitor = _DriverMonitorArt(profile)
|
|
self.view.driver_monitor_layer = self._render_driver_monitor
|
|
self.view.preview_pip_layer = self._render_preview_pip
|
|
self.target = rl.load_render_texture(*(viewport or profile.size))
|
|
if not self.target.id:
|
|
raise RuntimeError("Unable to create layout preview target")
|
|
except Exception:
|
|
self.close()
|
|
raise
|
|
|
|
def _render_background(self, rect: rl.Rectangle, state: OnroadState) -> None:
|
|
render_sample_road(rect, state, self.profile)
|
|
if not state.customization.get("widgetOrder", {}).get(str(self.profile)) and self.viewport is None:
|
|
self._render_side_cameras(state)
|
|
|
|
@staticmethod
|
|
def _map_placement(state: OnroadState):
|
|
placed = state.customization["layouts"]["large"].get("nav_map")
|
|
return placed if placed is not None and placed["enabled"] else None
|
|
|
|
def _render_map(self, _rect: rl.Rectangle, state: OnroadState) -> None:
|
|
placed = self._map_placement(state)
|
|
if placed is not None and self.map is not None:
|
|
self.map.draw(rl.Rectangle(placed["x"], placed["y"], placed["width"], placed["height"]), placed["opacity"] / 100.0)
|
|
|
|
def _render_preview_pip(self, rect, state, *, submit=None):
|
|
if submit is not None:
|
|
for key in CAMERA_WIDGETS:
|
|
submit(key, lambda key=key: self._render_side_cameras(state, only=key))
|
|
|
|
def _render_side_cameras(self, state: OnroadState, only=None) -> None:
|
|
for key in CAMERA_WIDGETS:
|
|
if only is not None and key != only:
|
|
continue
|
|
position = placement(state.customization, self.profile, key)
|
|
if not position["enabled"]:
|
|
continue
|
|
widget = PROFILES[str(self.profile)]["widgets"][key]
|
|
radius = widget["width"] / 2
|
|
center = rl.Vector2(position["x"] + radius, position["y"] + radius)
|
|
rl.draw_circle_v(center, radius, rl.Color(21, 44, 57, 230))
|
|
rl.draw_ring(center, radius - 3, radius, 0, 360, 64, rl.Color(94, 229, 238, 255))
|
|
|
|
def _render_driver_monitor(self, rect: rl.Rectangle, state: OnroadState) -> None:
|
|
top_icons = (self.profile == Profile.COMPACT and state.alert.size == AlertSize.NONE and
|
|
state.appearance.camera_view != CameraViewChoice.DRIVER and not state.reverse_driver_camera and
|
|
self.view.compact_hud._set_speed_alpha.x > 0.01 and
|
|
placement(state.customization, self.profile, "max_speed")["enabled"])
|
|
if self.viewport is None:
|
|
self.monitor.render(rect, state, top_icons=top_icons)
|
|
else:
|
|
rl.rl_push_matrix()
|
|
rl.rl_translatef(0, self.viewport[1] - 1080, 0)
|
|
try:
|
|
self.monitor.render(rect, state, top_icons=top_icons)
|
|
finally:
|
|
rl.rl_pop_matrix()
|
|
|
|
def render(self, state: OnroadState, scene: str) -> bytes:
|
|
if self.view is None or self.fonts is None or self.target is None:
|
|
raise RuntimeError("Layout preview resources are closed")
|
|
self._settle(state)
|
|
width, height = self.viewport or self.profile.size
|
|
placed = self._map_placement(state) if self.map is not None else None
|
|
if placed is not None:
|
|
from openpilot.starpilot.ui.onroad_map import sample_input
|
|
self.map.prepare(sample_input(), placed["width"], placed["height"]) # offscreen, before the preview target
|
|
rl.begin_texture_mode(self.target)
|
|
try:
|
|
rl.clear_background(rl.BLACK)
|
|
self.view.render(state)
|
|
if self.profile == Profile.LARGE and self.viewport is None:
|
|
render_sidebar(self.fonts, rl.Rectangle(width - SIDEBAR_WIDTH, 0, SIDEBAR_WIDTH, height))
|
|
self._label(scene)
|
|
finally:
|
|
rl.end_texture_mode()
|
|
image = rl.load_image_from_texture(self.target.texture)
|
|
try:
|
|
rl.image_format(image, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8)
|
|
rl.image_flip_vertical(image)
|
|
scale = min(1.0, 2560 / width, 1440 / height)
|
|
if scale < 1.0:
|
|
width, height = round(width * scale), round(height * scale)
|
|
rl.image_resize(image, width, height)
|
|
return _png_rgba(width, height, bytes(rl.ffi.buffer(image.data, width * height * 4)))
|
|
finally:
|
|
rl.unload_image(image)
|
|
|
|
def render_widget_art(self, state: OnroadState, positions: dict) -> bytes:
|
|
"""Transparent arrangement art from the same widgets/fonts as the live view."""
|
|
if self.view is None or self.fonts is None or self.target is None:
|
|
raise RuntimeError("Layout preview resources are closed")
|
|
self._settle(state)
|
|
canvas_width, canvas_height = self.viewport or self.profile.size
|
|
content = (rl.Rectangle(30, 30, canvas_width - 60, canvas_height - 60) if self.profile == Profile.LARGE
|
|
else rl.Rectangle(0, 0, 476, 240))
|
|
sprites = {}
|
|
# A bounded subset covers the edited widgets without adding an onroad render loop.
|
|
x = y = row_height = 0
|
|
captures = []
|
|
for key in RUNTIME_PREVIEW_WIDGETS[str(self.profile)]:
|
|
logical_width, logical_height = widget_size(state.customization, self.profile, key)
|
|
position = positions[key]
|
|
crop_x, crop_y = math.floor(position['x']), math.floor(position['y'])
|
|
offset_x, offset_y = crop_x - position['x'], crop_y - position['y']
|
|
width, height = math.ceil(logical_width - offset_x), math.ceil(logical_height - offset_y)
|
|
if x + width > 1024:
|
|
x, y, row_height = 0, y + row_height, 0
|
|
sprites[key] = {'x': x, 'y': y, 'width': width, 'height': height, 'offsetX': offset_x, 'offsetY': offset_y}
|
|
x += width
|
|
row_height = max(row_height, height)
|
|
if not (0 <= crop_x <= canvas_width - width and 0 <= crop_y <= canvas_height - height):
|
|
raise ValueError("Widget art crop lies outside the canvas")
|
|
rl.begin_texture_mode(self.target)
|
|
try:
|
|
rl.clear_background(rl.BLANK)
|
|
# Match the runtime's transparent cache coverage before exporting straight-alpha PNG.
|
|
rl.rl.rlSetBlendFactorsSeparate(0x0302, 0x0303, 1, 0x0303, 0x8006, 0x8006)
|
|
rl.begin_blend_mode(rl.BlendMode.BLEND_CUSTOM_SEPARATE)
|
|
try:
|
|
self.view._ordered_widgets(content, state, only_widget=key)
|
|
finally:
|
|
rl.end_blend_mode()
|
|
finally:
|
|
rl.end_texture_mode()
|
|
image = rl.load_image_from_texture(self.target.texture)
|
|
try:
|
|
rl.image_format(image, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8)
|
|
rl.image_flip_vertical(image)
|
|
rl.image_crop(image, rl.Rectangle(crop_x, crop_y, width, height))
|
|
captures.append((key, _straight_rgba(bytes(rl.ffi.buffer(image.data, width * height * 4)))))
|
|
finally:
|
|
rl.unload_image(image)
|
|
sheet_width = max(slot['x'] + slot['width'] for slot in sprites.values())
|
|
sheet_height = y + row_height
|
|
if not (0 < sheet_width <= 1024 and 0 < sheet_height <= 1440):
|
|
raise ValueError("Widget art sheet exceeds preview bounds")
|
|
pixels = bytearray(sheet_width * sheet_height * 4)
|
|
for key, data in captures:
|
|
slot = sprites[key]
|
|
for row in range(slot['height']):
|
|
start = ((slot['y'] + row) * sheet_width + slot['x']) * 4
|
|
pixels[start:start + slot['width'] * 4] = data[row * slot['width'] * 4:(row + 1) * slot['width'] * 4]
|
|
return _png_rgba(sheet_width, sheet_height, bytes(pixels),
|
|
widget_art={'version': 1, 'width': sheet_width, 'height': sheet_height, 'sprites': sprites})
|
|
|
|
def _settle(self, state: OnroadState) -> None:
|
|
if self.view is None:
|
|
return
|
|
if self.profile == Profile.COMPACT:
|
|
hud = self.view.compact_hud
|
|
# Freeze the synthetic sample inside the normal post-change set-speed blink.
|
|
# Otherwise the first frame/reset or an old cached scene can hide its glyphs.
|
|
now_ns = time.monotonic_ns()
|
|
hud._cruise_drive_frame = state.drive_frame
|
|
hud._last_cruise_display = (round(state.cruise_kph * (1 if state.metric else .621371))
|
|
if state.cruise_active and state.cruise_kph is not None else None)
|
|
hud._last_cruise_ns = now_ns if state.cruise_active else None
|
|
hud._set_speed_changed_ns = now_ns - 1_000_000_000 if state.cruise_active else None
|
|
hud._was_cruise_active = state.cruise_active
|
|
hud._set_speed_alpha.x = 1.0 if state.cruise_active else 0.0
|
|
hud._wheel_alpha.x = 255 * 0.9 if state.lateral_active else 0.0
|
|
hud._wheel_y.x = 0.0 if state.lateral_active else 25.0
|
|
self.view.torque_bar._torque_filter.x = state.torque_utilization if state.lateral_active else 0.0
|
|
self.view.torque_bar._alpha_filter.x = 1.0 if state.lateral_active else 0.0
|
|
|
|
def _label(self, scene: str) -> None:
|
|
if self.fonts is None:
|
|
return
|
|
large = self.profile == Profile.LARGE
|
|
names = {"experimental": "MANUAL EXPERIMENTAL", "cem_stop_light": "CEM TRAFFIC LIGHT",
|
|
"cem_lead": "CEM STOPPED LEAD", "cem_curve": "CEM CURVE"}
|
|
label = "SAMPLE: " + names.get(scene, scene.replace("_", " ").upper())
|
|
size = 27 if large else 14
|
|
measured = self.fonts.measure(label, FontRole.SEMI_BOLD, size)
|
|
x = (1860 if large else 476) - measured.width - (38 if large else 8)
|
|
y = 1000 if large else 213
|
|
rl.draw_rectangle_rounded(rl.Rectangle(x - 8, y - 3, measured.width + 16, measured.height + 6),
|
|
0.2, 4, rl.Color(0, 0, 0, 220))
|
|
self.fonts.draw(label, FontRole.SEMI_BOLD, size, x, y, rl.Color(255, 255, 255, 255))
|
|
|
|
def close(self) -> None:
|
|
if self.map is not None:
|
|
self.map.close()
|
|
self.map = None
|
|
if self.target is not None and rl.is_window_ready():
|
|
rl.unload_render_texture(self.target)
|
|
self.target = None
|
|
if self.monitor is not None:
|
|
self.monitor.close()
|
|
self.monitor = None
|
|
if self.view is not None:
|
|
self.view.close()
|
|
self.view = None
|
|
if self.fonts is not None:
|
|
self.fonts.close()
|
|
self.fonts = None
|
|
|
|
|
|
class LayoutPreviewRenderer:
|
|
def __init__(self):
|
|
self._canvas: _Canvas | None = None
|
|
self._last_use = 0.0
|
|
|
|
@property
|
|
def has_resources(self) -> bool:
|
|
return self._canvas is not None
|
|
|
|
def __call__(self, payload: dict) -> bytes:
|
|
if type(payload) is not dict or set(payload) not in ({"document", "profile", "scene"}, {"document", "profile", "scene", "art"}):
|
|
raise ValueError("Invalid layout preview request")
|
|
if "art" in payload and payload["art"] is not True:
|
|
raise ValueError("Invalid widget art request")
|
|
viewport = None
|
|
if payload['profile'] == 'projection':
|
|
from openpilot.starpilot.system.android_auto.projection_layout import projection_customization
|
|
layout = payload['document']['layout']
|
|
viewport = (layout['canvas']['width'], layout['canvas']['height'])
|
|
document = projection_customization(layout, validate_document(payload['document']['base']))
|
|
else:
|
|
document = validate_document(payload['document'])
|
|
if type(payload["profile"]) is not str or payload["profile"] not in ("large", "compact", "projection"):
|
|
raise ValueError("Invalid layout preview profile")
|
|
profile = Profile.LARGE if viewport else Profile(payload["profile"])
|
|
scene = payload["scene"]
|
|
state = sample_state(scene, document)
|
|
if viewport:
|
|
from openpilot.starpilot.ui.navigation_state import NavigationDisplay
|
|
state = replace(state, navigation=NavigationDisplay(("preview", 1), "Turn left onto Desert Inn Road", "turn", "left",
|
|
152.0, 6800.0, 540.0))
|
|
if not rl.is_window_ready():
|
|
raise RuntimeError("Layout preview requires the UI graphics context")
|
|
if self._canvas is None or self._canvas.profile != profile or self._canvas.viewport != viewport:
|
|
self.close()
|
|
self._canvas = _Canvas(profile, viewport)
|
|
self._last_use = time.monotonic()
|
|
if payload.get('art') is True:
|
|
positions = payload['document']['layout']['widgets'] if viewport else document['layouts'][str(profile)]
|
|
return self._canvas.render_widget_art(state, positions)
|
|
return self._canvas.render(state, scene)
|
|
|
|
def expire(self) -> None:
|
|
if self._canvas is not None and time.monotonic() - self._last_use >= IDLE_SECONDS:
|
|
self.close()
|
|
|
|
def close(self) -> None:
|
|
if self._canvas is not None:
|
|
self._canvas.close()
|
|
self._canvas = None
|