gtkradiant/contrib/bobtoolz/DBrush.cpp

862 lines
20 KiB
C++
Raw Normal View History

/*
BobToolz plugin for GtkRadiant
Copyright (C) 2001 Gordon Biggans
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
// DBrush.cpp: implementation of the DBrush class.
//
//////////////////////////////////////////////////////////////////////
#include "DBrush.h"
#ifdef WIN32
#pragma warning(disable : 4786)
#endif
#include <list>
#include "str.h"
#include "DPoint.h"
#include "DPlane.h"
#include "DEPair.h"
#include "DPatch.h"
#include "DEntity.h"
#include "DWinding.h"
#include "dialogs/dialogs-gtk.h"
#include "misc.h"
#include "iundo.h"
#include "generic/referencecounted.h"
#include "scenelib.h"
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
DBrush::DBrush(int ID)
{
m_nBrushID = ID;
bBoundsBuilt = false;
QER_entity = NULL;
QER_brush = NULL;
}
DBrush::~DBrush()
{
ClearFaces();
ClearPoints();
}
//////////////////////////////////////////////////////////////////////
// Implementation
//////////////////////////////////////////////////////////////////////
DPlane* DBrush::AddFace(const vec3_t va, const vec3_t vb, const vec3_t vc, const _QERFaceData* texData)
{
#ifdef _DEBUG
// Sys_Printf("(%f %f %f) (%f %f %f) (%f %f %f)\n", va[0], va[1], va[2], vb[0], vb[1], vb[2], vc[0], vc[1], vc[2]);
#endif
bBoundsBuilt = false;
DPlane* newFace = new DPlane(va, vb, vc, texData);
faceList.push_back(newFace);
return newFace;
}
int DBrush::BuildPoints()
{
ClearPoints();
if(faceList.size() <= 3) // if less than 3 faces, there can be no points
return 0; // with only 3 faces u can't have a bounded soild
for(std::list<DPlane *>::const_iterator p1=faceList.begin(); p1!=faceList.end(); p1++)
{
std::list<DPlane *>::const_iterator p2=p1;
for(p2++; p2!=faceList.end(); p2++)
{
std::list<DPlane *>::const_iterator p3=p2;
for(p3++; p3!=faceList.end(); p3++)
{
vec3_t pnt;
if((*p1)->PlaneIntersection(*p2, *p3, pnt))
{
int pos = PointPosition(pnt);
if(pos == POINT_IN_BRUSH)
{ // ???? shouldn't happen here
globalErrorStream() << "ERROR:: Build Brush Points: Point IN brush!!!\n";
}
else if(pos == POINT_ON_BRUSH)
{ // normal point
if(!HasPoint(pnt))
AddPoint(pnt);
/* else
Sys_Printf("Duplicate Point Found, pyramids ahoy!!!!!\n");*/
// point lies on more that 3 planes
}
// otherwise point is removed due to another plane..
// Sys_Printf("(%f, %f, %f)\n", pnt[0], pnt[1], pnt[2]);
}
}
}
}
#ifdef _DEBUG
// Sys_Printf("%i points on brush\n", pointList.size());
#endif
return pointList.size();
}
void DBrush_addFace(DBrush& brush, const _QERFaceData& faceData)
{
brush.AddFace(vector3_to_array(faceData.m_p0), vector3_to_array(faceData.m_p1), vector3_to_array(faceData.m_p2), 0);
}
typedef ReferenceCaller1<DBrush, const _QERFaceData&, DBrush_addFace> DBrushAddFaceCaller;
void DBrush_addFaceTextured(DBrush& brush, const _QERFaceData& faceData)
{
brush.AddFace(vector3_to_array(faceData.m_p0), vector3_to_array(faceData.m_p1), vector3_to_array(faceData.m_p2), &faceData);
}
typedef ReferenceCaller1<DBrush, const _QERFaceData&, DBrush_addFaceTextured> DBrushAddFaceTexturedCaller;
void DBrush::LoadFromBrush(scene::Instance& brush, bool textured)
{
ClearFaces();
ClearPoints();
GlobalBrushCreator().Brush_forEachFace(brush.path().top(), textured ? BrushFaceDataCallback(DBrushAddFaceTexturedCaller(*this)) : BrushFaceDataCallback(DBrushAddFaceCaller(*this)));
QER_entity = brush.path().parent().get_pointer();
QER_brush = brush.path().top().get_pointer();
}
int DBrush::PointPosition(vec3_t pnt)
{
int state = POINT_IN_BRUSH; // if nothing happens point is inside brush
for(std::list<DPlane *>::const_iterator chkPlane=faceList.begin(); chkPlane!=faceList.end(); chkPlane++)
{
float dist = (*chkPlane)->DistanceToPoint(pnt);
if(dist > MAX_ROUND_ERROR)
return POINT_OUT_BRUSH; // if point is in front of plane, it CANT be in the brush
else if(fabs(dist) < MAX_ROUND_ERROR)
state = POINT_ON_BRUSH; // if point is ON plane point is either ON the brush
// or outside it, it can no longer be in it
}
return state;
}
void DBrush::ClearPoints()
{
for(std::list<DPoint *>::const_iterator deadPoint=pointList.begin(); deadPoint!=pointList.end(); deadPoint++) {
delete *deadPoint;
}
pointList.clear();
}
void DBrush::ClearFaces()
{
bBoundsBuilt = false;
for(std::list<DPlane *>::const_iterator deadPlane=faceList.begin(); deadPlane!=faceList.end(); deadPlane++)
{
delete *deadPlane;
}
faceList.clear();
}
void DBrush::AddPoint(vec3_t pnt)
{
DPoint* newPoint = new DPoint;
VectorCopy(pnt, newPoint->_pnt);
pointList.push_back(newPoint);
}
bool DBrush::HasPoint(vec3_t pnt)
{
for(std::list<DPoint *>::const_iterator chkPoint=pointList.begin(); chkPoint!=pointList.end(); chkPoint++)
{
if(**chkPoint == pnt)
return true;
}
return false;
}
int DBrush::RemoveRedundantPlanes()
{
int cnt = 0;
std::list<DPlane *>::iterator chkPlane;
// find duplicate planes
std::list<DPlane *>::iterator p1=faceList.begin();
while( p1!=faceList.end() )
{
std::list<DPlane *>::iterator p2 = p1;
for(p2++; p2!=faceList.end(); p2++)
{
if(**p1 == **p2)
{
if(!strcmp((*p1)->m_shader.c_str(), "textures/common/caulk"))
{
delete *p1;
p1 = faceList.erase(p1); // duplicate plane
}
else
{
delete *p2;
p2 = faceList.erase(p2); // duplicate plane
}
cnt++;
break;
}
}
if( p2 == faceList.end() )
p1++;
}
//+djbob kill planes with bad normal, they are more of a nuisance than losing a brush
chkPlane=faceList.begin();
while( chkPlane!=faceList.end() )
{
if(VectorLength((*chkPlane)->normal) == 0) // plane has bad normal
{
delete *chkPlane;
chkPlane = faceList.erase(chkPlane);
cnt++;
} else {
chkPlane++;
}
}
//-djbob
if(pointList.size() == 0) // if points may not have been built, build them
/* if(BuildPoints() == 0) // just let the planes die if they are all bad
return cnt;*/
BuildPoints();
chkPlane=faceList.begin();
while(chkPlane != faceList.end())
{
if((*chkPlane)->IsRedundant(pointList)) // checks that plane "0wnz" :), 3 or more points
{
delete *chkPlane;
chkPlane = faceList.erase(chkPlane);
cnt++;
}
else
chkPlane++;
}
return cnt;
}
bool DBrush::GetBounds(vec3_t min, vec3_t max)
{
BuildBounds();
if(!bBoundsBuilt)
return false;
VectorCopy(bbox_min, min);
VectorCopy(bbox_max, max);
return true;
}
bool DBrush::BBoxCollision(DBrush* chkBrush)
{
vec3_t min1, min2;
vec3_t max1, max2;
GetBounds(min1, max1);
chkBrush->GetBounds(min2, max2);
if(min1[0] >= max2[0])
return false;
if(min1[1] >= max2[1])
return false;
if(min1[2] >= max2[2])
return false;
if(max1[0] <= min2[0])
return false;
if(max1[1] <= min2[1])
return false;
if(max1[2] <= min2[2])
return false;
return true;
}
DPlane* DBrush::HasPlane(DPlane* chkPlane)
{
for(std::list<DPlane *>::const_iterator brushPlane=faceList.begin(); brushPlane!=faceList.end(); brushPlane++)
{
if(**brushPlane == *chkPlane)
return *brushPlane;
}
return NULL;
}
bool DBrush::IsCutByPlane(DPlane *cuttingPlane)
{
bool isInFront;
if(pointList.size() == 0)
if(BuildPoints() == 0)
return false;
std::list<DPoint *>::const_iterator chkPnt = pointList.begin();
if(chkPnt == pointList.end())
return false;
float dist = cuttingPlane->DistanceToPoint((*chkPnt)->_pnt);
if(dist > MAX_ROUND_ERROR)
isInFront = false;
else if(dist < MAX_ROUND_ERROR)
isInFront = true;
else
return true;
for(chkPnt++=pointList.begin(); chkPnt!=pointList.end(); chkPnt++)
{
dist = cuttingPlane->DistanceToPoint((*chkPnt)->_pnt);
if(dist > MAX_ROUND_ERROR)
{
if(isInFront)
return true;
}
else if(dist < MAX_ROUND_ERROR)
{
if(!isInFront)
return true;
}
else
return true;
}
return false;
}
scene::Node* DBrush::BuildInRadiant(bool allowDestruction, int* changeCnt, scene::Node* entity)
{
if(allowDestruction)
{
bool kill = true;
for(std::list<DPlane *>::const_iterator chkPlane=faceList.begin(); chkPlane!=faceList.end(); chkPlane++)
{
if((*chkPlane)->m_bChkOk)
{
kill = false;
break;
}
}
if(kill)
return NULL;
}
//+djbob: fixed bug when brush had no faces "phantom brush" in radiant.
if(faceList.size() < 4)
{
globalErrorStream() << "Possible Phantom Brush Found, will not rebuild\n";
return NULL;
}
//-djbob
NodeSmartReference node(GlobalBrushCreator().createBrush());
for(std::list<DPlane *>::const_iterator buildPlane=faceList.begin(); buildPlane!=faceList.end(); buildPlane++) {
if((*buildPlane)->AddToBrush(node) && changeCnt) {
(*changeCnt)++;
}
}
if(entity) {
Node_getTraversable(*entity)->insert(node);
} else {
Node_getTraversable(GlobalRadiant().getMapWorldEntity())->insert(node);
}
QER_entity = entity;
QER_brush = node.get_pointer();
return node.get_pointer();
}
void DBrush::CutByPlane(DPlane *cutPlane, DBrush **newBrush1, DBrush **newBrush2)
{
if(!IsCutByPlane(cutPlane))
{
*newBrush1 = NULL;
*newBrush2 = NULL;
return;
}
DBrush* b1 = new DBrush;
DBrush* b2 = new DBrush;
for(std::list<DPlane *>::const_iterator parsePlane=faceList.begin(); parsePlane!=faceList.end(); parsePlane++)
{
b1->AddFace((*parsePlane)->points[0], (*parsePlane)->points[1], (*parsePlane)->points[2], NULL);
b2->AddFace((*parsePlane)->points[0], (*parsePlane)->points[1], (*parsePlane)->points[2], NULL);
}
b1->AddFace(cutPlane->points[0], cutPlane->points[1], cutPlane->points[2], NULL);
b2->AddFace(cutPlane->points[2], cutPlane->points[1], cutPlane->points[0], NULL);
b1->RemoveRedundantPlanes();
b2->RemoveRedundantPlanes();
*newBrush1 = b1;
*newBrush2 = b2;
}
bool DBrush::IntersectsWith(DBrush *chkBrush)
{
if(pointList.size() == 0)
if(BuildPoints() == 0)
return false; // invalid brush!!!!
if(chkBrush->pointList.size() == 0)
if(chkBrush->BuildPoints() == 0)
return false; // invalid brush!!!!
if(!BBoxCollision(chkBrush))
return false;
std::list<DPlane *>::const_iterator iplPlane;
for( iplPlane=faceList.begin(); iplPlane!=faceList.end(); iplPlane++)
{
bool allInFront = true;
for(std::list<DPoint *>::const_iterator iPoint=chkBrush->pointList.begin(); iPoint!=chkBrush->pointList.end(); iPoint++)
{
if((*iplPlane)->DistanceToPoint((*iPoint)->_pnt) < -MAX_ROUND_ERROR)
{
allInFront = false;
break;
}
}
if(allInFront)
return false;
}
for( iplPlane=chkBrush->faceList.begin(); iplPlane!=chkBrush->faceList.end(); iplPlane++)
{
bool allInFront = true;
for(std::list<DPoint *>::const_iterator iPoint=pointList.begin(); iPoint!=pointList.end(); iPoint++)
{
if((*iplPlane)->DistanceToPoint((*iPoint)->_pnt) < -MAX_ROUND_ERROR)
{
allInFront = false;
break;
}
}
if(allInFront)
return false;
}
return true;
}
bool DBrush::IntersectsWith(DPlane* p1, DPlane* p2, vec3_t v) {
vec3_t vDown = { 0, 0, -1 };
std::list<DPlane *>::const_iterator iplPlane;
for( iplPlane = faceList.begin(); iplPlane != faceList.end(); iplPlane++) {
DPlane* p = (*iplPlane);
vec_t d = DotProduct( p->normal, vDown );
if( d >= 0 ) {
continue;
}
if(p->PlaneIntersection(p1, p2, v)) {
if(PointPosition( v ) != POINT_OUT_BRUSH) {
return true;
}
}
}
return false;
}
void DBrush::BuildBounds()
{
if(!bBoundsBuilt)
{
if(pointList.size() == 0) // if points may not have been built, build them
if(BuildPoints() == 0)
return;
std::list<DPoint *>::const_iterator first = pointList.begin();
VectorCopy((*first)->_pnt, bbox_min);
VectorCopy((*first)->_pnt, bbox_max);
std::list<DPoint *>::const_iterator point=pointList.begin();
for( point++; point!=pointList.end(); point++)
{
if((*point)->_pnt[0] > bbox_max[0])
bbox_max[0] = (*point)->_pnt[0];
if((*point)->_pnt[1] > bbox_max[1])
bbox_max[1] = (*point)->_pnt[1];
if((*point)->_pnt[2] > bbox_max[2])
bbox_max[2] = (*point)->_pnt[2];
if((*point)->_pnt[0] < bbox_min[0])
bbox_min[0] = (*point)->_pnt[0];
if((*point)->_pnt[1] < bbox_min[1])
bbox_min[1] = (*point)->_pnt[1];
if((*point)->_pnt[2] < bbox_min[2])
bbox_min[2] = (*point)->_pnt[2];
}
bBoundsBuilt = true;
}
}
bool DBrush::BBoxTouch(DBrush *chkBrush)
{
vec3_t min1, min2;
vec3_t max1, max2;
GetBounds(min1, max1);
chkBrush->GetBounds(min2, max2);
if((min1[0] - max2[0]) > MAX_ROUND_ERROR)
return false;
if((min1[1] - max2[1]) > MAX_ROUND_ERROR)
return false;
if((min1[2] - max2[2]) > MAX_ROUND_ERROR)
return false;
if((min2[0] - max1[0]) > MAX_ROUND_ERROR)
return false;
if((min2[1] - max1[1]) > MAX_ROUND_ERROR)
return false;
if((min2[2] - max1[2]) > MAX_ROUND_ERROR)
return false;
int cnt = 0;
if((min2[0] - max1[0]) == 0)
cnt++;
if((min2[1] - max1[1]) == 0)
cnt++;
if((min2[2] - max1[2]) == 0)
cnt++;
if((min1[0] - max2[0]) == 0)
cnt++;
if((min1[1] - max2[1]) == 0)
cnt++;
if((min1[2] - max2[2]) == 0)
cnt++;
if(cnt > 1)
return false;
return true;
}
void DBrush::ResetChecks(std::list<Str>* exclusionList)
{
for(std::list<DPlane *>::const_iterator resetPlane=faceList.begin(); resetPlane!=faceList.end(); resetPlane++)
{
bool set = false;
if(exclusionList)
{
for(std::list<Str>::iterator eTexture = exclusionList->begin(); eTexture != exclusionList->end(); eTexture++)
{
if(strstr((*resetPlane)->m_shader.c_str(), eTexture->GetBuffer()))
{
set = true;
break;
}
}
}
(*resetPlane)->m_bChkOk = set;
}
}
DPlane* DBrush::HasPlaneInverted(DPlane *chkPlane)
{
for(std::list<DPlane *>::const_iterator brushPlane=faceList.begin(); brushPlane!=faceList.end(); brushPlane++)
{
if(**brushPlane != *chkPlane)
{
if(fabs((*brushPlane)->_d + chkPlane->_d) < 0.1)
return (*brushPlane);
}
}
return NULL;
}
bool DBrush::HasTexture(const char *textureName)
{
for(std::list<DPlane *>::const_iterator chkPlane=faceList.begin(); chkPlane!=faceList.end(); chkPlane++)
{
if(strstr((*chkPlane)->m_shader.c_str(), textureName))
return true;
}
return false;
}
bool DBrush::IsDetail()
{
for(std::list<DPlane *>::const_iterator chkPlane=faceList.begin(); chkPlane!=faceList.end(); chkPlane++)
{
if((*chkPlane)->texInfo.contents & FACE_DETAIL)
return true;
}
return false;
}
void DBrush::BuildFromWinding(DWinding *w)
{
if(w->numpoints < 3)
{
globalErrorStream() << "Winding has invalid number of points";
return;
}
DPlane* wPlane = w->WindingPlane();
DWinding* w2;
w2 = w->CopyWinding();
int i;
for(i = 0; i < w2->numpoints; i++)
VectorAdd(w2->p[i], wPlane->normal, w2->p[i]);
AddFace(w2->p[0], w2->p[1], w2->p[2], NULL);
AddFace(w->p[2], w->p[1], w->p[0], NULL);
for(i = 0; i < w->numpoints-1; i++)
AddFace(w2->p[i], w->p[i], w->p[i+1], NULL);
AddFace(w2->p[w->numpoints-1], w->p[w->numpoints-1], w->p[0], NULL);
delete wPlane;
delete w2;
}
void DBrush::SaveToFile(FILE *pFile)
{
fprintf(pFile, "{\n");
for(std::list<DPlane *>::const_iterator pp=faceList.begin(); pp!=faceList.end(); pp++)
{
char buffer[512];
sprintf(buffer, "( %.0f %.0f %.0f ) ( %.0f %.0f %.0f ) ( %.0f %.0f %.0f ) %s %.0f %.0f %f %f %.0f 0 0 0\n",
(*pp)->points[0][0], (*pp)->points[0][1], (*pp)->points[0][2],
(*pp)->points[1][0], (*pp)->points[1][1], (*pp)->points[1][2],
(*pp)->points[2][0], (*pp)->points[2][1], (*pp)->points[2][2],
(*pp)->m_shader.c_str(),
(*pp)->texInfo.m_texdef.shift[0], (*pp)->texInfo.m_texdef.shift[1],
(*pp)->texInfo.m_texdef.scale[0], (*pp)->texInfo.m_texdef.scale[0],
(*pp)->texInfo.m_texdef.rotate);
fprintf(pFile, buffer);
}
fprintf(pFile, "}\n");
}
void DBrush::Rotate(vec3_t vOrigin, vec3_t vRotation)
{
for(std::list<DPlane *>::const_iterator rotPlane=faceList.begin(); rotPlane!=faceList.end(); rotPlane++)
{
for(int i = 0; i < 3; i++)
VectorRotate((*rotPlane)->points[i], vRotation, vOrigin);
(*rotPlane)->Rebuild();
}
}
void DBrush::RotateAboutCentre(vec3_t vRotation)
{
vec3_t min, max, centre;
GetBounds(min, max);
VectorAdd(min, max, centre);
VectorScale(centre, 0.5f, centre);
Rotate(centre, vRotation);
}
bool DBrush::ResetTextures(const char* textureName, float fScale[2], float fShift[2], int rotation, const char* newTextureName,
int bResetTextureName, int bResetScale[2], int bResetShift[2], int bResetRotation)
{
if(textureName)
{
bool changed = false;
for(std::list<DPlane *>::const_iterator resetPlane=faceList.begin(); resetPlane!=faceList.end(); resetPlane++)
{
if(!strcmp((*resetPlane)->m_shader.c_str(), textureName))
{
if(bResetTextureName)
(*resetPlane)->m_shader = newTextureName;
if(bResetScale[0])
(*resetPlane)->texInfo.m_texdef.scale[0] = fScale[0];
if(bResetScale[1])
(*resetPlane)->texInfo.m_texdef.scale[1] = fScale[1];
if(bResetShift[0])
(*resetPlane)->texInfo.m_texdef.shift[0] = fShift[0];
if(bResetShift[1])
(*resetPlane)->texInfo.m_texdef.shift[1] = fShift[1];
if(bResetRotation)
(*resetPlane)->texInfo.m_texdef.rotate = (float)rotation;
changed = true;
}
}
return changed; // no point rebuilding unless we need to, only slows things down
}
else
{
for(std::list<DPlane *>::const_iterator resetPlane=faceList.begin(); resetPlane!=faceList.end(); resetPlane++)
{
if(bResetTextureName)
(*resetPlane)->m_shader = newTextureName;
if(bResetScale[0])
(*resetPlane)->texInfo.m_texdef.scale[0] = fScale[0];
if(bResetScale[1])
(*resetPlane)->texInfo.m_texdef.scale[1] = fScale[1];
if(bResetShift[0])
(*resetPlane)->texInfo.m_texdef.shift[0] = fShift[0];
if(bResetShift[1])
(*resetPlane)->texInfo.m_texdef.shift[1] = fShift[1];
if(bResetRotation)
(*resetPlane)->texInfo.m_texdef.rotate = (float)rotation;
}
return true;
}
}
bool DBrush::operator ==(DBrush* other)
{
std::list<DPlane *>::const_iterator chkPlane;
for(chkPlane=faceList.begin(); chkPlane!=faceList.end(); chkPlane++)
{
if(!other->HasPlane((*chkPlane)))
return false;
}
for(chkPlane=faceList.begin(); chkPlane!=faceList.end(); chkPlane++)
{
if(!HasPlane((*chkPlane)))
return false;
}
return true;
}
DPlane* DBrush::AddFace(const vec3_t va, const vec3_t vb, const vec3_t vc, const char *textureName, bool bDetail)
{
bBoundsBuilt = false;
DPlane* newFace = new DPlane(va, vb, vc, textureName, bDetail);
faceList.push_back(newFace);
return newFace;
}
DPlane* DBrush::FindPlaneWithClosestNormal( vec_t* normal ) {
vec_t bestDot = -2;
DPlane* bestDotPlane = NULL;
std::list<DPlane *>::const_iterator chkPlane;
for( chkPlane = faceList.begin(); chkPlane != faceList.end(); chkPlane++ ) {
DPlane* pPlane = (*chkPlane);
vec_t dot = DotProduct( pPlane->normal, normal );
if( dot > bestDot ) {
bestDot = dot;
bestDotPlane = pPlane;
}
}
return bestDotPlane;
}
int DBrush::FindPointsForPlane( DPlane* plane, DPoint** pnts, int maxpnts ) {
int numpnts = 0;
if(!maxpnts) {
return 0;
}
BuildPoints();
for( std::list<DPoint *>::const_iterator points = pointList.begin(); points != pointList.end(); points++ ) {
DPoint* point = (*points);
if( fabs(plane->DistanceToPoint( point->_pnt )) < MAX_ROUND_ERROR ) {
pnts[numpnts] = point;
numpnts++;
if(numpnts >= maxpnts) {
return numpnts;
}
}
}
return numpnts;
}
void DBrush::RemovePlane( DPlane* plane ) {
bBoundsBuilt = false;
for( std::list<DPlane *>::const_iterator deadPlane = faceList.begin(); deadPlane != faceList.end(); deadPlane++ ) {
if(*deadPlane == plane) {
delete *deadPlane;
faceList.remove( plane );
}
}
}