mirror of
https://github.com/overte-org/overte.git
synced 2025-08-08 12:37:51 +02:00
Merge pull request #8192 from AndrewMeadows/simple-hull-shapes
experimental ModelEntityItem collision shape options (no visual debugging)
This commit is contained in:
commit
5f71f77445
8 changed files with 113 additions and 74 deletions
|
@ -430,7 +430,8 @@ void RenderableModelEntityItem::render(RenderArgs* args) {
|
||||||
|
|
||||||
// check to see if when we added our models to the scene they were ready, if they were not ready, then
|
// check to see if when we added our models to the scene they were ready, if they were not ready, then
|
||||||
// fix them up in the scene
|
// fix them up in the scene
|
||||||
bool shouldShowCollisionHull = (args->_debugFlags & (int)RenderArgs::RENDER_DEBUG_HULLS) > 0;
|
bool shouldShowCollisionHull = (args->_debugFlags & (int)RenderArgs::RENDER_DEBUG_HULLS) > 0
|
||||||
|
&& getShapeType() == SHAPE_TYPE_COMPOUND;
|
||||||
if (_model->needsFixupInScene() || _showCollisionHull != shouldShowCollisionHull) {
|
if (_model->needsFixupInScene() || _showCollisionHull != shouldShowCollisionHull) {
|
||||||
_showCollisionHull = shouldShowCollisionHull;
|
_showCollisionHull = shouldShowCollisionHull;
|
||||||
render::PendingChanges pendingChanges;
|
render::PendingChanges pendingChanges;
|
||||||
|
@ -600,7 +601,7 @@ bool RenderableModelEntityItem::isReadyToComputeShape() {
|
||||||
|
|
||||||
// the model is still being downloaded.
|
// the model is still being downloaded.
|
||||||
return false;
|
return false;
|
||||||
} else if (type == SHAPE_TYPE_STATIC_MESH) {
|
} else if (type >= SHAPE_TYPE_SIMPLE_HULL && type <= SHAPE_TYPE_STATIC_MESH) {
|
||||||
return (_model && _model->isLoaded());
|
return (_model && _model->isLoaded());
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -614,7 +615,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) {
|
||||||
|
|
||||||
// should never fall in here when collision model not fully loaded
|
// should never fall in here when collision model not fully loaded
|
||||||
// hence we assert that all geometries exist and are loaded
|
// hence we assert that all geometries exist and are loaded
|
||||||
assert(_model->isLoaded() && _model->isCollisionLoaded());
|
assert(_model && _model->isLoaded() && _model->isCollisionLoaded());
|
||||||
const FBXGeometry& collisionGeometry = _model->getCollisionFBXGeometry();
|
const FBXGeometry& collisionGeometry = _model->getCollisionFBXGeometry();
|
||||||
|
|
||||||
ShapeInfo::PointCollection& pointCollection = info.getPointCollection();
|
ShapeInfo::PointCollection& pointCollection = info.getPointCollection();
|
||||||
|
@ -698,14 +699,19 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
info.setParams(type, dimensions, _compoundShapeURL);
|
info.setParams(type, dimensions, _compoundShapeURL);
|
||||||
} else if (type == SHAPE_TYPE_STATIC_MESH) {
|
} else if (type >= SHAPE_TYPE_SIMPLE_HULL && type <= SHAPE_TYPE_STATIC_MESH) {
|
||||||
|
updateModelBounds();
|
||||||
|
|
||||||
|
// should never fall in here when model not fully loaded
|
||||||
|
assert(_model && _model->isLoaded());
|
||||||
|
|
||||||
// compute meshPart local transforms
|
// compute meshPart local transforms
|
||||||
QVector<glm::mat4> localTransforms;
|
QVector<glm::mat4> localTransforms;
|
||||||
const FBXGeometry& geometry = _model->getFBXGeometry();
|
const FBXGeometry& fbxGeometry = _model->getFBXGeometry();
|
||||||
int numberOfMeshes = geometry.meshes.size();
|
int numberOfMeshes = fbxGeometry.meshes.size();
|
||||||
int totalNumVertices = 0;
|
int totalNumVertices = 0;
|
||||||
for (int i = 0; i < numberOfMeshes; i++) {
|
for (int i = 0; i < numberOfMeshes; i++) {
|
||||||
const FBXMesh& mesh = geometry.meshes.at(i);
|
const FBXMesh& mesh = fbxGeometry.meshes.at(i);
|
||||||
if (mesh.clusters.size() > 0) {
|
if (mesh.clusters.size() > 0) {
|
||||||
const FBXCluster& cluster = mesh.clusters.at(0);
|
const FBXCluster& cluster = mesh.clusters.at(0);
|
||||||
auto jointMatrix = _model->getRig()->getJointTransform(cluster.jointIndex);
|
auto jointMatrix = _model->getRig()->getJointTransform(cluster.jointIndex);
|
||||||
|
@ -723,30 +729,42 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateModelBounds();
|
auto& meshes = _model->getGeometry()->getGeometry()->getMeshes();
|
||||||
|
int32_t numMeshes = (int32_t)(meshes.size());
|
||||||
// should never fall in here when collision model not fully loaded
|
|
||||||
assert(_model->isLoaded());
|
|
||||||
|
|
||||||
ShapeInfo::PointCollection& pointCollection = info.getPointCollection();
|
ShapeInfo::PointCollection& pointCollection = info.getPointCollection();
|
||||||
pointCollection.clear();
|
pointCollection.clear();
|
||||||
|
if (type == SHAPE_TYPE_SIMPLE_COMPOUND) {
|
||||||
ShapeInfo::PointList points;
|
pointCollection.resize(numMeshes);
|
||||||
ShapeInfo::TriangleIndices& triangleIndices = info.getTriangleIndices();
|
} else {
|
||||||
auto& meshes = _model->getGeometry()->getGeometry()->getMeshes();
|
pointCollection.resize(1);
|
||||||
|
}
|
||||||
|
|
||||||
Extents extents;
|
Extents extents;
|
||||||
int meshCount = 0;
|
int meshCount = 0;
|
||||||
|
int pointListIndex = 0;
|
||||||
for (auto& mesh : meshes) {
|
for (auto& mesh : meshes) {
|
||||||
const gpu::BufferView& vertices = mesh->getVertexBuffer();
|
const gpu::BufferView& vertices = mesh->getVertexBuffer();
|
||||||
const gpu::BufferView& indices = mesh->getIndexBuffer();
|
const gpu::BufferView& indices = mesh->getIndexBuffer();
|
||||||
const gpu::BufferView& parts = mesh->getPartBuffer();
|
const gpu::BufferView& parts = mesh->getPartBuffer();
|
||||||
|
|
||||||
|
ShapeInfo::PointList& points = pointCollection[pointListIndex];
|
||||||
|
|
||||||
|
// reserve room
|
||||||
|
int32_t sizeToReserve = (int32_t)(vertices.getNumElements());
|
||||||
|
if (type == SHAPE_TYPE_SIMPLE_COMPOUND) {
|
||||||
|
// a list of points for each mesh
|
||||||
|
pointListIndex++;
|
||||||
|
} else {
|
||||||
|
// only one list of points
|
||||||
|
sizeToReserve += (int32_t)((gpu::Size)points.size());
|
||||||
|
}
|
||||||
|
points.reserve(sizeToReserve);
|
||||||
|
|
||||||
// copy points
|
// copy points
|
||||||
const glm::mat4& localTransform = localTransforms[meshCount];
|
|
||||||
uint32_t meshIndexOffset = (uint32_t)points.size();
|
uint32_t meshIndexOffset = (uint32_t)points.size();
|
||||||
|
const glm::mat4& localTransform = localTransforms[meshCount];
|
||||||
gpu::BufferView::Iterator<const glm::vec3> vertexItr = vertices.cbegin<const glm::vec3>();
|
gpu::BufferView::Iterator<const glm::vec3> vertexItr = vertices.cbegin<const glm::vec3>();
|
||||||
points.reserve((int32_t)((gpu::Size)points.size() + vertices.getNumElements()));
|
|
||||||
while (vertexItr != vertices.cend<const glm::vec3>()) {
|
while (vertexItr != vertices.cend<const glm::vec3>()) {
|
||||||
glm::vec3 point = extractTranslation(localTransform * glm::translate(*vertexItr));
|
glm::vec3 point = extractTranslation(localTransform * glm::translate(*vertexItr));
|
||||||
points.push_back(point);
|
points.push_back(point);
|
||||||
|
@ -754,55 +772,57 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) {
|
||||||
++vertexItr;
|
++vertexItr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy triangleIndices
|
if (type == SHAPE_TYPE_STATIC_MESH) {
|
||||||
triangleIndices.reserve((int32_t)((gpu::Size)(triangleIndices.size()) + indices.getNumElements()));
|
// copy into triangleIndices
|
||||||
gpu::BufferView::Iterator<const model::Mesh::Part> partItr = parts.cbegin<const model::Mesh::Part>();
|
ShapeInfo::TriangleIndices& triangleIndices = info.getTriangleIndices();
|
||||||
while (partItr != parts.cend<const model::Mesh::Part>()) {
|
triangleIndices.reserve((int32_t)((gpu::Size)(triangleIndices.size()) + indices.getNumElements()));
|
||||||
|
gpu::BufferView::Iterator<const model::Mesh::Part> partItr = parts.cbegin<const model::Mesh::Part>();
|
||||||
if (partItr->_topology == model::Mesh::TRIANGLES) {
|
while (partItr != parts.cend<const model::Mesh::Part>()) {
|
||||||
assert(partItr->_numIndices % 3 == 0);
|
if (partItr->_topology == model::Mesh::TRIANGLES) {
|
||||||
auto indexItr = indices.cbegin<const gpu::BufferView::Index>() + partItr->_startIndex;
|
assert(partItr->_numIndices % 3 == 0);
|
||||||
auto indexEnd = indexItr + partItr->_numIndices;
|
auto indexItr = indices.cbegin<const gpu::BufferView::Index>() + partItr->_startIndex;
|
||||||
while (indexItr != indexEnd) {
|
auto indexEnd = indexItr + partItr->_numIndices;
|
||||||
triangleIndices.push_back(*indexItr + meshIndexOffset);
|
while (indexItr != indexEnd) {
|
||||||
++indexItr;
|
|
||||||
}
|
|
||||||
} else if (partItr->_topology == model::Mesh::TRIANGLE_STRIP) {
|
|
||||||
assert(partItr->_numIndices > 2);
|
|
||||||
uint32_t approxNumIndices = 3 * partItr->_numIndices;
|
|
||||||
if (approxNumIndices > (uint32_t)(triangleIndices.capacity() - triangleIndices.size())) {
|
|
||||||
// we underestimated the final size of triangleIndices so we pre-emptively expand it
|
|
||||||
triangleIndices.reserve(triangleIndices.size() + approxNumIndices);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto indexItr = indices.cbegin<const gpu::BufferView::Index>() + partItr->_startIndex;
|
|
||||||
auto indexEnd = indexItr + (partItr->_numIndices - 2);
|
|
||||||
|
|
||||||
// first triangle uses the first three indices
|
|
||||||
triangleIndices.push_back(*(indexItr++) + meshIndexOffset);
|
|
||||||
triangleIndices.push_back(*(indexItr++) + meshIndexOffset);
|
|
||||||
triangleIndices.push_back(*(indexItr++) + meshIndexOffset);
|
|
||||||
|
|
||||||
// the rest use previous and next index
|
|
||||||
uint32_t triangleCount = 1;
|
|
||||||
while (indexItr != indexEnd) {
|
|
||||||
if ((*indexItr) != model::Mesh::PRIMITIVE_RESTART_INDEX) {
|
|
||||||
if (triangleCount % 2 == 0) {
|
|
||||||
// even triangles use first two indices in order
|
|
||||||
triangleIndices.push_back(*(indexItr - 2) + meshIndexOffset);
|
|
||||||
triangleIndices.push_back(*(indexItr - 1) + meshIndexOffset);
|
|
||||||
} else {
|
|
||||||
// odd triangles swap order of first two indices
|
|
||||||
triangleIndices.push_back(*(indexItr - 1) + meshIndexOffset);
|
|
||||||
triangleIndices.push_back(*(indexItr - 2) + meshIndexOffset);
|
|
||||||
}
|
|
||||||
triangleIndices.push_back(*indexItr + meshIndexOffset);
|
triangleIndices.push_back(*indexItr + meshIndexOffset);
|
||||||
++triangleCount;
|
++indexItr;
|
||||||
|
}
|
||||||
|
} else if (partItr->_topology == model::Mesh::TRIANGLE_STRIP) {
|
||||||
|
assert(partItr->_numIndices > 2);
|
||||||
|
uint32_t approxNumIndices = 3 * partItr->_numIndices;
|
||||||
|
if (approxNumIndices > (uint32_t)(triangleIndices.capacity() - triangleIndices.size())) {
|
||||||
|
// we underestimated the final size of triangleIndices so we pre-emptively expand it
|
||||||
|
triangleIndices.reserve(triangleIndices.size() + approxNumIndices);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto indexItr = indices.cbegin<const gpu::BufferView::Index>() + partItr->_startIndex;
|
||||||
|
auto indexEnd = indexItr + (partItr->_numIndices - 2);
|
||||||
|
|
||||||
|
// first triangle uses the first three indices
|
||||||
|
triangleIndices.push_back(*(indexItr++) + meshIndexOffset);
|
||||||
|
triangleIndices.push_back(*(indexItr++) + meshIndexOffset);
|
||||||
|
triangleIndices.push_back(*(indexItr++) + meshIndexOffset);
|
||||||
|
|
||||||
|
// the rest use previous and next index
|
||||||
|
uint32_t triangleCount = 1;
|
||||||
|
while (indexItr != indexEnd) {
|
||||||
|
if ((*indexItr) != model::Mesh::PRIMITIVE_RESTART_INDEX) {
|
||||||
|
if (triangleCount % 2 == 0) {
|
||||||
|
// even triangles use first two indices in order
|
||||||
|
triangleIndices.push_back(*(indexItr - 2) + meshIndexOffset);
|
||||||
|
triangleIndices.push_back(*(indexItr - 1) + meshIndexOffset);
|
||||||
|
} else {
|
||||||
|
// odd triangles swap order of first two indices
|
||||||
|
triangleIndices.push_back(*(indexItr - 1) + meshIndexOffset);
|
||||||
|
triangleIndices.push_back(*(indexItr - 2) + meshIndexOffset);
|
||||||
|
}
|
||||||
|
triangleIndices.push_back(*indexItr + meshIndexOffset);
|
||||||
|
++triangleCount;
|
||||||
|
}
|
||||||
|
++indexItr;
|
||||||
}
|
}
|
||||||
++indexItr;
|
|
||||||
}
|
}
|
||||||
|
++partItr;
|
||||||
}
|
}
|
||||||
++partItr;
|
|
||||||
}
|
}
|
||||||
++meshCount;
|
++meshCount;
|
||||||
}
|
}
|
||||||
|
@ -815,12 +835,13 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) {
|
||||||
scaleToFit[i] = 1.0f;
|
scaleToFit[i] = 1.0f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (int i = 0; i < points.size(); ++i) {
|
for (auto points : pointCollection) {
|
||||||
points[i] = (points[i] * scaleToFit);
|
for (int i = 0; i < points.size(); ++i) {
|
||||||
|
points[i] = (points[i] * scaleToFit);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pointCollection.push_back(points);
|
info.setParams(type, 0.5f * dimensions, _modelURL);
|
||||||
info.setParams(SHAPE_TYPE_STATIC_MESH, 0.5f * dimensions, _modelURL);
|
|
||||||
} else {
|
} else {
|
||||||
ModelEntityItem::computeShapeInfo(info);
|
ModelEntityItem::computeShapeInfo(info);
|
||||||
info.setParams(type, 0.5f * dimensions);
|
info.setParams(type, 0.5f * dimensions);
|
||||||
|
|
|
@ -101,6 +101,8 @@ const char* shapeTypeNames[] = {
|
||||||
"hull",
|
"hull",
|
||||||
"plane",
|
"plane",
|
||||||
"compound",
|
"compound",
|
||||||
|
"simple-hull",
|
||||||
|
"simple-compound",
|
||||||
"static-mesh"
|
"static-mesh"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -123,6 +125,8 @@ void buildStringToShapeTypeLookup() {
|
||||||
addShapeType(SHAPE_TYPE_HULL);
|
addShapeType(SHAPE_TYPE_HULL);
|
||||||
addShapeType(SHAPE_TYPE_PLANE);
|
addShapeType(SHAPE_TYPE_PLANE);
|
||||||
addShapeType(SHAPE_TYPE_COMPOUND);
|
addShapeType(SHAPE_TYPE_COMPOUND);
|
||||||
|
addShapeType(SHAPE_TYPE_SIMPLE_HULL);
|
||||||
|
addShapeType(SHAPE_TYPE_SIMPLE_COMPOUND);
|
||||||
addShapeType(SHAPE_TYPE_STATIC_MESH);
|
addShapeType(SHAPE_TYPE_STATIC_MESH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@ PacketVersion versionForPacketType(PacketType packetType) {
|
||||||
case PacketType::EntityAdd:
|
case PacketType::EntityAdd:
|
||||||
case PacketType::EntityEdit:
|
case PacketType::EntityEdit:
|
||||||
case PacketType::EntityData:
|
case PacketType::EntityData:
|
||||||
return VERSION_MODEL_ENTITIES_SUPPORT_STATIC_MESH;
|
return VERSION_MODEL_ENTITIES_SUPPORT_SIMPLE_HULLS;
|
||||||
case PacketType::AvatarIdentity:
|
case PacketType::AvatarIdentity:
|
||||||
case PacketType::AvatarData:
|
case PacketType::AvatarData:
|
||||||
case PacketType::BulkAvatarData:
|
case PacketType::BulkAvatarData:
|
||||||
|
|
|
@ -181,6 +181,7 @@ const PacketVersion VERSION_ENTITIES_NO_FLY_ZONES = 58;
|
||||||
const PacketVersion VERSION_ENTITIES_MORE_SHAPES = 59;
|
const PacketVersion VERSION_ENTITIES_MORE_SHAPES = 59;
|
||||||
const PacketVersion VERSION_ENTITIES_PROPERLY_ENCODE_SHAPE_EDITS = 60;
|
const PacketVersion VERSION_ENTITIES_PROPERLY_ENCODE_SHAPE_EDITS = 60;
|
||||||
const PacketVersion VERSION_MODEL_ENTITIES_SUPPORT_STATIC_MESH = 61;
|
const PacketVersion VERSION_MODEL_ENTITIES_SUPPORT_STATIC_MESH = 61;
|
||||||
|
const PacketVersion VERSION_MODEL_ENTITIES_SUPPORT_SIMPLE_HULLS = 62;
|
||||||
|
|
||||||
enum class AvatarMixerPacketVersion : PacketVersion {
|
enum class AvatarMixerPacketVersion : PacketVersion {
|
||||||
TranslationSupport = 17,
|
TranslationSupport = 17,
|
||||||
|
|
|
@ -204,7 +204,7 @@ btTriangleIndexVertexArray* createStaticMeshArray(const ShapeInfo& info) {
|
||||||
if (numIndices < INT16_MAX) {
|
if (numIndices < INT16_MAX) {
|
||||||
int16_t* indices = static_cast<int16_t*>((void*)(mesh.m_triangleIndexBase));
|
int16_t* indices = static_cast<int16_t*>((void*)(mesh.m_triangleIndexBase));
|
||||||
for (int32_t i = 0; i < numIndices; ++i) {
|
for (int32_t i = 0; i < numIndices; ++i) {
|
||||||
indices[i] = triangleIndices[i];
|
indices[i] = (int16_t)triangleIndices[i];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
int32_t* indices = static_cast<int32_t*>((void*)(mesh.m_triangleIndexBase));
|
int32_t* indices = static_cast<int32_t*>((void*)(mesh.m_triangleIndexBase));
|
||||||
|
@ -257,7 +257,9 @@ btCollisionShape* ShapeFactory::createShapeFromInfo(const ShapeInfo& info) {
|
||||||
shape = new btCapsuleShape(radius, height);
|
shape = new btCapsuleShape(radius, height);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case SHAPE_TYPE_COMPOUND: {
|
case SHAPE_TYPE_COMPOUND:
|
||||||
|
case SHAPE_TYPE_SIMPLE_HULL:
|
||||||
|
case SHAPE_TYPE_SIMPLE_COMPOUND: {
|
||||||
const ShapeInfo::PointCollection& pointCollection = info.getPointCollection();
|
const ShapeInfo::PointCollection& pointCollection = info.getPointCollection();
|
||||||
uint32_t numSubShapes = info.getNumSubShapes();
|
uint32_t numSubShapes = info.getNumSubShapes();
|
||||||
if (numSubShapes == 1) {
|
if (numSubShapes == 1) {
|
||||||
|
|
|
@ -83,12 +83,19 @@ void ShapeInfo::setOffset(const glm::vec3& offset) {
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t ShapeInfo::getNumSubShapes() const {
|
uint32_t ShapeInfo::getNumSubShapes() const {
|
||||||
if (_type == SHAPE_TYPE_NONE) {
|
switch (_type) {
|
||||||
return 0;
|
case SHAPE_TYPE_NONE:
|
||||||
} else if (_type == SHAPE_TYPE_COMPOUND) {
|
return 0;
|
||||||
return _pointCollection.size();
|
case SHAPE_TYPE_COMPOUND:
|
||||||
|
case SHAPE_TYPE_SIMPLE_COMPOUND:
|
||||||
|
return _pointCollection.size();
|
||||||
|
case SHAPE_TYPE_SIMPLE_HULL:
|
||||||
|
case SHAPE_TYPE_STATIC_MESH:
|
||||||
|
assert(_pointCollection.size() == 1);
|
||||||
|
// yes fall through to default
|
||||||
|
default:
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int ShapeInfo::getLargestSubshapePointCount() const {
|
int ShapeInfo::getLargestSubshapePointCount() const {
|
||||||
|
|
|
@ -39,6 +39,8 @@ enum ShapeType {
|
||||||
SHAPE_TYPE_HULL,
|
SHAPE_TYPE_HULL,
|
||||||
SHAPE_TYPE_PLANE,
|
SHAPE_TYPE_PLANE,
|
||||||
SHAPE_TYPE_COMPOUND,
|
SHAPE_TYPE_COMPOUND,
|
||||||
|
SHAPE_TYPE_SIMPLE_HULL,
|
||||||
|
SHAPE_TYPE_SIMPLE_COMPOUND,
|
||||||
SHAPE_TYPE_STATIC_MESH
|
SHAPE_TYPE_STATIC_MESH
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1646,6 +1646,8 @@
|
||||||
<option value="box">Box</option>
|
<option value="box">Box</option>
|
||||||
<option value="sphere">Sphere</option>
|
<option value="sphere">Sphere</option>
|
||||||
<option value="compound">Compound</option>
|
<option value="compound">Compound</option>
|
||||||
|
<option value="simple-hull">One Hull</option>
|
||||||
|
<option value="simple-compound">Hull Per Submesh</option>
|
||||||
<option value="static-mesh">Static Mesh</option>
|
<option value="static-mesh">Static Mesh</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue