Commit 08be00ea authored by Mathias Goldau's avatar Mathias Goldau
Browse files

[ADD] Almost working lic

parent e2f9d82a
......@@ -38,33 +38,38 @@ WColor::WColor( float red, float green, float blue, float alpha )
m_alpha( alpha )
{
// check if the given values are correct in range
assert( m_green <= 1.0 && m_green >= 0.0 && "WColor comopnent out of range" );
assert( m_blue <= 1.0 && m_blue >= 0.0 && "WColor comopnent out of range" );
assert( m_red <= 1.0 && m_red >= 0.0 && "WColor comopnent out of range" );
assert( m_alpha <= 1.0 && m_alpha >= 0.0 && "WColor comopnent out of range" );
// TODO(lmath): reenable asserts to WAsserts as soon as LIC module doesn't procude invalid colors
// assert( m_green <= 1.0 && m_green >= 0.0 && "WColor comopnent out of range" );
// assert( m_blue <= 1.0 && m_blue >= 0.0 && "WColor comopnent out of range" );
// assert( m_red <= 1.0 && m_red >= 0.0 && "WColor comopnent out of range" );
// assert( m_alpha <= 1.0 && m_alpha >= 0.0 && "WColor comopnent out of range" );
}
void WColor::setGreen( float green )
{
assert( green <= 1.0 && green >= 0.0 );
// TODO(lmath): reenable asserts to WAsserts as soon as LIC module doesn't procude invalid colors
// assert( green <= 1.0 && green >= 0.0 );
m_green = green;
}
void WColor::setBlue( float blue )
{
assert( blue <= 1.0 && blue >= 0.0 );
// TODO(lmath): reenable asserts to WAsserts as soon as LIC module doesn't procude invalid colors
// assert( blue <= 1.0 && blue >= 0.0 );
m_blue = blue;
}
void WColor::setRed( float red )
{
assert( red <= 1.0 && red >= 0.0 );
// TODO(lmath): reenable asserts to WAsserts as soon as LIC module doesn't procude invalid colors
// assert( red <= 1.0 && red >= 0.0 );
m_red = red;
}
void WColor::setAlpha( float alpha )
{
assert( alpha <= 1.0 && alpha >= 0.0 );
// TODO(lmath): reenable asserts to WAsserts as soon as LIC module doesn't procude invalid colors
// assert( alpha <= 1.0 && alpha >= 0.0 );
m_alpha = alpha;
}
......
......@@ -61,7 +61,7 @@ boost::shared_ptr< WPrototyped > WDataSetVector::getPrototype()
return m_prototype;
}
wmath::WVector3D WDataSetVector::interpolate( const wmath::WPosition& pos, bool *success )
wmath::WVector3D WDataSetVector::interpolate( const wmath::WPosition& pos, bool *success ) const
{
boost::shared_ptr< WGridRegular3D > grid = boost::shared_dynamic_cast< WGridRegular3D >( m_grid );
......
......@@ -69,7 +69,7 @@ public:
*
* \return Vector beeing the interpolate.
*/
wmath::WVector3D interpolate( const wmath::WPosition &pos, bool *success );
wmath::WVector3D interpolate( const wmath::WPosition &pos, bool *success ) const;
/**
* Get the vector on the given position in value set.
......
......@@ -396,8 +396,9 @@ void WTriangleMesh2::doLoopSubD()
delete[] newVertexPositions;
( *m_vertNormals ).resize( ( *m_verts ).size() );
( *m_vertColors ).resize( ( *m_verts ).size() );
m_vertNormals->resize( m_verts->size() );
m_vertColors->resize( m_verts->size() );
m_triangleColors->resize( m_triangles.size() / 3 );
m_meshDirty = true;
}
......@@ -700,3 +701,8 @@ boost::shared_ptr< std::list< boost::shared_ptr< WTriangleMesh2 > > > tm_utils::
return result;
}
osg::ref_ptr< osg::Vec4Array > WTriangleMesh2::getTriangleColors() const
{
return m_triangleColors;
}
......@@ -182,6 +182,13 @@ public:
*/
void setTriangleColor( size_t index, WColor color );
/**
* Return triangle colors
*
* \return OSG Vec4 Array of triangle colors
*/
osg::ref_ptr< osg::Vec4Array > getTriangleColors() const;
/**
* getter
*
......
......@@ -31,6 +31,7 @@
#include "../common/WLogger.h"
#include "../modules/applyMask/WMApplyMask.h"
#include "../modules/arbitraryRois/WMArbitraryRois.h"
#include "../modules/boundingBox/WMBoundingBox.h"
#include "../modules/clusterParamDisplay/WMClusterParamDisplay.h"
#include "../modules/clusterSlicer/WMClusterSlicer.h"
......@@ -47,22 +48,22 @@
#include "../modules/fiberDisplay/WMFiberDisplay.h"
#include "../modules/fiberSelection/WMFiberSelection.h"
#include "../modules/gaussFiltering/WMGaussFiltering.h"
#include "../modules/geometryGlyphs/WMGeometryGlyphs.h"
#include "../modules/hud/WMHud.h"
#include "../modules/lic/WMLIC.h"
#include "../modules/marchingCubes/WMMarchingCubes.h"
#include "../modules/meshReader/WMMeshReader.h"
#include "../modules/navSlices/WMNavSlices.h"
#include "../modules/surfaceParticles/WMSurfaceParticles.h"
#include "../modules/template/WMTemplate.h"
#include "../modules/voxelizer/WMVoxelizer.h"
#include "../modules/triangleMeshRenderer/WMTriangleMeshRenderer.h"
#include "../modules/writeNIfTI/WMWriteNIfTI.h"
#include "../modules/vectorPlot/WMVectorPlot.h"
#include "../modules/geometryGlyphs/WMGeometryGlyphs.h"
#include "../modules/arbitraryRois/WMArbitraryRois.h"
#include "../modules/meshReader/WMMeshReader.h"
#include "WModuleFactory.h"
#include "../modules/voxelizer/WMVoxelizer.h"
#include "../modules/writeNIfTI/WMWriteNIfTI.h"
#include "combiner/WApplyPrototypeCombiner.h"
#include "exceptions/WPrototypeNotUnique.h"
#include "exceptions/WPrototypeUnknown.h"
#include "combiner/WApplyPrototypeCombiner.h"
#include "WModuleFactory.h"
// factory instance as singleton
boost::shared_ptr< WModuleFactory > WModuleFactory::m_instance = boost::shared_ptr< WModuleFactory >();
......@@ -116,6 +117,7 @@ void WModuleFactory::load()
m_prototypes.insert( boost::shared_ptr< WModule >( new WMGeometryGlyphs() ) );
m_prototypes.insert( boost::shared_ptr< WModule >( new WMArbitraryRois() ) );
m_prototypes.insert( boost::shared_ptr< WModule >( new WMMeshReader() ) );
m_prototypes.insert( boost::shared_ptr< WModule >( new WMLIC() ) );
lock.unlock();
......
......@@ -23,12 +23,19 @@
//---------------------------------------------------------------------------
#include <string>
#include <vector>
#include <osg/LightModel>
#include "../../graphicsEngine/WGEUtils.h"
#include "../../kernel/WKernel.h"
#include "fibernavigator/SurfaceLIC.h"
#include "WMLIC.h"
WMLIC::WMLIC():
WModule()
WMLIC::WMLIC()
: WModule(),
m_moduleNode( new WGEGroupNode() ),
m_surfaceGeode( 0 )
{
}
......@@ -62,14 +69,14 @@ void WMLIC::connectors()
new WModuleInputData< WDataSetVector >( shared_from_this(),
"inVectorDS", "The vectors used for computing the Streamlines used for the LIC" )
);
m_meshOC = boost::shared_ptr< WModuleOutputData < WTriangleMesh2 > >(
new WModuleOutputData< WTriangleMesh2 >( shared_from_this(),
"outMesh", "The LIC" )
);
// m_meshOC = boost::shared_ptr< WModuleOutputData < WTriangleMesh2 > >(
// new WModuleOutputData< WTriangleMesh2 >( shared_from_this(),
// "outMesh", "The LIC" )
// );
addConnector( m_meshIC );
addConnector( m_vectorIC );
addConnector( m_meshOC );
// addConnector( m_meshOC );
WModule::connectors();
}
......@@ -77,6 +84,63 @@ void WMLIC::properties()
{
}
void WMLIC::renderMesh( boost::shared_ptr< WTriangleMesh2 > mesh )
{
m_moduleNode->remove( m_surfaceGeode );
osg::Geometry* surfaceGeometry = new osg::Geometry();
m_surfaceGeode = osg::ref_ptr< osg::Geode >( new osg::Geode );
surfaceGeometry->setVertexArray( mesh->getVertexArray() );
osg::DrawElementsUInt* surfaceElement;
surfaceElement = new osg::DrawElementsUInt( osg::PrimitiveSet::TRIANGLES, 0 );
std::vector< size_t > tris = mesh->getTriangles();
surfaceElement->reserve( tris.size() );
for( unsigned int vertId = 0; vertId < tris.size(); ++vertId )
{
surfaceElement->push_back( tris[vertId] );
}
surfaceGeometry->addPrimitiveSet( surfaceElement );
// ------------------------------------------------
// normals
surfaceGeometry->setNormalArray( mesh->getVertexNormalArray() );
surfaceGeometry->setNormalBinding( osg::Geometry::BIND_PER_VERTEX );
m_surfaceGeode->addDrawable( surfaceGeometry );
osg::StateSet* state = m_surfaceGeode->getOrCreateStateSet();
// ------------------------------------------------
// colors
surfaceGeometry->setColorArray( mesh->getTriangleColors() );
surfaceGeometry->setColorBinding( osg::Geometry::BIND_PER_PRIMITIVE );
osg::ref_ptr<osg::LightModel> lightModel = new osg::LightModel();
lightModel->setTwoSided( true );
state->setAttributeAndModes( lightModel.get(), osg::StateAttribute::ON );
state->setMode( GL_BLEND, osg::StateAttribute::ON );
{
osg::ref_ptr< osg::Material > material = new osg::Material();
material->setDiffuse( osg::Material::FRONT, osg::Vec4( 1.0, 1.0, 1.0, 1.0 ) );
material->setSpecular( osg::Material::FRONT, osg::Vec4( 0.0, 0.0, 0.0, 1.0 ) );
material->setAmbient( osg::Material::FRONT, osg::Vec4( 0.1, 0.1, 0.1, 1.0 ) );
material->setEmission( osg::Material::FRONT, osg::Vec4( 0.0, 0.0, 0.0, 1.0 ) );
material->setShininess( osg::Material::FRONT, 25.0 );
state->setAttribute( material );
}
m_moduleNode->insert( m_surfaceGeode );
m_shader = osg::ref_ptr< WShader > ( new WShader( "licMeshRenderer" ) );
m_shader->apply( m_surfaceGeode );
WKernel::getRunningKernel()->getGraphicsEngine()->getScene()->insert( m_moduleNode );
}
void WMLIC::moduleMain()
{
m_moduleState.setResetable( true, true );
......@@ -104,6 +168,13 @@ void WMLIC::moduleMain()
debugLog() << "Received Data.";
m_inMesh = newMesh;
m_inVector = newVector;
m_inMesh->doLoopSubD();
m_inMesh->doLoopSubD();
SurfaceLIC lic( m_inVector, m_inMesh );
lic.execute();
lic.updateMeshColor( m_inMesh );
// m_meshOC.updateData( m_inMesh );
renderMesh( m_inMesh );
}
}
}
......@@ -90,6 +90,12 @@ protected:
*/
virtual void properties();
/**
* Generates an OSG geode for the mesh
*
* \param mesh the mesh which represents the LIC
*/
void renderMesh( boost::shared_ptr< WTriangleMesh2 > mesh );
private:
boost::shared_ptr< WModuleInputData< WTriangleMesh2 > > m_meshIC; //!< The InputConnector for the mesh on which to paint
......@@ -97,8 +103,11 @@ private:
boost::shared_ptr< WTriangleMesh2 > m_inMesh; //!< The mesh given from the input connector
boost::shared_ptr< WDataSetVector > m_inVector; //!< The vector field used to compute the LIC given from the input connector
// boost::shared_ptr< WModuleOutputData< WTriangleMesh2 > > m_meshOC; //!< OutputConnector for the LIC'ed mesh
boost::shared_ptr< WModuleOutputData< WTriangleMesh2 > > m_meshOC; //!< OutputConnector for the LIC'ed mesh
osg::ref_ptr< WGEGroupNode > m_moduleNode; //!< Pointer to the modules group node.
osg::ref_ptr< osg::Geode > m_surfaceGeode; //!< Pointer to geode containing the surface.
osg::ref_ptr< WShader > m_shader; //!< The shader used for the iso surface in m_geode
};
#endif // WMLIC_H
......@@ -16,6 +16,7 @@
#include "FArray.h"
#include "FMultiIndex.h"
#include "../../../common/math/WVector3D.h"
#include <vector>
class FRefTensor;
......@@ -60,6 +61,8 @@ public:
*/
FTensor(unsigned char dimension, unsigned char order, bool clear=false);
FTensor( const wmath::WVector3D& vec );
/**
*\par Description:
*Constructor: provides a tensor of dimension \b dimension and order
......
......@@ -19,6 +19,7 @@
#include "FMatrix.h"
#include "FtQLiAlgorithm.h"
#include "../../../common/math/WVector3D.h"
#ifdef OUTLINE
#define inline
......@@ -60,6 +61,17 @@ inline FTensor::FTensor(unsigned char dim, unsigned char order, bool clear)
FArray::clear();
}
inline FTensor::FTensor( const wmath::WVector3D& vec )
: FArray()
{
this->dimension = 3;
this->order = 1;
FArray::resize(3);
comp[0] = vec[0];
comp[1] = vec[1];
comp[2] = vec[2];
}
//---------------------------------------------------------------------------
inline FTensor::FTensor(unsigned char dim, unsigned char order, const std::vector<double>& comp) : FArray( comp )
......
......@@ -9,6 +9,7 @@
#include "SurfaceLIC.h"
#include "../fantom/FTensor.h"
#include "../../../common/WLogger.h"
SurfaceLIC::SurfaceLIC( boost::shared_ptr< WDataSetVector > vectors, boost::shared_ptr< WTriangleMesh2 > mesh )
: m_vectors( vectors )
......@@ -34,6 +35,15 @@ SurfaceLIC::~SurfaceLIC()
delete m_mesh;
}
void SurfaceLIC::updateMeshColor( boost::shared_ptr< WTriangleMesh2 > mesh ) const
{
WAssert( static_cast< int >( mesh->triangleSize() ) == m_mesh->getNumTriangles(), "Meshes have not same number of triangles" );
for( int i = 0; i < m_mesh->getNumTriangles(); ++i )
{
mesh->setTriangleColor( i, m_mesh->getTriangleColor( i ) );
}
}
//---------------------------------------------------------------------------
void SurfaceLIC::execute()
......@@ -70,6 +80,7 @@ void SurfaceLIC::execute()
streamline = new MyLICStreamline(m_vectors, m_mesh);
streamline->setParams(&hit_texture, min_length, threshold);
wlog::debug( "SurfaceLIC" ) << "start calculating lic";
// m_dh->printDebug(_T("start calculating lic"), 1);
// iterate over all texture points
......@@ -88,6 +99,7 @@ void SurfaceLIC::execute()
{
calculatePixelLuminance(FIndex(modulo * j + i));
}
wlog::debug( "SurfaceLIC" ) << "lic done";
//m_dh->printDebug(_T("lic done"), 1);
} catch (FException& e)
......
......@@ -22,6 +22,12 @@ public:
virtual ~SurfaceLIC();
virtual void execute();
std::vector< std::vector<float> > testLines;
/**
* Converts old mesh into new WTriangleMesh2
*
* \return reference to the mesh
*/
void updateMeshColor( boost::shared_ptr< WTriangleMesh2 > mesh ) const;
private:
......
......@@ -11,7 +11,9 @@
#include "TensorField.h"
#include "../fantom/FVector.h"
#include "../../../common/WAssert.h"
#include "../../../common/math/WPosition.h"
#include "../../../dataHandler/WDataSetVector.h"
#include "../../../dataHandler/WGridRegular3D.h"
TensorField::TensorField( boost::shared_ptr< WDataSetVector > vectors )
: m_cells( vectors->getGrid()->size() ),
......@@ -41,70 +43,60 @@ TensorField::~TensorField()
FTensor TensorField::getInterpolatedVector(float x, float y, float z)
{
bool success = false;
wmath::WVector3D result = m_vectors->interpolate( wmath::WPosition( x, y, z ), &success );
WAssert( success, "Vector interpolation was not successful" );
return FTensor( FArray( result[0], result[1], result[2] ) );
// TODO(lmath) use std::min/std::max and grid for that
// int nx = wxMin(m_dh->columns-1, wxMax(0,(int)x));
// int ny = wxMin(m_dh->rows -1, wxMax(0,(int)y));
// int nz = wxMin(m_dh->frames -1, wxMax(0,(int)z));
//
// float xMult = x - (int)x;
// float yMult = y - (int)y;
// float zMult = z - (int)z;
//
// int nextX = wxMin(m_dh->columns-1, nx+1);
// int nextY = wxMin(m_dh->rows-1, ny+1);
// int nextZ = wxMin(m_dh->frames-1, nz+1);
//
// int xyzIndex = nx + ny * m_dh->columns + nz * m_dh->columns * m_dh->rows;
// int x1yzIndex = nextX + ny * m_dh->columns + nz * m_dh->columns * m_dh->rows;
// int xy1zIndex = nx + nextY * m_dh->columns + nz * m_dh->columns * m_dh->rows;
// int x1y1zIndex = nextX + nextY * m_dh->columns + nz * m_dh->columns * m_dh->rows;
// int xyz1Index = nx + ny * m_dh->columns + nextZ * m_dh->columns * m_dh->rows;
// int x1yz1Index = nextX + ny * m_dh->columns + nextZ * m_dh->columns * m_dh->rows;
// int xy1z1Index = nx + nextY * m_dh->columns + nextZ * m_dh->columns * m_dh->rows;
// int x1y1z1Index = nextX + nextY * m_dh->columns + nextZ * m_dh->columns * m_dh->rows;
//
// FTensor txyz = getTensorAtIndex(xyzIndex);
// FTensor tx1yz = getTensorAtIndex(x1yzIndex);
// FTensor txy1z = getTensorAtIndex(xy1zIndex);
// FTensor tx1y1z = getTensorAtIndex(x1y1zIndex);
// FTensor txyz1 = getTensorAtIndex(xyz1Index);
// FTensor tx1yz1 = getTensorAtIndex(x1yz1Index);
// FTensor txy1z1 = getTensorAtIndex(xy1z1Index);
// FTensor tx1y1z1 = getTensorAtIndex(x1y1z1Index);
//
//
// FMatrix matxyz = createMatrix(txyz, txyz);
// FMatrix matx1yz = createMatrix(tx1yz, tx1yz);
// FMatrix matxy1z = createMatrix(txy1z, txy1z);
// FMatrix matx1y1z = createMatrix(tx1y1z, tx1y1z);
// FMatrix matxyz1 = createMatrix(txyz1, txyz1);
// FMatrix matx1yz1 = createMatrix(tx1yz1, tx1yz1);
// FMatrix matxy1z1 = createMatrix(txy1z1, txy1z1);
// FMatrix matx1y1z1 = createMatrix(tx1y1z1, tx1y1z1);
//
// FMatrix i1 = matxyz * (1. - zMult) + matxyz1 * zMult;
// FMatrix i2 = matxy1z * (1. - zMult) + matxy1z1 * zMult;
// FMatrix j1 = matx1yz * (1. - zMult) + matx1yz1 * zMult;
// FMatrix j2 = matx1y1z * (1. - zMult) + matx1y1z1 * zMult;
//
// FMatrix w1 = i1 * (1. - yMult) + i2 * yMult;
// FMatrix w2 = j1 * (1. - yMult) + j2 * yMult;
//
// FMatrix matResult = w1 * (1. - xMult) + w2 * xMult;
// std::vector<FArray>evecs;
// FArray vals(0,0,0);
//
// matResult.getEigenSystem(vals, evecs);
//
// if (vals[0] >= vals[1] && vals[0] > vals[2]) return FTensor(evecs[0]);
// else if (vals[1] > vals[0] && vals[1] >= vals[2]) return FTensor(evecs[1]);
// else if (vals[2] >= vals[0] && vals[2] > vals[1]) return FTensor(evecs[2]);
// else return FTensor(evecs[0]);
boost::shared_ptr< WGridRegular3D > grid = boost::shared_dynamic_cast< WGridRegular3D >( m_vectors->getGrid() );
WAssert( grid, "This data set has a grid whose type is not yet supported for interpolation." );
WAssert( ( m_vectors->getValueSet()->order() == 1 && m_vectors->getValueSet()->dimension() == 3 ),
"Only implemented for 3D Vectors so far." );
wmath::WPosition pos( x, y, z );
WAssert( grid->encloses( pos ), "Grid does not enclose position to interpolate" );
std::vector< size_t > vertexIds = grid->getCellVertexIds( grid->getCellId( pos ) );
wmath::WPosition localPos = pos - grid->getPosition( vertexIds[0] );
// double xMult = localPos[0] / grid->getOffsetX();
// double yMult = localPos[1] / grid->getOffsetY();
// double zMult = localPos[2] / grid->getOffsetZ();
float xMult = x - (int)x;
float yMult = y - (int)y;
float zMult = z - (int)z;
FTensor txyz = FTensor( m_vectors->getVectorAt( vertexIds[0] ) );
FTensor tx1yz = FTensor( m_vectors->getVectorAt( vertexIds[1] ) );
FTensor txy1z = FTensor( m_vectors->getVectorAt( vertexIds[2] ) );
FTensor tx1y1z = FTensor( m_vectors->getVectorAt( vertexIds[3] ) );
FTensor txyz1 = FTensor( m_vectors->getVectorAt( vertexIds[4] ) );
FTensor tx1yz1 = FTensor( m_vectors->getVectorAt( vertexIds[5] ) );
FTensor txy1z1 = FTensor( m_vectors->getVectorAt( vertexIds[6] ) );
FTensor tx1y1z1 = FTensor( m_vectors->getVectorAt( vertexIds[7] ) );
FMatrix matxyz = createMatrix(txyz, txyz);
FMatrix matx1yz = createMatrix(tx1yz, tx1yz);
FMatrix matxy1z = createMatrix(txy1z, txy1z);
FMatrix matx1y1z = createMatrix(tx1y1z, tx1y1z);
FMatrix matxyz1 = createMatrix(txyz1, txyz1);
FMatrix matx1yz1 = createMatrix(tx1yz1, tx1yz1);
FMatrix matxy1z1 = createMatrix(txy1z1, txy1z1);
FMatrix matx1y1z1 = createMatrix(tx1y1z1, tx1y1z1);
FMatrix i1 = matxyz * (1. - zMult) + matxyz1 * zMult;
FMatrix i2 = matxy1z * (1. - zMult) + matxy1z1 * zMult;
FMatrix j1 = matx1yz * (1. - zMult) + matx1yz1 * zMult;
FMatrix j2 = matx1y1z * (1. - zMult) + matx1y1z1 * zMult;
FMatrix w1 = i1 * (1. - yMult) + i2 * yMult;
FMatrix w2 = j1 * (1. - yMult) + j2 * yMult;
FMatrix matResult = w1 * (1. - xMult) + w2 * xMult;
std::vector<FArray>evecs;
FArray vals(0,0,0);
matResult.getEigenSystem(vals, evecs);
if (vals[0] >= vals[1] && vals[0] > vals[2]) return FTensor(evecs[0]);
else if (vals[1] > vals[0] && vals[1] >= vals[2]) return FTensor(evecs[1]);
else if (vals[2] >= vals[0] && vals[2] > vals[1]) return FTensor(evecs[2]);
else return FTensor(evecs[0]);
}
FMatrix TensorField::createMatrix(FTensor lhs, FTensor rhs)
......
......@@ -20,6 +20,8 @@ TriangleMesh::TriangleMesh ( boost::shared_ptr< WTriangleMesh2 > mesh, boost::sh
m_triangleTensorsCalculated = false;
defaultColor = WColor( 0.78, 0.78, 0.78, 1.0 );
resizeVerts( mesh->vertSize() );
resizeTriangles( mesh->triangleSize() );
// copy vertices
for( size_t i = 0; i < mesh->vertSize(); ++i )
{
......@@ -596,10 +598,10 @@ Triangle TriangleMesh::getTriangle(const int triNum)
return triangles[triNum];
}
//wxColour TriangleMesh::getTriangleColor(const int triNum)
//{
// return triangleColor[triNum];
//}
WColor TriangleMesh::getTriangleColor(const int triNum)
{
return triangleColor[triNum];
}
std::vector<unsigned int> TriangleMesh::getStar(const int vertNum)
{
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment