Files
StarPilot/openpilot/starpilot/ui/settings_assets.py
T
firestar5683 a82935f326 User interfaces and Android Auto
Integrate onroad views, configurable widgets, local settings, steering-profile contracts, Bluetooth and Android Auto with their device and runtime services.

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
Co-authored-by: Daniel Koepping <elkoled@gmail.com>
Co-authored-by: Danny <dannyirwin31@yahoo.com>
Co-authored-by: DimitriChrysafis <Dimitri.chrysafis@gmail.com>
Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com>
Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com>
Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu>
Co-authored-by: Shane Smiskol <shane@smiskol.com>
Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com>
Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com>
Co-authored-by: stef <19478336+stefpi@users.noreply.github.com>
Co-authored-by: whoisdomi <dcantu83@gmail.com>

Co-authored-by: Intelli <contact@intelli.software>
Co-authored-by: pharmacomaniac <blittle65@gmail.com>

Co-authored-by: prabhaavp <prabhaav@msu.edu>
2026-10-14 20:02:39 -05:00

114 lines
4.9 KiB
Python

"""Bounded native resource ownership for the Settings development presentation."""
import hashlib
import json
import math
from pathlib import Path
import pyray as rl
from openpilot.starpilot.ui.settings_icons import draw_icon_geometry
class SettingsAssets:
def __init__(self, directory: Path):
self.directory = directory
manifest = json.loads(Path(__file__).with_name("settings-assets.json").read_text())
self._manifest = {entry["file"]: entry for entry in manifest["files"]}
self._images: dict[tuple, rl.Texture] = {}
self._icons: dict[tuple, rl.RenderTexture] = {}
self._pixel_scale = (max(1, rl.get_render_width() / max(1, rl.get_screen_width())),
max(1, rl.get_render_height() / max(1, rl.get_screen_height())))
def image(self, filename: str, width: int, height: int) -> rl.Texture:
key = filename, width, height
if key in self._images:
return self._images[key]
if filename not in self._manifest or not 0 < width <= 4096 or not 0 < height <= 4096:
raise ValueError("Unreviewed Settings image or dimensions")
record = self._manifest[filename]
path = self.directory / filename
with path.open("rb") as stream:
data = stream.read(record["bytes"] + 1)
if len(data) != record["bytes"] or hashlib.sha256(data).hexdigest() != record["sha256"]:
raise ValueError(f"Settings image differs from the reviewed manifest: {filename}")
image = rl.load_image(str(path))
texture = None
try:
if image.data == rl.ffi.NULL or not 0 < image.width <= 4096 or not 0 < image.height <= 4096:
raise ValueError(f"Invalid Settings image: {filename}")
logical_ratio = min(width / image.width, height / image.height)
logical_width, logical_height = round(image.width * logical_ratio), round(image.height * logical_ratio)
ratio = min(min(int(width * self._pixel_scale[0]), image.width) / image.width,
min(int(height * self._pixel_scale[1]), image.height) / image.height)
if ratio != 1:
rl.image_resize(image, int(image.width * ratio), int(image.height * ratio))
texture = rl.load_texture_from_image(image)
if texture.id == 0:
raise RuntimeError(f"Unable to load Settings texture: {filename}")
rl.set_texture_filter(texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR)
rl.set_texture_wrap(texture, rl.TextureWrap.TEXTURE_WRAP_CLAMP)
texture.width, texture.height = logical_width, logical_height
self._images[key] = texture
except BaseException:
if texture is not None and texture.id:
rl.unload_texture(texture)
raise
finally:
if image.data != rl.ffi.NULL:
rl.unload_image(image)
return texture
def prepare_icon(self, name: str, scale: float, color: rl.Color) -> None:
"""Create vector cache between frames; render-texture modes cannot nest."""
if name not in ("sound", "aicar", "steering", "system", "display", "vehicle") or not 0 < scale <= 3:
raise ValueError("Unreviewed Settings vector icon")
key = name, scale, color.r, color.g, color.b, color.a
if key in self._icons:
return
padding = max(1, math.ceil(3 * scale))
size = max(1, round(60 * scale)) + 2 * padding
target = rl.load_render_texture(size, size)
try:
if not target.id or not target.texture.id:
raise RuntimeError("Unable to create Settings vector cache")
rl.begin_texture_mode(target)
try:
rl.clear_background(rl.BLANK)
rl.rl_set_blend_factors_separate(rl.RL_SRC_ALPHA, rl.RL_ONE_MINUS_SRC_ALPHA,
rl.RL_ONE, rl.RL_ONE_MINUS_SRC_ALPHA, rl.RL_FUNC_ADD, rl.RL_FUNC_ADD)
rl.begin_blend_mode(rl.BlendMode.BLEND_CUSTOM_SEPARATE)
try:
draw_icon_geometry(name, padding, padding, scale, color)
finally:
rl.end_blend_mode()
finally:
rl.end_texture_mode()
rl.set_texture_filter(target.texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR)
rl.set_texture_wrap(target.texture, rl.TextureWrap.TEXTURE_WRAP_CLAMP)
except BaseException:
if target.id:
rl.unload_render_texture(target)
raise
self._icons[key] = target
def icon(self, name: str, x: float, y: float, scale: float, color: rl.Color) -> None:
target = self._icons[(name, scale, color.r, color.g, color.b, color.a)]
padding = max(1, math.ceil(3 * scale))
width, height = target.texture.width, target.texture.height
rl.begin_blend_mode(rl.BlendMode.BLEND_ALPHA_PREMULTIPLY)
try:
rl.draw_texture_pro(target.texture, rl.Rectangle(0, 0, width, -height),
rl.Rectangle(x - padding, y - padding, width, height), rl.Vector2(0, 0), 0, rl.WHITE)
finally:
rl.end_blend_mode()
def close(self) -> None:
if rl.is_window_ready():
for image in self._images.values():
rl.unload_texture(image)
for icon in self._icons.values():
rl.unload_render_texture(icon)
self._images.clear()
self._icons.clear()