mirror of
https://github.com/l1npengtul/nokhwa.git
synced 2026-07-04 02:27:26 +00:00
new format request type, quartar way wasm cam impl
This commit is contained in:
@@ -1,230 +0,0 @@
|
||||
use crate::frame_format::SourceFrameFormat;
|
||||
use crate::{
|
||||
frame_format::FrameFormat,
|
||||
types::{ApiBackend, CameraFormat, Resolution},
|
||||
};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
/// Tells the init function what camera format to pick.
|
||||
/// - `AbsoluteHighestResolution`: Pick the highest [`Resolution`], then pick the highest frame rate of those provided.
|
||||
/// - `AbsoluteHighestFrameRate`: Pick the highest frame rate, then the highest [`Resolution`].
|
||||
/// - `HighestResolution(Resolution)`: Pick the highest [`Resolution`] for the given framerate.
|
||||
/// - `HighestFrameRate(u32)`: Pick the highest frame rate for the given [`Resolution`].
|
||||
/// - `Exact`: Pick the exact [`CameraFormat`] provided.
|
||||
/// - `Closest`: Pick the closest [`CameraFormat`] provided in order of [`Resolution`], and FPS.
|
||||
/// - `ClosestGreater`: Pick the closest [`CameraFormat`] provided in order of [`Resolution`], and FPS. The returned format's [`Resolution`] **and** FPS will be **greater than or equal to** the provided [`CameraFormat`]
|
||||
/// - `ClosestLess`: Pick the closest [`CameraFormat`] provided in order of [`Resolution`], and FPS.The returned format's [`Resolution`] **and** FPS will be **less than or equal to** the provided [`CameraFormat`]
|
||||
/// - `None`: Pick a random [`CameraFormat`]
|
||||
#[derive(Copy, Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum RequestedFormatType {
|
||||
AbsoluteHighestResolution,
|
||||
AbsoluteHighestFrameRate,
|
||||
HighestResolution(u32),
|
||||
HighestFrameRate(Resolution),
|
||||
Exact(CameraFormat),
|
||||
ClosestGreater(CameraFormat),
|
||||
ClosestLess(CameraFormat),
|
||||
Closest(CameraFormat),
|
||||
None,
|
||||
}
|
||||
|
||||
// TODO: Format Filter Builder to provide more interactive API for fulfillment of formats.
|
||||
|
||||
/// How you get your [`FrameFormat`] from the
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FormatFilter {
|
||||
filter_pref: RequestedFormatType,
|
||||
fcc_primary: BTreeSet<FrameFormat>,
|
||||
fcc_platform: BTreeMap<ApiBackend, BTreeSet<u128>>,
|
||||
|
||||
}
|
||||
|
||||
impl FormatFilter {
|
||||
pub fn new(fmt_type: RequestedFormatType) -> Self {
|
||||
Self {
|
||||
filter_pref: fmt_type,
|
||||
fcc_primary: Default::default(),
|
||||
fcc_platform: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_allowed_frame_format(&mut self, frame_format: FrameFormat) {
|
||||
self.fcc_primary.insert(frame_format);
|
||||
}
|
||||
|
||||
pub fn add_allowed_frame_format_many(&mut self, frame_formats: impl AsRef<[FrameFormat]>) {
|
||||
self.fcc_primary.extend(frame_formats.as_ref().iter());
|
||||
}
|
||||
|
||||
pub fn add_allowed_platform_specific(&mut self, platform: ApiBackend, frame_format: u128) {
|
||||
match self.fcc_platform.get_mut(&platform) {
|
||||
Some(fccs) => {
|
||||
fccs.insert(frame_format);
|
||||
}
|
||||
None => {
|
||||
self.fcc_platform
|
||||
.insert(platform, BTreeSet::from([frame_format]));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn add_allowed_platform_specific_many(
|
||||
&mut self,
|
||||
platform_specifics: impl AsRef<[(ApiBackend, u128)]>,
|
||||
) {
|
||||
for (platform, frame_format) in platform_specifics.as_ref().into_iter() {
|
||||
match self.fcc_platform.get_mut(&platform) {
|
||||
Some(fccs) => {
|
||||
fccs.insert(*frame_format);
|
||||
}
|
||||
None => {
|
||||
self.fcc_platform
|
||||
.insert(*platform, BTreeSet::from([*frame_format]));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_allowed_frame_format(mut self, frame_format: FrameFormat) -> Self {
|
||||
self.fcc_primary.insert(frame_format);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_allowed_frame_format_many(
|
||||
mut self,
|
||||
frame_formats: impl AsRef<[FrameFormat]>,
|
||||
) -> Self {
|
||||
self.fcc_primary.extend(frame_formats.as_ref().iter());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_allowed_platform_specific(
|
||||
mut self,
|
||||
platform: ApiBackend,
|
||||
frame_format: u128,
|
||||
) -> Self {
|
||||
self.add_allowed_platform_specific(platform, frame_format);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_allowed_platform_specific_many(
|
||||
mut self,
|
||||
platform_specifics: impl AsRef<[(ApiBackend, u128)]>,
|
||||
) -> Self {
|
||||
self.add_allowed_platform_specific_many(platform_specifics);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FormatFilter {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
filter_pref: RequestedFormatType::Closest(CameraFormat::new(
|
||||
Resolution::new(640, 480),
|
||||
FrameFormat::Yuv422.into(),
|
||||
30,
|
||||
)),
|
||||
fcc_primary: BTreeSet::from([FrameFormat::Yuv422]),
|
||||
fcc_platform: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_fulfill(
|
||||
sources: impl AsRef<[CameraFormat]>,
|
||||
filter: FormatFilter,
|
||||
) -> Option<CameraFormat> {
|
||||
let mut sources = sources
|
||||
.as_ref()
|
||||
.into_iter()
|
||||
.filter(|cam_filter| match cam_filter.format() {
|
||||
SourceFrameFormat::FrameFormat(fmt) => filter.fcc_primary.contains(&fmt),
|
||||
SourceFrameFormat::PlatformSpecific(plat) => filter
|
||||
.fcc_platform
|
||||
.get(&plat.backend())
|
||||
.map(|x| x.contains(&plat.format()))
|
||||
.unwrap_or(false),
|
||||
});
|
||||
|
||||
match filter.filter_pref {
|
||||
RequestedFormatType::AbsoluteHighestResolution => {
|
||||
let mut sources = sources.collect::<Vec<&CameraFormat>>();
|
||||
sources.sort_by(|a, b| a.resolution().cmp(&b.resolution()));
|
||||
sources.last().copied().copied()
|
||||
}
|
||||
RequestedFormatType::AbsoluteHighestFrameRate => {
|
||||
let mut sources = sources.collect::<Vec<&CameraFormat>>();
|
||||
sources.sort_by(|a, b| a.frame_rate().cmp(&b.frame_rate()));
|
||||
sources.last().copied().copied()
|
||||
}
|
||||
RequestedFormatType::HighestResolution(filter_fps) => {
|
||||
let mut sources = sources
|
||||
.filter(|format| format.frame_rate() == filter_fps)
|
||||
.collect::<Vec<&CameraFormat>>();
|
||||
sources.sort();
|
||||
sources.last().copied().copied()
|
||||
}
|
||||
RequestedFormatType::HighestFrameRate(filter_res) => {
|
||||
let mut sources = sources
|
||||
.filter(|format| format.resolution() == filter_res)
|
||||
.collect::<Vec<&CameraFormat>>();
|
||||
sources.sort();
|
||||
sources.last().copied().copied()
|
||||
}
|
||||
RequestedFormatType::Exact(exact) => {
|
||||
sources.filter(|format| format == &&exact).last().copied()
|
||||
}
|
||||
RequestedFormatType::Closest(closest) => {
|
||||
let mut sources = sources
|
||||
.map(|format| {
|
||||
let dist = distance_3d_camerafmt_relative(closest, *format);
|
||||
(dist, *format)
|
||||
})
|
||||
.collect::<Vec<(f64, CameraFormat)>>();
|
||||
sources.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
sources.first().copied().map(|(_, cf)| cf)
|
||||
}
|
||||
RequestedFormatType::ClosestGreater(closest) => {
|
||||
let mut sources = sources
|
||||
.filter(|format| {
|
||||
format.resolution() >= closest.resolution()
|
||||
&& format.frame_rate() >= closest.frame_rate()
|
||||
})
|
||||
.map(|format| {
|
||||
let dist = distance_3d_camerafmt_relative(closest, *format);
|
||||
(dist, *format)
|
||||
})
|
||||
.collect::<Vec<(f64, CameraFormat)>>();
|
||||
sources.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
sources.first().copied().map(|(_, cf)| cf)
|
||||
}
|
||||
RequestedFormatType::ClosestLess(closest) => {
|
||||
let mut sources = sources
|
||||
.filter(|format| {
|
||||
format.resolution() <= closest.resolution()
|
||||
&& format.frame_rate() <= closest.frame_rate()
|
||||
})
|
||||
.map(|format| {
|
||||
let dist = distance_3d_camerafmt_relative(closest, *format);
|
||||
(dist, *format)
|
||||
})
|
||||
.collect::<Vec<(f64, CameraFormat)>>();
|
||||
sources.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
sources.first().copied().map(|(_, cf)| cf)
|
||||
}
|
||||
RequestedFormatType::None => sources.nth(0).map(|x| *x),
|
||||
}
|
||||
}
|
||||
|
||||
fn distance_3d_camerafmt_relative(a: CameraFormat, b: CameraFormat) -> f64 {
|
||||
let res_x_diff = b.resolution().x() - a.resolution().x();
|
||||
let res_y_diff = b.resolution().y() - a.resolution().y();
|
||||
let fps_diff = b.frame_rate() - a.frame_rate();
|
||||
|
||||
let x = res_x_diff.pow(2) as f64;
|
||||
let y = res_y_diff.pow(2) as f64;
|
||||
let z = fps_diff.pow(2) as f64;
|
||||
|
||||
x + y + z
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
use crate::frame_format::SourceFrameFormat;
|
||||
use crate::types::Range;
|
||||
use crate::{
|
||||
frame_format::FrameFormat,
|
||||
types::{ApiBackend, CameraFormat, Resolution},
|
||||
};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
#[derive(Copy, Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq)]
|
||||
pub enum CustomFormatRequestType {
|
||||
HighestFPS,
|
||||
HighestResolution,
|
||||
Closest,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialOrd, PartialEq)]
|
||||
pub struct FormatRequest {
|
||||
resolution: Option<Range<Resolution>>,
|
||||
frame_rate: Option<Range<u32>>,
|
||||
frame_format: Option<Vec<FrameFormat>>,
|
||||
req_type: Option<CustomFormatRequestType>,
|
||||
}
|
||||
|
||||
impl FormatRequest {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn with_resolution(mut self, resolution: Resolution, exact: bool) -> Self {
|
||||
self.resolution = Some(resolution);
|
||||
self.resolution_exact = exact;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn reset_resolution(mut self) -> Self {
|
||||
self.resolution = None;
|
||||
self.resolution_exact = false;
|
||||
self
|
||||
}
|
||||
pub fn with_frame_rate(mut self, frame_rate: u32, exact: bool) -> Self {
|
||||
self.frame_rate = Some(frame_rate);
|
||||
self.frame_rate_exact = exact;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_standard_frame_rate() {}
|
||||
|
||||
pub fn reset_frame_rate(mut self) -> Self {
|
||||
self.frame_rate = None;
|
||||
self.frame_rate_exact = false;
|
||||
self
|
||||
}
|
||||
pub fn with_frame_formats(mut self, frame_formats: Vec<FrameFormat>) -> Self {
|
||||
self.frame_format = Some(frame_formats);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_standard_frame_formats(mut self) -> Self {
|
||||
self.append_frame_formats(&mut vec![
|
||||
FrameFormat::MJpeg,
|
||||
FrameFormat::Rgb8,
|
||||
FrameFormat::Yuv422,
|
||||
FrameFormat::Nv12,
|
||||
])
|
||||
}
|
||||
|
||||
pub fn push_frame_format(mut self, frame_format: FrameFormat) -> Self {
|
||||
match &mut self.frame_format {
|
||||
Some(ffs) => ffs.push(frame_format),
|
||||
None => self.frame_format = Some(vec![frame_format]),
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn remove_frame_format(mut self, frame_format: FrameFormat) -> Self {
|
||||
if let Some(ffs) = &mut self.frame_format {
|
||||
if let Some(idx) = ffs.iter().position(frame_format) {
|
||||
ffs.remove(idx)
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn append_frame_formats(mut self, frame_formats: &mut Vec<FrameFormat>) -> Self {
|
||||
match &mut self.frame_format {
|
||||
Some(ffs) => ffs.append(frame_formats),
|
||||
None => self.frame_format = Some(frame_formats.clone()),
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn reset_frame_formats(mut self) -> Self {
|
||||
self.frame_format = None;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_request_type(mut self, request_type: CustomFormatRequestType) -> Self {
|
||||
self.req_type = Some(request_type);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn reset_request_type(mut self) -> Self {
|
||||
self.req_type = None;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_format_request(
|
||||
request: FormatRequest,
|
||||
availible_formats: Vec<CameraFormat>,
|
||||
) -> CameraFormat {
|
||||
// filter out by
|
||||
}
|
||||
@@ -21,8 +21,9 @@
|
||||
//! Core type definitions for `nokhwa`
|
||||
pub mod buffer;
|
||||
pub mod error;
|
||||
pub mod format_filter;
|
||||
pub mod format_request;
|
||||
pub mod frame_format;
|
||||
pub mod traits;
|
||||
pub mod types;
|
||||
pub mod decoder;
|
||||
pub mod utils;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
error::NokhwaError,
|
||||
format_filter::FormatFilter,
|
||||
format_request::FormatFilter,
|
||||
frame_format::SourceFrameFormat,
|
||||
types::{
|
||||
ApiBackend, CameraControl, CameraFormat, CameraInfo, ControlValueSetter,
|
||||
@@ -315,17 +315,6 @@ pub trait AsyncCaptureTrait: CaptureTrait {
|
||||
fourcc: SourceFrameFormat,
|
||||
) -> Result<(), NokhwaError>;
|
||||
|
||||
/// Gets the value of [`KnownCameraControl`].
|
||||
/// # Errors
|
||||
/// If the `control` is not supported or there is an error while getting the camera control values (e.g. unexpected value, too high, etc)
|
||||
/// this will error.
|
||||
async fn camera_control_async(&self, control: KnownCameraControl) -> Result<CameraControl, NokhwaError>;
|
||||
|
||||
/// Gets the current supported list of [`KnownCameraControl`]
|
||||
/// # Errors
|
||||
/// If the list cannot be collected, this will error. This can be treated as a "nothing supported".
|
||||
async fn camera_controls_async(&self) -> Result<Vec<CameraControl>, NokhwaError>;
|
||||
|
||||
/// Sets the control to `control` in the camera.
|
||||
/// Usually, the pipeline is calling [`camera_control()`](CaptureTrait::camera_control), getting a camera control that way
|
||||
/// then calling [`value()`](CameraControl::value()) to get a [`ControlValueSetter`] and setting the value that way.
|
||||
|
||||
+93
-16
@@ -1,25 +1,101 @@
|
||||
use crate::{
|
||||
error::NokhwaError,
|
||||
format_filter::RequestedFormatType,
|
||||
frame_format::{FrameFormat, SourceFrameFormat},
|
||||
};
|
||||
#[cfg(feature = "serialize")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Debug;
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
cmp::Ordering,
|
||||
fmt::{Display, Formatter},
|
||||
};
|
||||
|
||||
impl Default for RequestedFormatType {
|
||||
fn default() -> Self {
|
||||
RequestedFormatType::None
|
||||
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
|
||||
pub struct Range<T>
|
||||
where
|
||||
T: Copy + Clone + Debug + PartialOrd + PartialEq,
|
||||
{
|
||||
minimum: Option<T>,
|
||||
lower_inclusive: bool,
|
||||
maximum: Option<T>,
|
||||
upper_inclusive: bool,
|
||||
preferred: T,
|
||||
}
|
||||
|
||||
impl<T> Range<T>
|
||||
where
|
||||
T: Copy + Clone + Debug + PartialOrd + PartialEq,
|
||||
{
|
||||
pub fn new(preferred: T, min: Option<T>, max: Option<T>) -> Self {
|
||||
Self {
|
||||
minimum: min,
|
||||
lower_inclusive: true,
|
||||
maximum: max,
|
||||
upper_inclusive: false,
|
||||
preferred,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_inclusive(
|
||||
preferred: T,
|
||||
min: Option<T>,
|
||||
lower_inclusive: bool,
|
||||
max: Option<T>,
|
||||
upper_inclusive: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
minimum: min,
|
||||
lower_inclusive,
|
||||
maximum: max,
|
||||
upper_inclusive,
|
||||
preferred,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn does_fit(&self, item: T) -> bool {
|
||||
if item == self.preferred {
|
||||
true
|
||||
}
|
||||
|
||||
if let Some(min) = self.minimum {
|
||||
let test = if self.lower_inclusive {
|
||||
min >= item
|
||||
} else {
|
||||
min > item
|
||||
};
|
||||
if test {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(max) = self.maximum {
|
||||
let test = if self.lower_inclusive {
|
||||
max <= item
|
||||
} else {
|
||||
max < item
|
||||
};
|
||||
if test {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for RequestedFormatType {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{self:?}")
|
||||
impl<T> Default for Range<T>
|
||||
where
|
||||
T: Default,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Range {
|
||||
minimum: None,
|
||||
lower_inclusive: true,
|
||||
maximum: None,
|
||||
upper_inclusive: false,
|
||||
preferred: T::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,7 +384,7 @@ impl CameraInfo {
|
||||
// OK, i just checkeed back on this code. WTF was I on when I wrote `&(impl AsRef<str> + ?Sized)` ????
|
||||
// I need to get on the same shit that my previous self was on, because holy shit that stuff is strong as FUCK!
|
||||
// Finally fixed this insanity. Hopefully I didnt torment anyone by actually putting this in a stable release.
|
||||
pub fn new(human_name: &str, description: &str, misc: &str, index: CameraIndex) -> Self {
|
||||
pub fn new(human_name: &str, description: &str, misc: &str, index: &CameraIndex) -> Self {
|
||||
CameraInfo {
|
||||
human_name: human_name.to_string(),
|
||||
description: description.to_string(),
|
||||
@@ -563,8 +639,8 @@ pub enum ControlValueDescription {
|
||||
},
|
||||
StringList {
|
||||
value: String,
|
||||
availible: Vec<String>
|
||||
}
|
||||
availible: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ControlValueDescription {
|
||||
@@ -598,7 +674,9 @@ impl ControlValueDescription {
|
||||
ControlValueDescription::RGB { value, .. } => {
|
||||
ControlValueSetter::RGB(value.0, value.1, value.2)
|
||||
}
|
||||
ControlValueDescription::StringList { value, .. } => ControlValueSetter::StringList(value.clone()),
|
||||
ControlValueDescription::StringList { value, .. } => {
|
||||
ControlValueSetter::StringList(value.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -696,7 +774,7 @@ impl ControlValueDescription {
|
||||
},
|
||||
ControlValueDescription::StringList { value, availible } => {
|
||||
availible.contains(setter.as_str())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// match setter {
|
||||
@@ -864,7 +942,7 @@ impl Display for ControlValueDescription {
|
||||
}
|
||||
ControlValueDescription::StringList { value, availible } => {
|
||||
write!(f, "Current: {value}, Availible: {availible:?}")
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1113,7 +1191,7 @@ impl Display for ControlValueSetter {
|
||||
}
|
||||
ControlValueSetter::StringList(s) => {
|
||||
write!(f, "StringListValue: {s}")
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1346,7 +1424,7 @@ pub fn yuyv422_to_rgb(data: &[u8], rgba: bool) -> Result<Vec<u8>, NokhwaError> {
|
||||
/// If the stream is invalid Yuv422, or the destination buffer is not large enough, this will error.
|
||||
#[inline]
|
||||
pub fn buf_yuyv422_to_rgb(data: &[u8], dest: &mut [u8], rgba: bool) -> Result<(), NokhwaError> {
|
||||
let mut buf:Vec<u8> = Vec::new();
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
if data.len() % 4 != 0 {
|
||||
return Err(NokhwaError::ProcessFrameError {
|
||||
src: FrameFormat::Yuv422.into(),
|
||||
@@ -1381,7 +1459,6 @@ pub fn buf_yuyv422_to_rgb(data: &[u8], dest: &mut [u8], rgba: bool) -> Result<()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
// equation from https://en.wikipedia.org/wiki/YUV#Converting_between_Y%E2%80%B2UV_and_RGB
|
||||
/// Convert `YCbCr` 4:4:4 to a RGB888. [For further reading](https://en.wikipedia.org/wiki/YUV#Converting_between_Y%E2%80%B2UV_and_RGB)
|
||||
#[allow(clippy::many_single_char_names)]
|
||||
|
||||
Reference in New Issue
Block a user