Implement vector color and ray compatibility

This commit is contained in:
2026-08-08 22:28:48 +00:00
parent 28a6f54f7e
commit 3068e5ee9e
13 changed files with 2724 additions and 1835 deletions

View File

@@ -0,0 +1,429 @@
//! Native four-channel color compatible with `LibreMetaverse`'s `Color4`.
#![allow(clippy::cast_possible_truncation)] // Explicit C# numeric conversions are part of the API.
#![allow(clippy::cast_sign_loss)] // FloatToByte clamps before the reference-compatible cast.
#![allow(clippy::float_cmp)] // Exact component equality is required by the reference type.
#![allow(clippy::inherent_to_string)] // The mapped C# method is named ToString.
#![allow(clippy::items_after_statements)] // Constants stay next to their compatibility formulas.
#![allow(clippy::many_single_char_names)] // Color/HSV formulas use the reference channel names.
#![allow(clippy::missing_errors_doc)] // Result shapes are fixed by the public compatibility map.
#![allow(clippy::must_use_candidate)] // Attributes are not part of the mapped C# surface.
#![allow(clippy::needless_pass_by_value)] // Owned arguments mirror mapped C# value parameters.
#![allow(clippy::should_implement_trait)] // Operator entry points have fixed generated names.
use crate::Error;
use crate::compat::Object;
use crate::math_compat::{
checked_range, clamp_f32, compare_f32, format_f32, hash_f32, lerp_f32, max_f32, min_f32,
};
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Color4 {
pub r: f32,
pub g: f32,
pub b: f32,
pub a: f32,
}
impl Color4 {
#[must_use]
pub const fn black() -> Self {
Self {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
}
}
#[must_use]
pub const fn white() -> Self {
Self {
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
}
}
pub fn new_with_byte_byte_byte_byte(r: u8, g: u8, b: u8, a: u8) -> Result<Self, Error> {
const QUANTA: f32 = 1.0 / 255.0;
Ok(Self {
r: f32::from(r) * QUANTA,
g: f32::from(g) * QUANTA,
b: f32::from(b) * QUANTA,
a: f32::from(a) * QUANTA,
})
}
pub fn new_with_single_single_single_single(
r: f32,
g: f32,
b: f32,
a: f32,
) -> Result<Self, Error> {
if r > 1.0 || g > 1.0 || b > 1.0 || a > 1.0 {
return Err(Error::Argument);
}
Ok(Self {
r: clamp_f32(r, 0.0, 1.0),
g: clamp_f32(g, 0.0, 1.0),
b: clamp_f32(b, 0.0, 1.0),
a: clamp_f32(a, 0.0, 1.0),
})
}
pub fn new_with_bytes_int32_boolean(
bytes: Vec<u8>,
pos: i32,
inverted: bool,
) -> Result<Self, Error> {
Self::from_bytes_slice(&bytes, pos, inverted, false)
}
pub fn new_with_bytes_int32_boolean_boolean(
bytes: Vec<u8>,
pos: i32,
inverted: bool,
alpha_inverted: bool,
) -> Result<Self, Error> {
Self::from_bytes_slice(&bytes, pos, inverted, alpha_inverted)
}
fn from_bytes_slice(
bytes: &[u8],
pos: i32,
inverted: bool,
alpha_inverted: bool,
) -> Result<Self, Error> {
let range = checked_range(pos, 4, bytes.len())?;
let values = &bytes[range];
const QUANTA: f32 = 1.0 / 255.0;
let channel = |value: u8| f32::from(if inverted { 255 - value } else { value }) * QUANTA;
let mut result = Self {
r: channel(values[0]),
g: channel(values[1]),
b: channel(values[2]),
a: channel(values[3]),
};
if alpha_inverted {
result.a = 1.0 - result.a;
}
Ok(result)
}
pub fn from_bytes_with_bytes_int32_boolean(
bytes: Vec<u8>,
pos: i32,
inverted: bool,
) -> Result<Self, Error> {
Self::new_with_bytes_int32_boolean(bytes, pos, inverted)
}
pub fn from_bytes_with_bytes_int32_boolean_boolean(
bytes: Vec<u8>,
pos: i32,
inverted: bool,
alpha_inverted: bool,
) -> Result<Self, Error> {
Self::new_with_bytes_int32_boolean_boolean(bytes, pos, inverted, alpha_inverted)
}
pub fn compare_to(&self, color: Self) -> Result<i32, Error> {
let this_hue = self.get_hue()?;
let that_hue = color.get_hue()?;
Ok(if this_hue < 0.0 && that_hue < 0.0 {
if self.r == color.r {
compare_f32(self.a, color.a)
} else {
compare_f32(self.r, color.r)
}
} else if this_hue == that_hue {
compare_f32(self.a, color.a)
} else {
compare_f32(this_hue, that_hue)
})
}
pub fn get_bytes_with_method(&self) -> Result<Vec<u8>, Error> {
self.get_bytes_with_boolean(false)
}
pub fn get_bytes_with_boolean(&self, inverted: bool) -> Result<Vec<u8>, Error> {
let mut bytes = vec![0; 4];
self.to_bytes_with_bytes_int32_boolean(&mut bytes, 0, inverted)?;
Ok(bytes)
}
pub fn get_float_bytes(&self) -> Result<Vec<u8>, Error> {
let mut bytes = vec![0; 16];
self.to_float_bytes(&mut bytes, 0)?;
Ok(bytes)
}
pub fn to_bytes_with_bytes_int32(&self, dest: &mut [u8], pos: i32) -> Result<(), Error> {
self.to_bytes_with_bytes_int32_boolean(dest, pos, false)
}
pub fn to_bytes_with_bytes_int32_boolean(
&self,
dest: &mut [u8],
pos: i32,
inverted: bool,
) -> Result<(), Error> {
for (offset, value) in [(0, self.r), (1, self.g), (2, self.b), (3, self.a)] {
let index = checked_range(
pos.checked_add(offset).ok_or(Error::IndexOutOfRange)?,
1,
dest.len(),
)?
.start;
let byte = float_to_byte(value);
dest[index] = if inverted { 255 - byte } else { byte };
}
Ok(())
}
pub fn to_float_bytes(&self, dest: &mut [u8], pos: i32) -> Result<(), Error> {
for (offset, value) in [(0, self.r), (4, self.g), (8, self.b), (12, self.a)] {
crate::byte_order::write_single_little_endian(
dest,
pos.checked_add(offset).ok_or(Error::IndexOutOfRange)?,
value,
)?;
}
Ok(())
}
pub fn get_hue(&self) -> Result<f32, Error> {
const HUE_MAX: f32 = 360.0;
let max = max_f32(max_f32(self.r, self.g), self.b);
let min = min_f32(min_f32(self.r, self.g), self.b);
let tolerance = (f64::from(max) * 0.000_01).abs();
if f64::from((max - min).abs()) < tolerance {
return Ok(-1.0);
}
if f64::from((self.r - max).abs()) < tolerance {
let b_delta = ((max - self.b) * (HUE_MAX / 6.0) + (max - min) / 2.0) / (max - min);
let g_delta = ((max - self.g) * (HUE_MAX / 6.0) + (max - min) / 2.0) / (max - min);
Ok(b_delta - g_delta)
} else if f64::from((self.g - max).abs()) < tolerance {
let r_delta = ((max - self.r) * (HUE_MAX / 6.0) + (max - min) / 2.0) / (max - min);
let b_delta = ((max - self.b) * (HUE_MAX / 6.0) + (max - min) / 2.0) / (max - min);
Ok(HUE_MAX / 3.0 + r_delta - b_delta)
} else {
let g_delta = ((max - self.g) * (HUE_MAX / 6.0) + (max - min) / 2.0) / (max - min);
let r_delta = ((max - self.r) * (HUE_MAX / 6.0) + (max - min) / 2.0) / (max - min);
Ok(2.0 * HUE_MAX / 3.0 + g_delta - r_delta)
}
}
pub fn from_hsv(hue: f64, saturation: f64, value: f64) -> Result<Self, Error> {
let (mut r, mut g, mut b) = (0.0, 0.0, 0.0);
if saturation == 0.0 {
r = value;
g = value;
b = value;
} else {
let sector_pos = hue / 60.0;
let sector_number = sector_pos.floor() as i32;
let fractional = sector_pos - f64::from(sector_number);
let p = value * (1.0 - saturation);
let q = value * (1.0 - saturation * fractional);
let t = value * (1.0 - saturation * (1.0 - fractional));
match sector_number {
0 => {
r = value;
g = t;
b = p;
}
1 => {
r = q;
g = value;
b = p;
}
2 => {
r = p;
g = value;
b = t;
}
3 => {
r = p;
g = q;
b = value;
}
4 => {
r = t;
g = p;
b = value;
}
5 => {
r = value;
g = p;
b = q;
}
_ => {}
}
}
Self::new_with_single_single_single_single(r as f32, g as f32, b as f32, 1.0)
}
pub fn lerp(value1: Self, value2: Self, amount: f32) -> Result<Self, Error> {
Self::new_with_single_single_single_single(
clamp_f32(lerp_f32(value1.r, value2.r, amount), 0.0, 1.0),
clamp_f32(lerp_f32(value1.g, value2.g, amount), 0.0, 1.0),
clamp_f32(lerp_f32(value1.b, value2.b, amount), 0.0, 1.0),
clamp_f32(lerp_f32(value1.a, value2.a, amount), 0.0, 1.0),
)
}
pub fn to_string(&self) -> String {
format!(
"<{}, {}, {}, {}>",
format_f32(self.r),
format_f32(self.g),
format_f32(self.b),
format_f32(self.a)
)
}
pub fn to_rgb_string(&self) -> Result<String, Error> {
Ok(format!(
"<{}, {}, {}>",
format_f32(self.r),
format_f32(self.g),
format_f32(self.b)
))
}
pub fn equals_with_object(&self, obj: Option<Object>) -> bool {
matches!(obj, Some(Object::Color4(value)) if Self::eq(*self, value))
}
pub fn equals_with_color4(&self, other: Self) -> bool {
Self::eq(*self, other)
}
pub fn get_hash_code(&self) -> i32 {
hash_f32(self.r) ^ hash_f32(self.g) ^ hash_f32(self.b) ^ hash_f32(self.a)
}
pub fn eq(lhs: Self, rhs: Self) -> bool {
lhs.r == rhs.r && lhs.g == rhs.g && lhs.b == rhs.b && lhs.a == rhs.a
}
pub fn ne(lhs: Self, rhs: Self) -> bool {
!Self::eq(lhs, rhs)
}
pub fn add(lhs: Self, rhs: Self) -> Self {
Self::channel_op(lhs, rhs, |a, b| a + b)
}
pub fn sub(lhs: Self, rhs: Self) -> Self {
Self::channel_op(lhs, rhs, |a, b| a - b)
}
pub fn mul(lhs: Self, rhs: Self) -> Self {
Self::channel_op(lhs, rhs, |a, b| a * b)
}
fn channel_op(lhs: Self, rhs: Self, op: impl Fn(f32, f32) -> f32) -> Self {
Self {
r: clamp_f32(op(lhs.r, rhs.r), 0.0, 1.0),
g: clamp_f32(op(lhs.g, rhs.g), 0.0, 1.0),
b: clamp_f32(op(lhs.b, rhs.b), 0.0, 1.0),
a: clamp_f32(op(lhs.a, rhs.a), 0.0, 1.0),
}
}
}
fn float_to_byte(value: f32) -> u8 {
(clamp_f32(value, 0.0, 1.0) * 255.0).floor() as u8
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn byte_and_float_serialization_match_golden_values() {
let color = Color4::new_with_single_single_single_single(1.0, 0.5, 0.25, 0.0).unwrap();
assert_eq!(color.get_bytes_with_method().unwrap(), [255, 127, 63, 0]);
assert_eq!(
color.get_bytes_with_boolean(true).unwrap(),
[0, 128, 192, 255]
);
let decoded =
Color4::new_with_bytes_int32_boolean_boolean(vec![0, 128, 192, 255], 0, true, true)
.unwrap();
assert_eq!(decoded.r, 1.0);
assert_eq!(decoded.a, 1.0);
let float_bytes = color.get_float_bytes().unwrap();
assert_eq!(&float_bytes[0..4], &1.0_f32.to_le_bytes());
assert_eq!(&float_bytes[4..8], &0.5_f32.to_le_bytes());
}
#[test]
fn output_preserves_partial_mutation_before_bad_index() {
let mut output = [0xaa; 2];
assert_eq!(
Color4::white().to_bytes_with_bytes_int32(&mut output, 0),
Err(Error::IndexOutOfRange)
);
assert_eq!(output, [255, 255]);
let mut floats = [0xaa; 6];
assert_eq!(
Color4::white().to_float_bytes(&mut floats, 0),
Err(Error::IndexOutOfRange)
);
assert_eq!(&floats[..4], &1.0_f32.to_le_bytes());
assert_eq!(&floats[4..], &[0xaa; 2]);
}
#[test]
fn constructor_clamping_hsv_and_channel_operators_match_reference() {
assert_eq!(
Color4::new_with_single_single_single_single(-1.0, 0.5, 0.0, 1.0).unwrap(),
Color4 {
r: 0.0,
g: 0.5,
b: 0.0,
a: 1.0
}
);
assert_eq!(
Color4::new_with_single_single_single_single(1.0001, 0.0, 0.0, 0.0),
Err(Error::Argument)
);
assert_eq!(
Color4::from_hsv(0.0, 1.0, 1.0).unwrap(),
Color4 {
r: 1.0,
g: 0.0,
b: 0.0,
a: 1.0
}
);
assert_eq!(
Color4::add(
Color4 {
r: 0.75,
g: 0.25,
b: 0.0,
a: 0.5
},
Color4 {
r: 0.5,
g: 0.5,
b: 0.5,
a: 0.75
}
),
Color4 {
r: 1.0,
g: 0.75,
b: 0.5,
a: 1.0
}
);
}
#[test]
fn hue_comparison_strings_and_hashes_match_reference_rules() {
let red = Color4 {
r: 1.0,
g: 0.0,
b: 0.0,
a: 1.0,
};
let green = Color4 {
r: 0.0,
g: 1.0,
b: 0.0,
a: 1.0,
};
assert_eq!(red.get_hue().unwrap(), 0.0);
assert_eq!(green.get_hue().unwrap(), 120.0);
assert_eq!(red.compare_to(green).unwrap(), -1);
assert_eq!(red.to_string(), "<1, 0, 0, 1>");
assert_eq!(red.to_rgb_string().unwrap(), "<1, 0, 0>");
let negative_zero = Color4 { r: -0.0, ..red };
let positive_zero = Color4 { r: 0.0, ..red };
assert_eq!(negative_zero.get_hash_code(), positive_zero.get_hash_code());
assert!(red.equals_with_object(Some(Object::from(red))));
}
}

