♻️ Extract gpu_buffers.rs and renderable_model.rs
parent
a1c21b92c9
commit
24418bfa55
|
@ -10,6 +10,7 @@ use opengl_rust::*;
|
||||||
|
|
||||||
use file::load_small_file;
|
use file::load_small_file;
|
||||||
use iqm::Model;
|
use iqm::Model;
|
||||||
|
use renderable_model::RenderableModel;
|
||||||
use shader::{ShaderProgram, ShaderObject};
|
use shader::{ShaderProgram, ShaderObject};
|
||||||
use texture::Texture;
|
use texture::Texture;
|
||||||
use timestep::TimeStep;
|
use timestep::TimeStep;
|
||||||
|
@ -172,12 +173,12 @@ fn main () {
|
||||||
let model_data = load_small_file ("pumpking.iqm", 1024 * 1024);
|
let model_data = load_small_file ("pumpking.iqm", 1024 * 1024);
|
||||||
let model = Model::from_slice (&model_data [..]);
|
let model = Model::from_slice (&model_data [..]);
|
||||||
|
|
||||||
let renderable_model = glezz::RenderableModel::from_iqm (&model);
|
let renderable_model = RenderableModel::from_iqm (&model);
|
||||||
|
|
||||||
let sky_data = load_small_file ("sky-sphere.iqm", 1024 * 1024);
|
let sky_data = load_small_file ("sky-sphere.iqm", 1024 * 1024);
|
||||||
let sky_model = Model::from_slice (&sky_data [..]);
|
let sky_model = Model::from_slice (&sky_data [..]);
|
||||||
|
|
||||||
let renderable_sky = glezz::RenderableModel::from_iqm (&sky_model);
|
let renderable_sky = RenderableModel::from_iqm (&sky_model);
|
||||||
|
|
||||||
glezz::enable_vertex_attrib_array (attrs ["pos"]);
|
glezz::enable_vertex_attrib_array (attrs ["pos"]);
|
||||||
glezz::enable_vertex_attrib_array (attrs ["uv"]);
|
glezz::enable_vertex_attrib_array (attrs ["uv"]);
|
||||||
|
|
215
src/glezz.rs
215
src/glezz.rs
|
@ -1,14 +1,6 @@
|
||||||
// Trivial wrappers around GLESv2 C functions that should be safe
|
// Trivial wrappers around GLESv2 C functions that should be safe
|
||||||
|
|
||||||
use byteorder::{ByteOrder, LittleEndian, ReadBytesExt};
|
|
||||||
use glam::{Mat4, Vec3, Vec4};
|
use glam::{Mat4, Vec3, Vec4};
|
||||||
use std::collections::*;
|
|
||||||
use std::convert::TryInto;
|
|
||||||
use std::cmp;
|
|
||||||
use std::io::Cursor;
|
|
||||||
use std::ffi::c_void;
|
|
||||||
|
|
||||||
use crate::iqm;
|
|
||||||
|
|
||||||
pub fn clear_color (r: f32, g: f32, b: f32, a: f32) {
|
pub fn clear_color (r: f32, g: f32, b: f32, a: f32) {
|
||||||
unsafe {
|
unsafe {
|
||||||
|
@ -69,211 +61,6 @@ pub fn uniform_matrix_4fv (uni: i32, m: &Mat4) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// More abstract things
|
// More abstract things below here
|
||||||
|
|
||||||
// Only contains f32 floats
|
|
||||||
|
|
||||||
pub struct VertexBuffer {
|
|
||||||
id: u32,
|
|
||||||
// Not bytes.
|
|
||||||
len: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VertexBuffer {
|
|
||||||
pub fn from_slice (slice: &[f32]) -> Self {
|
|
||||||
const FLOAT_SIZE: usize = 4;
|
|
||||||
|
|
||||||
let id = {
|
|
||||||
let mut id = 0;
|
|
||||||
unsafe {
|
|
||||||
gl::GenBuffers (1, &mut id);
|
|
||||||
gl::BindBuffer (gl::ARRAY_BUFFER, id);
|
|
||||||
|
|
||||||
gl::BufferData (gl::ARRAY_BUFFER, (slice.len () * FLOAT_SIZE).try_into ().unwrap (), &slice [0] as *const f32 as *const c_void, gl::STATIC_DRAW);
|
|
||||||
}
|
|
||||||
assert! (id != 0);
|
|
||||||
id
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
|
||||||
id,
|
|
||||||
len: slice.len (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bind (&self) {
|
|
||||||
unsafe {
|
|
||||||
gl::BindBuffer (gl::ARRAY_BUFFER, self.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Drop for VertexBuffer {
|
|
||||||
fn drop (&mut self) {
|
|
||||||
if self.id == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
gl::DeleteBuffers (1, &self.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.id = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct IndexBuffer {
|
|
||||||
id: u32,
|
|
||||||
// Not bytes. Number of indexes.
|
|
||||||
len: usize,
|
|
||||||
max: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IndexBuffer {
|
|
||||||
pub fn from_slice (slice: &[u8]) -> Self {
|
|
||||||
let mut rdr = Cursor::new (slice);
|
|
||||||
|
|
||||||
let mut max = None;
|
|
||||||
|
|
||||||
const IDX_SIZE: usize = 4;
|
|
||||||
|
|
||||||
assert_eq! (slice.len () % IDX_SIZE, 0);
|
|
||||||
|
|
||||||
for _ in 0..slice.len () / IDX_SIZE {
|
|
||||||
let idx = rdr.read_u32::<LittleEndian> ().unwrap ();
|
|
||||||
|
|
||||||
max = match max {
|
|
||||||
None => Some (idx),
|
|
||||||
Some (max) => Some (cmp::max (max, idx)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let id = {
|
|
||||||
let mut id = 0;
|
|
||||||
unsafe {
|
|
||||||
gl::GenBuffers (1, &mut id);
|
|
||||||
gl::BindBuffer (gl::ELEMENT_ARRAY_BUFFER, id);
|
|
||||||
|
|
||||||
gl::BufferData (gl::ELEMENT_ARRAY_BUFFER, slice.len ().try_into ().unwrap (), &slice [0] as *const u8 as *const c_void, gl::STATIC_DRAW);
|
|
||||||
}
|
|
||||||
assert! (id != 0);
|
|
||||||
id
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
|
||||||
id,
|
|
||||||
len: slice.len () / IDX_SIZE,
|
|
||||||
max: max.unwrap (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bind (&self) {
|
|
||||||
unsafe {
|
|
||||||
gl::BindBuffer (gl::ELEMENT_ARRAY_BUFFER, self.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for IndexBuffer {
|
|
||||||
fn drop (&mut self) {
|
|
||||||
if self.id == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
gl::DeleteBuffers (1, &self.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.id = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Takes ownership of mesh stuff in an opaque way that's abstract
|
|
||||||
// from the IQM model. IQM is zero-copy, but this is not.
|
|
||||||
// Since it's opaque, I can drop in a VBO/IBO setup when I'm not lazy.
|
|
||||||
|
|
||||||
struct RenderableMesh {
|
|
||||||
first_triangle: usize,
|
|
||||||
num_triangles: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RenderableModel {
|
|
||||||
num_pos: usize,
|
|
||||||
num_uv: usize,
|
|
||||||
num_normal: usize,
|
|
||||||
|
|
||||||
vertexes: VertexBuffer,
|
|
||||||
indexes: IndexBuffer,
|
|
||||||
|
|
||||||
meshes: Vec <RenderableMesh>,
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe fn vertex_attrib_pointer (id: Option <u32>, num_coords: i32, float_offset: usize) {
|
|
||||||
const FALSE_U8: u8 = 0;
|
|
||||||
const FLOAT_SIZE: i32 = 4;
|
|
||||||
|
|
||||||
if let Some (id) = id {
|
|
||||||
gl::VertexAttribPointer (id, num_coords, gl::FLOAT, FALSE_U8, FLOAT_SIZE * num_coords, (float_offset * 4) as *const u8 as *const c_void);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderableModel {
|
|
||||||
pub fn from_iqm (model: &iqm::Model) -> RenderableModel {
|
|
||||||
let pos_bytes = model.get_vertex_slice (iqm::types::POSITION);
|
|
||||||
let uv_bytes = model.get_vertex_slice (iqm::types::TEXCOORD);
|
|
||||||
let normal_bytes = model.get_vertex_slice (iqm::types::NORMAL);
|
|
||||||
|
|
||||||
let num_pos = pos_bytes.len () / 4;
|
|
||||||
let num_uv = uv_bytes.len () / 4;
|
|
||||||
let num_normal = normal_bytes.len () / 4;
|
|
||||||
|
|
||||||
let mut vertex_vec = vec! [0.0; num_pos + num_uv + num_normal];
|
|
||||||
|
|
||||||
LittleEndian::read_f32_into (pos_bytes, &mut vertex_vec [0..num_pos]);
|
|
||||||
LittleEndian::read_f32_into (uv_bytes, &mut vertex_vec [num_pos..num_pos + num_uv]);
|
|
||||||
LittleEndian::read_f32_into (normal_bytes, &mut vertex_vec [num_pos + num_uv..num_pos + num_uv + num_normal]);
|
|
||||||
|
|
||||||
let vertexes = VertexBuffer::from_slice (&vertex_vec);
|
|
||||||
|
|
||||||
let index_slice = model.get_all_indexes ();
|
|
||||||
let indexes = IndexBuffer::from_slice (index_slice);
|
|
||||||
|
|
||||||
let max_index: usize = indexes.max.try_into ().unwrap ();
|
|
||||||
|
|
||||||
assert! (max_index * 3 < num_pos);
|
|
||||||
assert! (max_index * 2 < num_uv);
|
|
||||||
assert! (max_index * 3 < num_normal);
|
|
||||||
|
|
||||||
let meshes = model.meshes.iter ()
|
|
||||||
.map (|mesh| RenderableMesh {
|
|
||||||
first_triangle: mesh.first_triangle.try_into ().unwrap (),
|
|
||||||
num_triangles: mesh.num_triangles.try_into ().unwrap (),
|
|
||||||
})
|
|
||||||
.collect ();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
num_pos,
|
|
||||||
num_uv,
|
|
||||||
num_normal,
|
|
||||||
|
|
||||||
vertexes,
|
|
||||||
indexes,
|
|
||||||
meshes,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn draw (&self, attrs: &HashMap <String, Option <u32>>, mesh_num: usize)
|
|
||||||
{
|
|
||||||
let mesh = &self.meshes [mesh_num];
|
|
||||||
|
|
||||||
self.vertexes.bind ();
|
|
||||||
self.indexes.bind ();
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
vertex_attrib_pointer (attrs ["pos"], 3, 0);
|
|
||||||
vertex_attrib_pointer (attrs ["uv"], 2, self.num_pos);
|
|
||||||
vertex_attrib_pointer (attrs ["normal"], 3, self.num_pos + self.num_uv);
|
|
||||||
|
|
||||||
gl::DrawRangeElements (gl::TRIANGLES, 0, self.indexes.max, mesh.num_triangles * 3, gl::UNSIGNED_INT, (mesh.first_triangle * 3 * 4) as *const u8 as *const c_void);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
use byteorder::{LittleEndian, ReadBytesExt};
|
||||||
|
|
||||||
|
use std::cmp;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
use std::ffi::c_void;
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
// Only contains f32 floats
|
||||||
|
|
||||||
|
pub struct VertexBuffer {
|
||||||
|
id: u32,
|
||||||
|
// Not bytes.
|
||||||
|
len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VertexBuffer {
|
||||||
|
pub fn from_slice (slice: &[f32]) -> Self {
|
||||||
|
const FLOAT_SIZE: usize = 4;
|
||||||
|
|
||||||
|
let id = {
|
||||||
|
let mut id = 0;
|
||||||
|
unsafe {
|
||||||
|
gl::GenBuffers (1, &mut id);
|
||||||
|
gl::BindBuffer (gl::ARRAY_BUFFER, id);
|
||||||
|
|
||||||
|
gl::BufferData (gl::ARRAY_BUFFER, (slice.len () * FLOAT_SIZE).try_into ().unwrap (), &slice [0] as *const f32 as *const c_void, gl::STATIC_DRAW);
|
||||||
|
}
|
||||||
|
assert! (id != 0);
|
||||||
|
id
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
len: slice.len (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bind (&self) {
|
||||||
|
unsafe {
|
||||||
|
gl::BindBuffer (gl::ARRAY_BUFFER, self.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Drop for VertexBuffer {
|
||||||
|
fn drop (&mut self) {
|
||||||
|
if self.id == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
gl::DeleteBuffers (1, &self.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.id = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct IndexBuffer {
|
||||||
|
id: u32,
|
||||||
|
// Not bytes. Number of indexes.
|
||||||
|
len: usize,
|
||||||
|
max: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IndexBuffer {
|
||||||
|
pub fn from_slice (slice: &[u8]) -> Self {
|
||||||
|
let mut rdr = Cursor::new (slice);
|
||||||
|
|
||||||
|
let mut max = None;
|
||||||
|
|
||||||
|
const IDX_SIZE: usize = 4;
|
||||||
|
|
||||||
|
assert_eq! (slice.len () % IDX_SIZE, 0);
|
||||||
|
|
||||||
|
for _ in 0..slice.len () / IDX_SIZE {
|
||||||
|
let idx = rdr.read_u32::<LittleEndian> ().unwrap ();
|
||||||
|
|
||||||
|
max = match max {
|
||||||
|
None => Some (idx),
|
||||||
|
Some (max) => Some (cmp::max (max, idx)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = {
|
||||||
|
let mut id = 0;
|
||||||
|
unsafe {
|
||||||
|
gl::GenBuffers (1, &mut id);
|
||||||
|
gl::BindBuffer (gl::ELEMENT_ARRAY_BUFFER, id);
|
||||||
|
|
||||||
|
gl::BufferData (gl::ELEMENT_ARRAY_BUFFER, slice.len ().try_into ().unwrap (), &slice [0] as *const u8 as *const c_void, gl::STATIC_DRAW);
|
||||||
|
}
|
||||||
|
assert! (id != 0);
|
||||||
|
id
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
len: slice.len () / IDX_SIZE,
|
||||||
|
max: max.unwrap (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bind (&self) {
|
||||||
|
unsafe {
|
||||||
|
gl::BindBuffer (gl::ELEMENT_ARRAY_BUFFER, self.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max (&self) -> u32 {
|
||||||
|
self.max
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for IndexBuffer {
|
||||||
|
fn drop (&mut self) {
|
||||||
|
if self.id == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
gl::DeleteBuffers (1, &self.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.id = 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,3 @@
|
||||||
use iota::iota;
|
|
||||||
use nom::{
|
use nom::{
|
||||||
IResult,
|
IResult,
|
||||||
bytes::complete::{tag},
|
bytes::complete::{tag},
|
||||||
|
@ -8,6 +7,7 @@ use nom::{
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
|
|
||||||
pub mod consts {
|
pub mod consts {
|
||||||
|
use iota::iota;
|
||||||
iota! {
|
iota! {
|
||||||
pub const VERSION: usize = iota;
|
pub const VERSION: usize = iota;
|
||||||
, FILESIZE
|
, FILESIZE
|
||||||
|
|
|
@ -3,7 +3,9 @@ extern crate iota;
|
||||||
|
|
||||||
pub mod file;
|
pub mod file;
|
||||||
pub mod glezz;
|
pub mod glezz;
|
||||||
|
pub mod gpu_buffers;
|
||||||
pub mod iqm;
|
pub mod iqm;
|
||||||
|
pub mod renderable_model;
|
||||||
pub mod shader;
|
pub mod shader;
|
||||||
pub mod texture;
|
pub mod texture;
|
||||||
pub mod timestep;
|
pub mod timestep;
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
use byteorder::{ByteOrder, LittleEndian};
|
||||||
|
|
||||||
|
use std::collections::*;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
use std::ffi::c_void;
|
||||||
|
|
||||||
|
use crate::gpu_buffers::*;
|
||||||
|
use crate::iqm;
|
||||||
|
|
||||||
|
// Takes ownership of mesh stuff in an opaque way that's abstract
|
||||||
|
// from the IQM model. IQM is zero-copy, but this is not.
|
||||||
|
// Since it's opaque, I can drop in a VBO/IBO setup when I'm not lazy.
|
||||||
|
|
||||||
|
struct RenderableMesh {
|
||||||
|
first_triangle: usize,
|
||||||
|
num_triangles: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RenderableModel {
|
||||||
|
num_pos: usize,
|
||||||
|
num_uv: usize,
|
||||||
|
num_normal: usize,
|
||||||
|
|
||||||
|
vertexes: VertexBuffer,
|
||||||
|
indexes: IndexBuffer,
|
||||||
|
|
||||||
|
meshes: Vec <RenderableMesh>,
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn vertex_attrib_pointer (id: Option <u32>, num_coords: i32, float_offset: usize) {
|
||||||
|
const FALSE_U8: u8 = 0;
|
||||||
|
const FLOAT_SIZE: i32 = 4;
|
||||||
|
|
||||||
|
if let Some (id) = id {
|
||||||
|
gl::VertexAttribPointer (id, num_coords, gl::FLOAT, FALSE_U8, FLOAT_SIZE * num_coords, (float_offset * 4) as *const u8 as *const c_void);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderableModel {
|
||||||
|
pub fn from_iqm (model: &iqm::Model) -> RenderableModel {
|
||||||
|
let pos_bytes = model.get_vertex_slice (iqm::types::POSITION);
|
||||||
|
let uv_bytes = model.get_vertex_slice (iqm::types::TEXCOORD);
|
||||||
|
let normal_bytes = model.get_vertex_slice (iqm::types::NORMAL);
|
||||||
|
|
||||||
|
let num_pos = pos_bytes.len () / 4;
|
||||||
|
let num_uv = uv_bytes.len () / 4;
|
||||||
|
let num_normal = normal_bytes.len () / 4;
|
||||||
|
|
||||||
|
let mut vertex_vec = vec! [0.0; num_pos + num_uv + num_normal];
|
||||||
|
|
||||||
|
LittleEndian::read_f32_into (pos_bytes, &mut vertex_vec [0..num_pos]);
|
||||||
|
LittleEndian::read_f32_into (uv_bytes, &mut vertex_vec [num_pos..num_pos + num_uv]);
|
||||||
|
LittleEndian::read_f32_into (normal_bytes, &mut vertex_vec [num_pos + num_uv..num_pos + num_uv + num_normal]);
|
||||||
|
|
||||||
|
let vertexes = VertexBuffer::from_slice (&vertex_vec);
|
||||||
|
|
||||||
|
let index_slice = model.get_all_indexes ();
|
||||||
|
let indexes = IndexBuffer::from_slice (index_slice);
|
||||||
|
|
||||||
|
let max_index: usize = indexes.max ().try_into ().unwrap ();
|
||||||
|
|
||||||
|
assert! (max_index * 3 < num_pos);
|
||||||
|
assert! (max_index * 2 < num_uv);
|
||||||
|
assert! (max_index * 3 < num_normal);
|
||||||
|
|
||||||
|
let meshes = model.meshes.iter ()
|
||||||
|
.map (|mesh| RenderableMesh {
|
||||||
|
first_triangle: mesh.first_triangle.try_into ().unwrap (),
|
||||||
|
num_triangles: mesh.num_triangles.try_into ().unwrap (),
|
||||||
|
})
|
||||||
|
.collect ();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
num_pos,
|
||||||
|
num_uv,
|
||||||
|
num_normal,
|
||||||
|
|
||||||
|
vertexes,
|
||||||
|
indexes,
|
||||||
|
meshes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw (&self, attrs: &HashMap <String, Option <u32>>, mesh_num: usize)
|
||||||
|
{
|
||||||
|
let mesh = &self.meshes [mesh_num];
|
||||||
|
|
||||||
|
self.vertexes.bind ();
|
||||||
|
self.indexes.bind ();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
vertex_attrib_pointer (attrs ["pos"], 3, 0);
|
||||||
|
vertex_attrib_pointer (attrs ["uv"], 2, self.num_pos);
|
||||||
|
vertex_attrib_pointer (attrs ["normal"], 3, self.num_pos + self.num_uv);
|
||||||
|
|
||||||
|
gl::DrawRangeElements (gl::TRIANGLES, 0, self.indexes.max (), mesh.num_triangles * 3, gl::UNSIGNED_INT, (mesh.first_triangle * 3 * 4) as *const u8 as *const c_void);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue