|
|
|
|
@@ -1,10 +1,4 @@
|
|
|
|
|
use core::ops::{Bound,RangeBounds};
|
|
|
|
|
|
|
|
|
|
use strafesnet_common::integer::{Planar64Vec3,Ratio,Fixed,vec3::Vector3};
|
|
|
|
|
use crate::model::into_giga_time;
|
|
|
|
|
use crate::model::{SubmeshVertId,SubmeshEdgeId,SubmeshDirectedEdgeId,SubmeshFaceId,TransformedMesh,GigaTime};
|
|
|
|
|
use crate::mesh_query::{MeshQuery,MeshTopology,DirectedEdge,UndirectedEdge};
|
|
|
|
|
use crate::physics::{Time,Trajectory};
|
|
|
|
|
|
|
|
|
|
struct AsRefHelper<T>(T);
|
|
|
|
|
impl<T> AsRef<T> for AsRefHelper<T>{
|
|
|
|
|
@@ -13,31 +7,30 @@ impl<T> AsRef<T> for AsRefHelper<T>{
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//Note that a face on a minkowski mesh refers to a pair of fevs on the meshes it's summed from
|
|
|
|
|
//(face,vertex)
|
|
|
|
|
//(edge,edge)
|
|
|
|
|
//(vertex,face)
|
|
|
|
|
#[derive(Clone,Copy,Debug,Eq,PartialEq)]
|
|
|
|
|
pub enum MinkowskiVert{
|
|
|
|
|
VertVert(SubmeshVertId,SubmeshVertId),
|
|
|
|
|
#[derive(Clone,Copy)]
|
|
|
|
|
pub struct MinkowskiVert<M0:MeshTopology,M1:MeshTopology>{
|
|
|
|
|
vert0:M0::Vert,
|
|
|
|
|
vert1:M1::Vert,
|
|
|
|
|
}
|
|
|
|
|
// TODO: remove this
|
|
|
|
|
impl core::ops::Neg for MinkowskiVert{
|
|
|
|
|
type Output=Self;
|
|
|
|
|
fn neg(self)->Self::Output{
|
|
|
|
|
match self{
|
|
|
|
|
MinkowskiVert::VertVert(v0,v1)=>MinkowskiVert::VertVert(v1,v0),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#[derive(Clone,Copy)]
|
|
|
|
|
pub enum MinkowskiEdge<M0:MeshTopology,M1:MeshTopology>{
|
|
|
|
|
VertEdge(M0::Vert,M1::Edge),
|
|
|
|
|
EdgeVert(M0::Edge,M1::Vert),
|
|
|
|
|
}
|
|
|
|
|
#[derive(Clone,Copy,Debug)]
|
|
|
|
|
pub enum MinkowskiEdge{
|
|
|
|
|
VertEdge(SubmeshVertId,SubmeshEdgeId),
|
|
|
|
|
EdgeVert(SubmeshEdgeId,SubmeshVertId),
|
|
|
|
|
//EdgeEdge when edges are parallel
|
|
|
|
|
#[derive(Clone,Copy)]
|
|
|
|
|
pub enum MinkowskiDirectedEdge<M0:MeshTopology,M1:MeshTopology>{
|
|
|
|
|
VertEdge(M0::Vert,M1::DirectedEdge),
|
|
|
|
|
EdgeVert(M0::DirectedEdge,M1::Vert),
|
|
|
|
|
}
|
|
|
|
|
impl UndirectedEdge for MinkowskiEdge{
|
|
|
|
|
type DirectedEdge=MinkowskiDirectedEdge;
|
|
|
|
|
#[derive(Clone,Copy)]
|
|
|
|
|
pub enum MinkowskiFace<M0:MeshTopology,M1:MeshTopology>{
|
|
|
|
|
VertFace(M0::Vert,M1::Face),
|
|
|
|
|
EdgeEdge(M0::Edge,M1::Edge,bool),
|
|
|
|
|
FaceVert(M0::Face,M1::Vert),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<M0:MeshTopology,M1:MeshTopology> UndirectedEdge for MinkowskiEdge<M0,M1>{
|
|
|
|
|
type DirectedEdge=MinkowskiDirectedEdge<M0,M1>;
|
|
|
|
|
fn as_directed(self,parity:bool)->Self::DirectedEdge{
|
|
|
|
|
match self{
|
|
|
|
|
MinkowskiEdge::VertEdge(v0,e1)=>MinkowskiDirectedEdge::VertEdge(v0,e1.as_directed(parity)),
|
|
|
|
|
@@ -45,14 +38,8 @@ impl UndirectedEdge for MinkowskiEdge{
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#[derive(Clone,Copy,Debug)]
|
|
|
|
|
pub enum MinkowskiDirectedEdge{
|
|
|
|
|
VertEdge(SubmeshVertId,SubmeshDirectedEdgeId),
|
|
|
|
|
EdgeVert(SubmeshDirectedEdgeId,SubmeshVertId),
|
|
|
|
|
//EdgeEdge when edges are parallel
|
|
|
|
|
}
|
|
|
|
|
impl DirectedEdge for MinkowskiDirectedEdge{
|
|
|
|
|
type UndirectedEdge=MinkowskiEdge;
|
|
|
|
|
impl<M0:MeshTopology,M1:MeshTopology> DirectedEdge for MinkowskiDirectedEdge<M0,M1>{
|
|
|
|
|
type UndirectedEdge=MinkowskiEdge<M0,M1>;
|
|
|
|
|
fn as_undirected(self)->Self::UndirectedEdge{
|
|
|
|
|
match self{
|
|
|
|
|
MinkowskiDirectedEdge::VertEdge(v0,e1)=>MinkowskiEdge::VertEdge(v0,e1.as_undirected()),
|
|
|
|
|
@@ -61,156 +48,27 @@ impl DirectedEdge for MinkowskiDirectedEdge{
|
|
|
|
|
}
|
|
|
|
|
fn parity(&self)->bool{
|
|
|
|
|
match self{
|
|
|
|
|
MinkowskiDirectedEdge::VertEdge(_,e)
|
|
|
|
|
|MinkowskiDirectedEdge::EdgeVert(e,_)=>e.parity(),
|
|
|
|
|
MinkowskiDirectedEdge::VertEdge(_,e)=>e.parity(),
|
|
|
|
|
MinkowskiDirectedEdge::EdgeVert(e,_)=>e.parity(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#[derive(Clone,Copy,Debug,Hash)]
|
|
|
|
|
pub enum MinkowskiFace{
|
|
|
|
|
VertFace(SubmeshVertId,SubmeshFaceId),
|
|
|
|
|
EdgeEdge(SubmeshEdgeId,SubmeshEdgeId,bool),
|
|
|
|
|
FaceVert(SubmeshFaceId,SubmeshVertId),
|
|
|
|
|
//EdgeFace
|
|
|
|
|
//FaceEdge
|
|
|
|
|
//FaceFace
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct MinkowskiMesh<'a>{
|
|
|
|
|
mesh0:TransformedMesh<'a>,
|
|
|
|
|
mesh1:TransformedMesh<'a>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: remove this
|
|
|
|
|
impl<'a> core::ops::Neg for &MinkowskiMesh<'a>{
|
|
|
|
|
type Output=MinkowskiMesh<'a>;
|
|
|
|
|
fn neg(self)->Self::Output{
|
|
|
|
|
MinkowskiMesh::minkowski_sum(self.mesh1,self.mesh0)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl MinkowskiMesh<'_>{
|
|
|
|
|
pub fn minkowski_sum<'a>(mesh0:TransformedMesh<'a>,mesh1:TransformedMesh<'a>)->MinkowskiMesh<'a>{
|
|
|
|
|
MinkowskiMesh{
|
|
|
|
|
mesh0,
|
|
|
|
|
mesh1,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
pub fn predict_collision_in(&self,trajectory:&Trajectory,range:impl RangeBounds<Time>)->Option<(MinkowskiFace,GigaTime)>{
|
|
|
|
|
let start_position=match range.start_bound(){
|
|
|
|
|
Bound::Included(time)=>trajectory.extrapolated_position(*time),
|
|
|
|
|
Bound::Excluded(time)=>trajectory.extrapolated_position(*time),
|
|
|
|
|
Bound::Unbounded=>trajectory.position,
|
|
|
|
|
};
|
|
|
|
|
let fev=crate::minimum_difference::closest_fev_not_inside(self,start_position)?;
|
|
|
|
|
//continue forwards along the body parabola
|
|
|
|
|
fev.crawl(self,trajectory,range.start_bound(),range.end_bound()).hit()
|
|
|
|
|
}
|
|
|
|
|
pub fn predict_collision_out(&self,trajectory:&Trajectory,range:impl RangeBounds<Time>)->Option<(MinkowskiFace,GigaTime)>{
|
|
|
|
|
let (lower_bound,upper_bound)=(range.start_bound(),range.end_bound());
|
|
|
|
|
// TODO: handle unbounded collision using infinity fev
|
|
|
|
|
let start_position=match upper_bound{
|
|
|
|
|
Bound::Included(time)=>trajectory.extrapolated_position(*time),
|
|
|
|
|
Bound::Excluded(time)=>trajectory.extrapolated_position(*time),
|
|
|
|
|
Bound::Unbounded=>trajectory.position,
|
|
|
|
|
};
|
|
|
|
|
let fev=crate::minimum_difference::closest_fev_not_inside(self,start_position)?;
|
|
|
|
|
// swap and negate bounds to do a time inversion
|
|
|
|
|
let (lower_bound,upper_bound)=(upper_bound.map(|&t|-t),lower_bound.map(|&t|-t));
|
|
|
|
|
let time_reversed_trajectory=-trajectory;
|
|
|
|
|
//continue backwards along the body parabola
|
|
|
|
|
fev.crawl(self,&time_reversed_trajectory,lower_bound.as_ref(),upper_bound.as_ref()).hit()
|
|
|
|
|
//no need to test -time<time_limit because of the first step
|
|
|
|
|
.map(|(face,time)|(face,-time))
|
|
|
|
|
}
|
|
|
|
|
pub fn predict_collision_face_out(&self,trajectory:&Trajectory,range:impl RangeBounds<Time>,contact_face_id:MinkowskiFace)->Option<(MinkowskiDirectedEdge,GigaTime)>{
|
|
|
|
|
// TODO: make better
|
|
|
|
|
use crate::face_crawler::{low,upp};
|
|
|
|
|
//no algorithm needed, there is only one state and two cases (Edge,None)
|
|
|
|
|
//determine when it passes an edge ("sliding off" case)
|
|
|
|
|
let start_time=range.start_bound().map(|&t|(t-trajectory.time).to_ratio());
|
|
|
|
|
let mut best_time=range.end_bound().map(|&t|into_giga_time(t,trajectory.time));
|
|
|
|
|
let mut best_edge=None;
|
|
|
|
|
let face_n=self.face_nd(contact_face_id).0;
|
|
|
|
|
self.for_each_face_edge(contact_face_id,|directed_edge_id|{
|
|
|
|
|
let edge_n=self.directed_edge_n(directed_edge_id);
|
|
|
|
|
//f x e points in
|
|
|
|
|
let n=face_n.cross(edge_n);
|
|
|
|
|
let &[v0,v1]=self.edge_verts(directed_edge_id.as_undirected()).as_ref();
|
|
|
|
|
let d=n.dot(self.vert(v0)+self.vert(v1));
|
|
|
|
|
//WARNING! d outside of *2
|
|
|
|
|
//WARNING: truncated precision
|
|
|
|
|
//wrap for speed
|
|
|
|
|
for dt in Fixed::<4,128>::zeroes2(((n.dot(trajectory.position))*2-d).wrap_4(),n.dot(trajectory.velocity).wrap_4()*2,n.dot(trajectory.acceleration).wrap_4()){
|
|
|
|
|
if low(&start_time,&dt)&&upp(&dt,&best_time)&&n.dot(trajectory.extrapolated_velocity_ratio_dt(dt)).is_negative(){
|
|
|
|
|
best_time=Bound::Included(dt);
|
|
|
|
|
best_edge=Some((directed_edge_id,dt));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
best_edge
|
|
|
|
|
}
|
|
|
|
|
pub fn contains_point(&self,point:Planar64Vec3)->bool{
|
|
|
|
|
crate::minimum_difference::contains_point(self,point)
|
|
|
|
|
pub struct Minkowski<M0,M1>{
|
|
|
|
|
mesh0:M0,
|
|
|
|
|
mesh1:M1,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<M0,M1> Minkowski<M0,M1>{
|
|
|
|
|
pub fn sum(mesh0:M0,mesh1:M1)->Self{
|
|
|
|
|
Self{mesh0,mesh1}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
impl MeshQuery for MinkowskiMesh<'_>{
|
|
|
|
|
type Direction=Planar64Vec3;
|
|
|
|
|
type Position=Planar64Vec3;
|
|
|
|
|
type Normal=Vector3<Fixed<3,96>>;
|
|
|
|
|
type Offset=Fixed<4,128>;
|
|
|
|
|
// TODO: relative d
|
|
|
|
|
fn face_nd(&self,face_id:MinkowskiFace)->(Self::Normal,Self::Offset){
|
|
|
|
|
match face_id{
|
|
|
|
|
MinkowskiFace::VertFace(v0,f1)=>{
|
|
|
|
|
let (n,d)=self.mesh1.face_nd(f1);
|
|
|
|
|
(-n,d-n.dot(self.mesh0.vert(v0)))
|
|
|
|
|
},
|
|
|
|
|
MinkowskiFace::EdgeEdge(e0,e1,parity)=>{
|
|
|
|
|
let edge0_n=self.mesh0.edge_n(e0);
|
|
|
|
|
let edge1_n=self.mesh1.edge_n(e1);
|
|
|
|
|
let &[e0v0,e0v1]=self.mesh0.edge_verts(e0).as_ref();
|
|
|
|
|
let &[e1v0,e1v1]=self.mesh1.edge_verts(e1).as_ref();
|
|
|
|
|
let n=edge0_n.cross(edge1_n);
|
|
|
|
|
let e0d=n.dot(self.mesh0.vert(e0v0)+self.mesh0.vert(e0v1));
|
|
|
|
|
let e1d=n.dot(self.mesh1.vert(e1v0)+self.mesh1.vert(e1v1));
|
|
|
|
|
((n*(parity as i64*4-2)).widen_3(),((e0d-e1d)*(parity as i64*2-1)).widen_4())
|
|
|
|
|
},
|
|
|
|
|
MinkowskiFace::FaceVert(f0,v1)=>{
|
|
|
|
|
let (n,d)=self.mesh0.face_nd(f0);
|
|
|
|
|
(n,d-n.dot(self.mesh1.vert(v1)))
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
fn vert(&self,vert_id:MinkowskiVert)->Planar64Vec3{
|
|
|
|
|
match vert_id{
|
|
|
|
|
MinkowskiVert::VertVert(v0,v1)=>{
|
|
|
|
|
self.mesh0.vert(v0)-self.mesh1.vert(v1)
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
fn hint_point(&self)->Planar64Vec3{
|
|
|
|
|
self.mesh0.hint_point()-self.mesh1.hint_point()
|
|
|
|
|
}
|
|
|
|
|
fn farthest_vert(&self,dir:Planar64Vec3)->MinkowskiVert{
|
|
|
|
|
MinkowskiVert::VertVert(self.mesh0.farthest_vert(dir),self.mesh1.farthest_vert(-dir))
|
|
|
|
|
}
|
|
|
|
|
fn edge_n(&self,edge_id:Self::Edge)->Self::Direction{
|
|
|
|
|
let &[v0,v1]=self.edge_verts(edge_id).as_ref();
|
|
|
|
|
self.vert(v1)-self.vert(v0)
|
|
|
|
|
}
|
|
|
|
|
fn directed_edge_n(&self,directed_edge_id:Self::DirectedEdge)->Self::Direction{
|
|
|
|
|
let &[v0,v1]=self.edge_verts(directed_edge_id.as_undirected()).as_ref();
|
|
|
|
|
(self.vert(v1)-self.vert(v0))*((directed_edge_id.parity() as i64)*2-1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
impl MeshTopology for MinkowskiMesh<'_>{
|
|
|
|
|
type Face=MinkowskiFace;
|
|
|
|
|
type Edge=MinkowskiEdge;
|
|
|
|
|
type DirectedEdge=MinkowskiDirectedEdge;
|
|
|
|
|
type Vert=MinkowskiVert;
|
|
|
|
|
impl<M0:MeshTopology,M1:MeshTopology> MeshTopology for Minkowski<M0,M1>{
|
|
|
|
|
type Vert=MinkowskiVert<M0,M1>;
|
|
|
|
|
type Edge=MinkowskiEdge<M0,M1>;
|
|
|
|
|
type DirectedEdge=MinkowskiDirectedEdge<M0,M1>;
|
|
|
|
|
type Face=MinkowskiFace<M0,M1>;
|
|
|
|
|
fn for_each_vert_edge(&self,vert_id:Self::Vert,mut f:impl FnMut(Self::DirectedEdge)){
|
|
|
|
|
match vert_id{
|
|
|
|
|
MinkowskiVert::VertVert(v0,v1)=>{
|
|
|
|
|
@@ -375,33 +233,67 @@ impl MeshTopology for MinkowskiMesh<'_>{
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn is_empty_volume(normals:&[Vector3<Fixed<3,96>>])->bool{
|
|
|
|
|
let len=normals.len();
|
|
|
|
|
for i in 0..len-1{
|
|
|
|
|
for j in i+1..len{
|
|
|
|
|
let n=normals[i].cross(normals[j]);
|
|
|
|
|
let mut d_comp=None;
|
|
|
|
|
for k in 0..len{
|
|
|
|
|
if k!=i&&k!=j{
|
|
|
|
|
let d=n.dot(normals[k]).is_negative();
|
|
|
|
|
if let &Some(comp)=&d_comp{
|
|
|
|
|
// This is testing if d_comp*d < 0
|
|
|
|
|
if comp^d{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}else{
|
|
|
|
|
d_comp=Some(d);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
use strafesnet_common::integer::vec3::Vector3;
|
|
|
|
|
use strafesnet_common::integer::Fixed;
|
|
|
|
|
impl<M0:MeshQuery,M1:MeshQuery> MeshQuery for Minkowski<M0,M1>
|
|
|
|
|
where
|
|
|
|
|
M0:MeshQuery<
|
|
|
|
|
Direction=Vector3<Fixed<1,32>>,
|
|
|
|
|
Position=Vector3<Fixed<1,32>>,
|
|
|
|
|
Normal=Vector3<Fixed<3,96>>,
|
|
|
|
|
Offset=Fixed<4,128>,
|
|
|
|
|
>,
|
|
|
|
|
M1:MeshQuery<
|
|
|
|
|
Direction=Vector3<Fixed<1,32>>,
|
|
|
|
|
Position=Vector3<Fixed<1,32>>,
|
|
|
|
|
Normal=Vector3<Fixed<3,96>>,
|
|
|
|
|
Offset=Fixed<4,128>,
|
|
|
|
|
>,
|
|
|
|
|
{
|
|
|
|
|
type Direction=M0::Direction;
|
|
|
|
|
type Position=M0::Position;
|
|
|
|
|
type Normal=M0::Normal;
|
|
|
|
|
type Offset=M0::Offset;
|
|
|
|
|
fn vert(&self,vert_id:MinkowskiVert<M0,M1>)->Planar64Vec3{
|
|
|
|
|
self.mesh0.vert(vert_id.vert0)-self.mesh1.vert(vert_id.vert1)
|
|
|
|
|
}
|
|
|
|
|
fn face_nd(&self,face_id:MinkowskiFace<M0,M1>)->(Self::Normal,Self::Offset){
|
|
|
|
|
match face_id{
|
|
|
|
|
MinkowskiFace::VertFace(v0,f1)=>{
|
|
|
|
|
let (n,d)=self.mesh1.face_nd(f1);
|
|
|
|
|
(-n,d-n.dot(self.mesh0.vert(v0)))
|
|
|
|
|
},
|
|
|
|
|
MinkowskiFace::EdgeEdge(e0,e1,parity)=>{
|
|
|
|
|
let edge0_n=self.mesh0.edge_n(e0);
|
|
|
|
|
let edge1_n=self.mesh1.edge_n(e1);
|
|
|
|
|
let &[e0v0,e0v1]=self.mesh0.edge_verts(e0).as_ref();
|
|
|
|
|
let &[e1v0,e1v1]=self.mesh1.edge_verts(e1).as_ref();
|
|
|
|
|
let n=edge0_n.cross(edge1_n);
|
|
|
|
|
let e0d=n.dot(self.mesh0.vert(e0v0)+self.mesh0.vert(e0v1));
|
|
|
|
|
let e1d=n.dot(self.mesh1.vert(e1v0)+self.mesh1.vert(e1v1));
|
|
|
|
|
((n*(parity as i64*4-2)).widen_3(),((e0d-e1d)*(parity as i64*2-1)).widen_4())
|
|
|
|
|
},
|
|
|
|
|
MinkowskiFace::FaceVert(f0,v1)=>{
|
|
|
|
|
let (n,d)=self.mesh0.face_nd(f0);
|
|
|
|
|
(n,d-n.dot(self.mesh1.vert(v1)))
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_is_empty_volume(){
|
|
|
|
|
use strafesnet_common::integer::vec3;
|
|
|
|
|
assert!(!is_empty_volume(&[vec3::X.widen_3(),vec3::Y.widen_3(),vec3::Z.widen_3()]));
|
|
|
|
|
assert!(is_empty_volume(&[vec3::X.widen_3(),vec3::Y.widen_3(),vec3::Z.widen_3(),vec3::NEG_X.widen_3()]));
|
|
|
|
|
fn hint_point(&self)->Planar64Vec3{
|
|
|
|
|
self.mesh0.hint_point()-self.mesh1.hint_point()
|
|
|
|
|
}
|
|
|
|
|
fn farthest_vert(&self,dir:Planar64Vec3)->MinkowskiVert{
|
|
|
|
|
MinkowskiVert{
|
|
|
|
|
vert0:self.mesh0.farthest_vert(dir),
|
|
|
|
|
vert1:self.mesh1.farthest_vert(-dir),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
fn edge_n(&self,edge_id:Self::Edge)->Self::Direction{
|
|
|
|
|
let &[v0,v1]=self.edge_verts(edge_id).as_ref();
|
|
|
|
|
self.vert(v1)-self.vert(v0)
|
|
|
|
|
}
|
|
|
|
|
fn directed_edge_n(&self,directed_edge_id:Self::DirectedEdge)->Self::Direction{
|
|
|
|
|
let &[v0,v1]=self.edge_verts(directed_edge_id.as_undirected()).as_ref();
|
|
|
|
|
(self.vert(v1)-self.vert(v0))*((directed_edge_id.parity() as i64)*2-1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|