View File

@@ -6,6 +6,7 @@
use std::collections::BTreeMap;
use std::future::Future;
use std::hash::Hash;
use std::marker::PhantomData;
use std::pin::Pin;
use std::sync::{
@@ -19,10 +20,47 @@ pub trait ReadWrite: std::io::Read + std::io::Write {}
impl<T: std::io::Read + std::io::Write> ReadWrite for T {}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[derive(Clone, Debug)]
pub enum Object {
Color4(crate::Color4),
String(String),
UUID(crate::UUID),
Vector2(crate::Vector2),
Vector3(crate::Vector3),
Vector3d(crate::Vector3d),
Vector4(crate::Vector4),
}
impl PartialEq for Object {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Color4(lhs), Self::Color4(rhs)) => crate::Color4::eq(*lhs, *rhs),
(Self::String(lhs), Self::String(rhs)) => lhs == rhs,
(Self::UUID(lhs), Self::UUID(rhs)) => lhs == rhs,
(Self::Vector2(lhs), Self::Vector2(rhs)) => crate::Vector2::eq(*lhs, *rhs),
(Self::Vector3(lhs), Self::Vector3(rhs)) => crate::Vector3::eq(*lhs, *rhs),
(Self::Vector3d(lhs), Self::Vector3d(rhs)) => crate::Vector3d::eq(*lhs, *rhs),
(Self::Vector4(lhs), Self::Vector4(rhs)) => crate::Vector4::eq(*lhs, *rhs),
_ => false,
}
}
}
impl Eq for Object {}
impl std::hash::Hash for Object {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
std::mem::discriminant(self).hash(state);
match self {
Self::Color4(value) => value.get_hash_code().hash(state),
Self::String(value) => value.hash(state),
Self::UUID(value) => value.hash(state),
Self::Vector2(value) => value.get_hash_code().hash(state),
Self::Vector3(value) => value.get_hash_code().hash(state),
Self::Vector3d(value) => value.get_hash_code().hash(state),
Self::Vector4(value) => value.get_hash_code().hash(state),
}
}
}
impl From<String> for Object {
@@ -37,6 +75,32 @@ impl From<&str> for Object {
}
}
impl From<crate::Color4> for Object {
fn from(value: crate::Color4) -> Self {
Self::Color4(value)
}
}
impl From<crate::Vector2> for Object {
fn from(value: crate::Vector2) -> Self {
Self::Vector2(value)
}
}
impl From<crate::Vector3> for Object {
fn from(value: crate::Vector3) -> Self {
Self::Vector3(value)
}
}
impl From<crate::Vector3d> for Object {
fn from(value: crate::Vector3d) -> Self {
Self::Vector3d(value)
}
}
impl From<crate::Vector4> for Object {
fn from(value: crate::Vector4) -> Self {
Self::Vector4(value)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Utf16CodeUnit(pub u16);

File diff suppressed because it is too large Load Diff

View File

@@ -3,11 +3,14 @@
extern crate self as libremetaverse_types;
mod byte_order;
mod color4;
pub mod compat;
mod crc32;
mod generated;
mod math_compat;
pub mod shim;
mod uuid;
mod vectors;
pub use generated::*;
pub use shim::{Error, NotImplemented, not_implemented};

View File

@@ -0,0 +1,189 @@
//! Small helpers that preserve the scalar behavior used by the C# value types.
#![allow(clippy::cast_possible_truncation)] // Hashes intentionally fold raw IEEE bit patterns.
#![allow(clippy::cast_possible_wrap)] // .NET hash codes reinterpret unsigned IEEE bits as i32.
#![allow(clippy::float_cmp)] // Signed-zero and exact scalar compatibility require exact comparisons.
use crate::Error;
use std::cmp::Ordering;
pub(crate) fn checked_range(
pos: i32,
width: usize,
len: usize,
) -> Result<std::ops::Range<usize>, Error> {
let start = usize::try_from(pos).map_err(|_| Error::IndexOutOfRange)?;
let end = start.checked_add(width).ok_or(Error::IndexOutOfRange)?;
if end > len {
return Err(Error::IndexOutOfRange);
}
Ok(start..end)
}
pub(crate) fn clamp_f32(value: f32, min: f32, max: f32) -> f32 {
if value > max {
max
} else if value < min {
min
} else {
value
}
}
pub(crate) fn clamp_f64(value: f64, min: f64, max: f64) -> f64 {
if value > max {
max
} else if value < min {
min
} else {
value
}
}
pub(crate) fn lerp_f32(value1: f32, value2: f32, amount: f32) -> f32 {
value1 + (value2 - value1) * amount
}
pub(crate) fn lerp_f64(value1: f64, value2: f64, amount: f64) -> f64 {
value1 + (value2 - value1) * amount
}
pub(crate) fn smooth_step_f32(value1: f32, value2: f32, amount: f32) -> f32 {
let amount = clamp_f32(amount, 0.0, 1.0);
lerp_f32(value1, value2, amount * amount * (3.0 - 2.0 * amount))
}
pub(crate) fn smooth_step_f64(value1: f64, value2: f64, amount: f64) -> f64 {
let amount = clamp_f64(amount, 0.0, 1.0);
lerp_f64(value1, value2, amount * amount * (3.0 - 2.0 * amount))
}
pub(crate) fn compare_f32(lhs: f32, rhs: f32) -> i32 {
match (lhs.is_nan(), rhs.is_nan()) {
(true, true) => 0,
(true, false) => -1,
(false, true) => 1,
(false, false) => match lhs.partial_cmp(&rhs).expect("non-NaN floats are ordered") {
Ordering::Less => -1,
Ordering::Equal => 0,
Ordering::Greater => 1,
},
}
}
pub(crate) fn compare_f64(lhs: f64, rhs: f64) -> i32 {
match (lhs.is_nan(), rhs.is_nan()) {
(true, true) => 0,
(true, false) => -1,
(false, true) => 1,
(false, false) => match lhs.partial_cmp(&rhs).expect("non-NaN floats are ordered") {
Ordering::Less => -1,
Ordering::Equal => 0,
Ordering::Greater => 1,
},
}
}
pub(crate) fn hash_f32(value: f32) -> i32 {
let bits = if value == 0.0 {
0
} else if value.is_nan() {
0x7fc0_0000
} else {
value.to_bits()
};
bits as i32
}
pub(crate) fn hash_f64(value: f64) -> i32 {
let bits = if value == 0.0 {
0
} else if value.is_nan() {
0x7ff8_0000_0000_0000
} else {
value.to_bits()
};
((bits as u32) ^ ((bits >> 32) as u32)) as i32
}
pub(crate) fn max_f32(lhs: f32, rhs: f32) -> f32 {
if lhs.is_nan() {
lhs
} else if rhs.is_nan() {
rhs
} else if lhs == rhs {
if lhs == 0.0 && lhs.is_sign_negative() {
rhs
} else {
lhs
}
} else if lhs > rhs {
lhs
} else {
rhs
}
}
pub(crate) fn min_f32(lhs: f32, rhs: f32) -> f32 {
if lhs.is_nan() {
lhs
} else if rhs.is_nan() {
rhs
} else if lhs == rhs {
if lhs == 0.0 && lhs.is_sign_negative() {
lhs
} else {
rhs
}
} else if lhs < rhs {
lhs
} else {
rhs
}
}
pub(crate) fn max_f64(lhs: f64, rhs: f64) -> f64 {
if lhs.is_nan() {
lhs
} else if rhs.is_nan() {
rhs
} else {
lhs.max(rhs)
}
}
pub(crate) fn min_f64(lhs: f64, rhs: f64) -> f64 {
if lhs.is_nan() {
lhs
} else if rhs.is_nan() {
rhs
} else {
lhs.min(rhs)
}
}
pub(crate) fn parse_f32(value: &str) -> Result<f32, Error> {
value.trim().parse().map_err(|_| Error::Argument)
}
pub(crate) fn parse_f64(value: &str) -> Result<f64, Error> {
value.trim().parse().map_err(|_| Error::Argument)
}
pub(crate) fn format_f32(value: f32) -> String {
match value {
value if value.is_nan() => "NaN".to_owned(),
value if value == f32::INFINITY => "Infinity".to_owned(),
value if value == f32::NEG_INFINITY => "-Infinity".to_owned(),
value => value.to_string().replace('e', "E"),
}
}
pub(crate) fn format_f64(value: f64) -> String {
match value {
value if value.is_nan() => "NaN".to_owned(),
value if value == f64::INFINITY => "Infinity".to_owned(),
value if value == f64::NEG_INFINITY => "-Infinity".to_owned(),
value => value.to_string().replace('e', "E"),
}
}

File diff suppressed because it is too large Load Diff