Numerous small changes suggested by clippy
This commit is contained in:
parent
6a1fe84af7
commit
fb744258b2
|
|
@ -47,7 +47,7 @@ impl AccumulationBuffer {
|
||||||
let buffer_colour_bias = &mut self.colour_bias_buffer[row][column];
|
let buffer_colour_bias = &mut self.colour_bias_buffer[row][column];
|
||||||
let buffer_weight = &mut self.weight_buffer[row][column];
|
let buffer_weight = &mut self.weight_buffer[row][column];
|
||||||
let buffer_weight_bias = &mut self.weight_bias_buffer[row][column];
|
let buffer_weight_bias = &mut self.weight_bias_buffer[row][column];
|
||||||
let photon_colour = ColourXyz::from_photon(&photon);
|
let photon_colour = ColourXyz::from_photon(photon);
|
||||||
let weight_sum_y = weight - *buffer_weight_bias;
|
let weight_sum_y = weight - *buffer_weight_bias;
|
||||||
let weight_sum_t = *buffer_weight + weight_sum_y;
|
let weight_sum_t = *buffer_weight + weight_sum_y;
|
||||||
*buffer_weight_bias = (weight_sum_t - *buffer_weight) - weight_sum_y;
|
*buffer_weight_bias = (weight_sum_t - *buffer_weight) - weight_sum_y;
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ pub fn partial_render_scene(
|
||||||
let mut output_image_tile = AccumulationBuffer::new(tile.width(), tile.height());
|
let mut output_image_tile = AccumulationBuffer::new(tile.width(), tile.height());
|
||||||
let image_sampler = ImageSampler::new(width, height, scene.camera_location);
|
let image_sampler = ImageSampler::new(width, height, scene.camera_location);
|
||||||
let integrator = SimpleRandomIntegrator {};
|
let integrator = SimpleRandomIntegrator {};
|
||||||
let sampler = Sampler { scene: &scene };
|
let sampler = Sampler { scene };
|
||||||
for column in 0..tile.width() {
|
for column in 0..tile.width() {
|
||||||
for row in 0..tile.height() {
|
for row in 0..tile.height() {
|
||||||
let ray = image_sampler.ray_for_pixel(tile.start_row + row, tile.start_column + column);
|
let ray = image_sampler.ray_for_pixel(tile.start_row + row, tile.start_column + column);
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ impl Photon {
|
||||||
pub fn set_intensity(&self, intensity: f64) -> Photon {
|
pub fn set_intensity(&self, intensity: f64) -> Photon {
|
||||||
Photon {
|
Photon {
|
||||||
wavelength: self.wavelength,
|
wavelength: self.wavelength,
|
||||||
intensity: intensity,
|
intensity,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
use std::convert::TryInto;
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::BufWriter;
|
use std::io::BufWriter;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
@ -20,7 +19,7 @@ impl ImageRgbU8 {
|
||||||
|
|
||||||
pub fn get_colour(&self, row: usize, column: usize) -> ColourRgbU8 {
|
pub fn get_colour(&self, row: usize, column: usize) -> ColourRgbU8 {
|
||||||
ColourRgbU8 {
|
ColourRgbU8 {
|
||||||
values: self.data[row][column].try_into().expect("Wrong length."),
|
values: self.data[row][column],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,7 +51,7 @@ impl ImageRgbU8 {
|
||||||
|
|
||||||
pub fn write_png(&self, filename: &Path) -> Result<(), std::io::Error> {
|
pub fn write_png(&self, filename: &Path) -> Result<(), std::io::Error> {
|
||||||
let file = File::create(filename)?;
|
let file = File::create(filename)?;
|
||||||
let ref mut file_buffer = BufWriter::new(file);
|
let file_buffer = &mut BufWriter::new(file);
|
||||||
|
|
||||||
let mut encoder = png::Encoder::new(
|
let mut encoder = png::Encoder::new(
|
||||||
file_buffer,
|
file_buffer,
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ impl Integrator for SimpleRandomIntegrator {
|
||||||
let MaterialSampleResult {
|
let MaterialSampleResult {
|
||||||
direction: w_o,
|
direction: w_o,
|
||||||
pdf: w_o_pdf,
|
pdf: w_o_pdf,
|
||||||
} = info.material.sample(&w_i, &photon);
|
} = info.material.sample(&w_i, photon);
|
||||||
let world_space_w_o = bsdf_to_world_space * w_o;
|
let world_space_w_o = bsdf_to_world_space * w_o;
|
||||||
info.material.bsdf()(
|
info.material.bsdf()(
|
||||||
&w_o,
|
&w_o,
|
||||||
|
|
@ -45,7 +45,7 @@ impl Integrator for SimpleRandomIntegrator {
|
||||||
photon.wavelength,
|
photon.wavelength,
|
||||||
)),
|
)),
|
||||||
Some(recursive_hit) => {
|
Some(recursive_hit) => {
|
||||||
self.integrate(&sampler, &recursive_hit, &photon, recursion_limit - 1)
|
self.integrate(sampler, &recursive_hit, photon, recursion_limit - 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.scale_intensity(w_o_pdf)
|
.scale_intensity(w_o_pdf)
|
||||||
|
|
|
||||||
|
|
@ -35,13 +35,13 @@ impl Integrator for WhittedIntegrator {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|light| {
|
.map(|light| {
|
||||||
match sampler.sample(&Ray::new(info.location, light.direction).bias(0.000_000_1)) {
|
match sampler.sample(&Ray::new(info.location, light.direction).bias(0.000_000_1)) {
|
||||||
Some(_) => self.ambient_light.emit_photon(&photon),
|
Some(_) => self.ambient_light.emit_photon(photon),
|
||||||
None => info.material.bsdf()(
|
None => info.material.bsdf()(
|
||||||
&(world_to_bsdf_space * info.retro),
|
&(world_to_bsdf_space * info.retro),
|
||||||
&(world_to_bsdf_space * light.direction),
|
&(world_to_bsdf_space * light.direction),
|
||||||
&light
|
&light
|
||||||
.spectrum
|
.spectrum
|
||||||
.emit_photon(&photon)
|
.emit_photon(photon)
|
||||||
.scale_intensity(light.direction.dot(&info.normal).abs()),
|
.scale_intensity(light.direction.dot(&info.normal).abs()),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
@ -49,7 +49,7 @@ impl Integrator for WhittedIntegrator {
|
||||||
.chain(
|
.chain(
|
||||||
[info
|
[info
|
||||||
.material
|
.material
|
||||||
.sample(&(world_to_bsdf_space * info.retro), &photon)]
|
.sample(&(world_to_bsdf_space * info.retro), photon)]
|
||||||
.iter()
|
.iter()
|
||||||
.map(|MaterialSampleResult { direction, pdf: _ }| {
|
.map(|MaterialSampleResult { direction, pdf: _ }| {
|
||||||
let world_space_direction = bsdf_to_world_space * direction;
|
let world_space_direction = bsdf_to_world_space * direction;
|
||||||
|
|
@ -62,9 +62,9 @@ impl Integrator for WhittedIntegrator {
|
||||||
&(world_to_bsdf_space * info.retro),
|
&(world_to_bsdf_space * info.retro),
|
||||||
direction,
|
direction,
|
||||||
&self.integrate(
|
&self.integrate(
|
||||||
&sampler,
|
sampler,
|
||||||
&recursive_hit,
|
&recursive_hit,
|
||||||
&photon,
|
photon,
|
||||||
recursion_limit - 1,
|
recursion_limit - 1,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ impl Vec3 {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn norm_squared(&self) -> f64 {
|
pub fn norm_squared(&self) -> f64 {
|
||||||
self.dot(&self)
|
self.dot(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn norm(&self) -> f64 {
|
pub fn norm(&self) -> f64 {
|
||||||
|
|
@ -119,14 +119,12 @@ impl Vec3 {
|
||||||
} else {
|
} else {
|
||||||
2
|
2
|
||||||
}
|
}
|
||||||
} else {
|
} else if y < z {
|
||||||
if y < z {
|
|
||||||
1
|
1
|
||||||
} else {
|
} else {
|
||||||
2
|
2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub fn component_mul(&self, rhs: &Self) -> Self {
|
pub fn component_mul(&self, rhs: &Self) -> Self {
|
||||||
let mut coords = [0.0; 3];
|
let mut coords = [0.0; 3];
|
||||||
|
|
|
||||||
|
|
@ -47,16 +47,16 @@ mod wavefront_obj {
|
||||||
) -> Vec<Triangle> {
|
) -> Vec<Triangle> {
|
||||||
if let Some(v0_index) = polygon.iter().next() {
|
if let Some(v0_index) = polygon.iter().next() {
|
||||||
let (v0_vertex, v0_normal) =
|
let (v0_vertex, v0_normal) =
|
||||||
get_vertex_and_normal(v0_index, &vertex_positions, &normal_positions);
|
get_vertex_and_normal(v0_index, &vertex_positions, normal_positions);
|
||||||
polygon
|
polygon
|
||||||
.iter()
|
.iter()
|
||||||
.skip(1)
|
.skip(1)
|
||||||
.zip(polygon.iter().skip(2))
|
.zip(polygon.iter().skip(2))
|
||||||
.map(|(v1_index, v2_index)| {
|
.map(|(v1_index, v2_index)| {
|
||||||
let (v1_vertex, v1_normal) =
|
let (v1_vertex, v1_normal) =
|
||||||
get_vertex_and_normal(v1_index, &vertex_positions, &normal_positions);
|
get_vertex_and_normal(v1_index, vertex_positions, normal_positions);
|
||||||
let (v2_vertex, v2_normal) =
|
let (v2_vertex, v2_normal) =
|
||||||
get_vertex_and_normal(v2_index, &vertex_positions, &normal_positions);
|
get_vertex_and_normal(v2_index, vertex_positions, normal_positions);
|
||||||
let vertices = [v0_vertex, v1_vertex, v2_vertex];
|
let vertices = [v0_vertex, v1_vertex, v2_vertex];
|
||||||
let normals = [v0_normal, v1_normal, v2_normal];
|
let normals = [v0_normal, v1_normal, v2_normal];
|
||||||
Triangle {
|
Triangle {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use crate::math::Vec3;
|
||||||
|
|
||||||
use super::{RandomDistribution, UnitDisc};
|
use super::{RandomDistribution, UnitDisc};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
pub struct CosineWeightedHemisphere {
|
pub struct CosineWeightedHemisphere {
|
||||||
unit_disc: UnitDisc,
|
unit_disc: UnitDisc,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,12 @@ impl SkyLightPdf {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for SkyLightPdf {
|
||||||
|
fn default() -> SkyLightPdf {
|
||||||
|
SkyLightPdf::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl RandomDistribution<Vec3> for SkyLightPdf {
|
impl RandomDistribution<Vec3> for SkyLightPdf {
|
||||||
fn value(&self) -> Vec3 {
|
fn value(&self) -> Vec3 {
|
||||||
let mut rng = thread_rng();
|
let mut rng = thread_rng();
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use crate::math::Vec3;
|
||||||
|
|
||||||
use super::RandomDistribution;
|
use super::RandomDistribution;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
pub struct UniformHemisphere {}
|
pub struct UniformHemisphere {}
|
||||||
|
|
||||||
impl UniformHemisphere {
|
impl UniformHemisphere {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,12 @@ pub struct UnitDisc {
|
||||||
square_distribution: UniformSquare,
|
square_distribution: UniformSquare,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for UnitDisc {
|
||||||
|
fn default() -> UnitDisc {
|
||||||
|
UnitDisc::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl UnitDisc {
|
impl UnitDisc {
|
||||||
pub fn new() -> UnitDisc {
|
pub fn new() -> UnitDisc {
|
||||||
let square_distribution = UniformSquare::new(Vec2::new(-1.0, -1.0), 2.0);
|
let square_distribution = UniformSquare::new(Vec2::new(-1.0, -1.0), 2.0);
|
||||||
|
|
|
||||||
|
|
@ -92,24 +92,24 @@ fn closest_intersection(
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Intersect for BoundingVolumeHierarchy {
|
impl Intersect for BoundingVolumeHierarchy {
|
||||||
fn intersect<'a>(&'a self, ray: &Ray) -> Option<IntersectionInfo> {
|
fn intersect(&self, ray: &Ray) -> Option<IntersectionInfo> {
|
||||||
match self {
|
match self {
|
||||||
BoundingVolumeHierarchy::Node {
|
BoundingVolumeHierarchy::Node {
|
||||||
bounds,
|
bounds,
|
||||||
left,
|
left,
|
||||||
right,
|
right,
|
||||||
} => {
|
} => {
|
||||||
if bounds.intersect(&ray) {
|
if bounds.intersect(ray) {
|
||||||
closest_intersection(left.intersect(&ray), right.intersect(&ray))
|
closest_intersection(left.intersect(ray), right.intersect(ray))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BoundingVolumeHierarchy::Leaf { bounds, primitives } => {
|
BoundingVolumeHierarchy::Leaf { bounds, primitives } => {
|
||||||
if bounds.intersect(&ray) {
|
if bounds.intersect(ray) {
|
||||||
primitives
|
primitives
|
||||||
.iter()
|
.iter()
|
||||||
.map(|elem| elem.intersect(&ray))
|
.map(|elem| elem.intersect(ray))
|
||||||
.fold(None, closest_intersection)
|
.fold(None, closest_intersection)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ pub struct IntersectionInfo {
|
||||||
/// intersected with a [Ray](Ray)
|
/// intersected with a [Ray](Ray)
|
||||||
pub trait Intersect: Send + Sync {
|
pub trait Intersect: Send + Sync {
|
||||||
/// Test if the ray intersects the object, and return information about the object and intersection.
|
/// Test if the ray intersects the object, and return information about the object and intersection.
|
||||||
fn intersect<'a>(&'a self, ray: &Ray) -> Option<IntersectionInfo>;
|
fn intersect(&self, ray: &Ray) -> Option<IntersectionInfo>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A geometric object that can be intersected with a ray
|
/// A geometric object that can be intersected with a ray
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ impl Plane {
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
impl Intersect for Plane {
|
impl Intersect for Plane {
|
||||||
fn intersect<'a>(&'a self, ray: &Ray) -> Option<IntersectionInfo> {
|
fn intersect(&self, ray: &Ray) -> Option<IntersectionInfo> {
|
||||||
let ray_direction_dot_plane_normal = ray.direction.dot(&self.normal);
|
let ray_direction_dot_plane_normal = ray.direction.dot(&self.normal);
|
||||||
let point_on_plane = self.normal * self.distance_from_origin;
|
let point_on_plane = self.normal * self.distance_from_origin;
|
||||||
let point_on_plane_minus_ray_origin_dot_normal =
|
let point_on_plane_minus_ray_origin_dot_normal =
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ impl Sphere {
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
impl Intersect for Sphere {
|
impl Intersect for Sphere {
|
||||||
fn intersect<'a>(&'a self, ray: &Ray) -> Option<IntersectionInfo> {
|
fn intersect<'a>(&'_ self, ray: &Ray) -> Option<IntersectionInfo> {
|
||||||
let r_o = ray.origin;
|
let r_o = ray.origin;
|
||||||
let centre_coords = self.centre;
|
let centre_coords = self.centre;
|
||||||
let a = ray
|
let a = ray
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ pub struct Triangle {
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
impl Intersect for Triangle {
|
impl Intersect for Triangle {
|
||||||
fn intersect<'a>(&'a self, ray: &Ray) -> Option<IntersectionInfo> {
|
fn intersect(&self, ray: &Ray) -> Option<IntersectionInfo> {
|
||||||
let translation = -ray.origin;
|
let translation = -ray.origin;
|
||||||
let indices = indices_with_index_of_largest_element_last(&ray.direction);
|
let indices = indices_with_index_of_largest_element_last(&ray.direction);
|
||||||
let permuted_ray_direction = permute_vector_elements(&ray.direction, &indices);
|
let permuted_ray_direction = permute_vector_elements(&ray.direction, &indices);
|
||||||
|
|
@ -126,7 +126,7 @@ fn is_valid_permutation(indices: &[usize; 3]) -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn permute_vector_elements(v: &Vec3, indices: &[usize; 3]) -> Vec3 {
|
fn permute_vector_elements(v: &Vec3, indices: &[usize; 3]) -> Vec3 {
|
||||||
debug_assert!(is_valid_permutation(&indices));
|
debug_assert!(is_valid_permutation(indices));
|
||||||
Vec3::new(v[indices[0]], v[indices[1]], v[indices[2]])
|
Vec3::new(v[indices[0]], v[indices[1]], v[indices[2]])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ impl HasBoundingBox for Vec<Box<dyn Primitive>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Intersect for Vec<Box<dyn Primitive>> {
|
impl Intersect for Vec<Box<dyn Primitive>> {
|
||||||
fn intersect<'a>(&'a self, ray: &Ray) -> Option<IntersectionInfo> {
|
fn intersect(&self, ray: &Ray) -> Option<IntersectionInfo> {
|
||||||
self.iter()
|
self.iter()
|
||||||
.flat_map(|primitive| primitive.intersect(&ray))
|
.flat_map(|primitive| primitive.intersect(ray))
|
||||||
.min_by(
|
.min_by(
|
||||||
|a, b| match PartialOrd::partial_cmp(&a.distance, &b.distance) {
|
|a, b| match PartialOrd::partial_cmp(&a.distance, &b.distance) {
|
||||||
None => std::cmp::Ordering::Less,
|
None => std::cmp::Ordering::Less,
|
||||||
|
|
@ -32,9 +32,9 @@ impl HasBoundingBox for Vec<Box<dyn Aggregate>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Intersect for Vec<Box<dyn Aggregate>> {
|
impl Intersect for Vec<Box<dyn Aggregate>> {
|
||||||
fn intersect<'a>(&'a self, ray: &Ray) -> Option<IntersectionInfo> {
|
fn intersect(&self, ray: &Ray) -> Option<IntersectionInfo> {
|
||||||
self.iter()
|
self.iter()
|
||||||
.flat_map(|aggregate| aggregate.intersect(&ray))
|
.flat_map(|aggregate| aggregate.intersect(ray))
|
||||||
.min_by(
|
.min_by(
|
||||||
|a, b| match PartialOrd::partial_cmp(&a.distance, &b.distance) {
|
|a, b| match PartialOrd::partial_cmp(&a.distance, &b.distance) {
|
||||||
None => std::cmp::Ordering::Less,
|
None => std::cmp::Ordering::Less,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ impl<'a> Sampler<'a> {
|
||||||
self.scene
|
self.scene
|
||||||
.objects
|
.objects
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|object| object.intersect(&ray))
|
.flat_map(|object| object.intersect(ray))
|
||||||
.min_by(
|
.min_by(
|
||||||
|a, b| match PartialOrd::partial_cmp(&a.distance, &b.distance) {
|
|a, b| match PartialOrd::partial_cmp(&a.distance, &b.distance) {
|
||||||
None => std::cmp::Ordering::Less,
|
None => std::cmp::Ordering::Less,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
pub enum BinaryTree<Value, LeafValue>
|
pub enum BinaryTree<Value, LeafValue> {
|
||||||
{
|
|
||||||
Branch {
|
Branch {
|
||||||
value: Value,
|
value: Value,
|
||||||
left: Box<Self>,
|
left: Box<Self>,
|
||||||
|
|
@ -11,8 +10,7 @@ pub enum BinaryTree<Value, LeafValue>
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Value, LeafValue> BinaryTree<Value, LeafValue>
|
impl<Value, LeafValue> BinaryTree<Value, LeafValue> {
|
||||||
{
|
|
||||||
pub fn count_leaves(&self) -> usize {
|
pub fn count_leaves(&self) -> usize {
|
||||||
match self {
|
match self {
|
||||||
Self::Branch {
|
Self::Branch {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use crate::raycasting::{Primitive, Triangle};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub fn triangulate_polygon(
|
pub fn triangulate_polygon(
|
||||||
vertices: &Vec<Vec3>,
|
vertices: &[Vec3],
|
||||||
normal: &Vec3,
|
normal: &Vec3,
|
||||||
material: Arc<dyn Material>,
|
material: Arc<dyn Material>,
|
||||||
) -> Vec<Arc<dyn Primitive>> {
|
) -> Vec<Arc<dyn Primitive>> {
|
||||||
|
|
@ -124,7 +124,7 @@ pub fn generate_dodecahedron(
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|face| {
|
.flat_map(|face| {
|
||||||
let normal = (face[1] - face[0]).cross(&(face[2] - face[1]));
|
let normal = (face[1] - face[0]).cross(&(face[2] - face[1]));
|
||||||
let transformed_face = face.iter().map(|v| centre + v * scale).collect();
|
let transformed_face: Vec<_> = face.iter().map(|v| centre + v * scale).collect();
|
||||||
triangulate_polygon(&transformed_face, &normal, Arc::clone(&material))
|
triangulate_polygon(&transformed_face, &normal, Arc::clone(&material))
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue