mirror of
https://github.com/overte-org/overte.git
synced 2025-08-09 09:48:44 +02:00
Fix for merging after streaming, added ability to "set" a spanner's attributes
in the actual metavoxel data.
This commit is contained in:
parent
0448596e58
commit
23556f0cf7
8 changed files with 218 additions and 16 deletions
|
@ -100,6 +100,7 @@ MetavoxelEditor::MetavoxelEditor() :
|
||||||
addTool(new InsertSpannerTool(this));
|
addTool(new InsertSpannerTool(this));
|
||||||
addTool(new RemoveSpannerTool(this));
|
addTool(new RemoveSpannerTool(this));
|
||||||
addTool(new ClearSpannersTool(this));
|
addTool(new ClearSpannersTool(this));
|
||||||
|
addTool(new SetSpannerTool(this));
|
||||||
|
|
||||||
updateAttributes();
|
updateAttributes();
|
||||||
|
|
||||||
|
@ -529,15 +530,15 @@ void GlobalSetTool::apply() {
|
||||||
Application::getInstance()->getMetavoxels()->applyEdit(message);
|
Application::getInstance()->getMetavoxels()->applyEdit(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
InsertSpannerTool::InsertSpannerTool(MetavoxelEditor* editor) :
|
PlaceSpannerTool::PlaceSpannerTool(MetavoxelEditor* editor, const QString& name, const QString& placeText) :
|
||||||
MetavoxelTool(editor, "Insert Spanner") {
|
MetavoxelTool(editor, name) {
|
||||||
|
|
||||||
QPushButton* button = new QPushButton("Insert");
|
QPushButton* button = new QPushButton(placeText);
|
||||||
layout()->addWidget(button);
|
layout()->addWidget(button);
|
||||||
connect(button, SIGNAL(clicked()), SLOT(insert()));
|
connect(button, SIGNAL(clicked()), SLOT(place()));
|
||||||
}
|
}
|
||||||
|
|
||||||
void InsertSpannerTool::simulate(float deltaTime) {
|
void PlaceSpannerTool::simulate(float deltaTime) {
|
||||||
if (Application::getInstance()->isMouseHidden()) {
|
if (Application::getInstance()->isMouseHidden()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -558,7 +559,7 @@ void InsertSpannerTool::simulate(float deltaTime) {
|
||||||
spanner->getRenderer()->simulate(deltaTime);
|
spanner->getRenderer()->simulate(deltaTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
void InsertSpannerTool::render() {
|
void PlaceSpannerTool::render() {
|
||||||
if (Application::getInstance()->isMouseHidden()) {
|
if (Application::getInstance()->isMouseHidden()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -567,28 +568,36 @@ void InsertSpannerTool::render() {
|
||||||
spanner->getRenderer()->render(SPANNER_ALPHA);
|
spanner->getRenderer()->render(SPANNER_ALPHA);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool InsertSpannerTool::appliesTo(const AttributePointer& attribute) const {
|
bool PlaceSpannerTool::appliesTo(const AttributePointer& attribute) const {
|
||||||
return attribute->inherits("SpannerSetAttribute");
|
return attribute->inherits("SpannerSetAttribute");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool InsertSpannerTool::eventFilter(QObject* watched, QEvent* event) {
|
bool PlaceSpannerTool::eventFilter(QObject* watched, QEvent* event) {
|
||||||
if (event->type() == QEvent::MouseButtonPress) {
|
if (event->type() == QEvent::MouseButtonPress) {
|
||||||
insert();
|
place();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void InsertSpannerTool::insert() {
|
void PlaceSpannerTool::place() {
|
||||||
AttributePointer attribute = AttributeRegistry::getInstance()->getAttribute(_editor->getSelectedAttribute());
|
AttributePointer attribute = AttributeRegistry::getInstance()->getAttribute(_editor->getSelectedAttribute());
|
||||||
if (!attribute) {
|
if (!attribute) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
SharedObjectPointer spanner = _editor->getValue().value<SharedObjectPointer>();
|
SharedObjectPointer spanner = _editor->getValue().value<SharedObjectPointer>();
|
||||||
MetavoxelEditMessage message = { QVariant::fromValue(InsertSpannerEdit(attribute, spanner)) };
|
MetavoxelEditMessage message = { createEdit(attribute, spanner) };
|
||||||
Application::getInstance()->getMetavoxels()->applyEdit(message);
|
Application::getInstance()->getMetavoxels()->applyEdit(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InsertSpannerTool::InsertSpannerTool(MetavoxelEditor* editor) :
|
||||||
|
PlaceSpannerTool(editor, "Insert Spanner", "Insert") {
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant InsertSpannerTool::createEdit(const AttributePointer& attribute, const SharedObjectPointer& spanner) {
|
||||||
|
return QVariant::fromValue(InsertSpannerEdit(attribute, spanner));
|
||||||
|
}
|
||||||
|
|
||||||
RemoveSpannerTool::RemoveSpannerTool(MetavoxelEditor* editor) :
|
RemoveSpannerTool::RemoveSpannerTool(MetavoxelEditor* editor) :
|
||||||
MetavoxelTool(editor, "Remove Spanner", false) {
|
MetavoxelTool(editor, "Remove Spanner", false) {
|
||||||
}
|
}
|
||||||
|
@ -625,3 +634,16 @@ void ClearSpannersTool::clear() {
|
||||||
MetavoxelEditMessage message = { QVariant::fromValue(ClearSpannersEdit(attribute)) };
|
MetavoxelEditMessage message = { QVariant::fromValue(ClearSpannersEdit(attribute)) };
|
||||||
Application::getInstance()->getMetavoxels()->applyEdit(message);
|
Application::getInstance()->getMetavoxels()->applyEdit(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SetSpannerTool::SetSpannerTool(MetavoxelEditor* editor) :
|
||||||
|
PlaceSpannerTool(editor, "Set Spanner", "Set") {
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SetSpannerTool::appliesTo(const AttributePointer& attribute) const {
|
||||||
|
return attribute == AttributeRegistry::getInstance()->getSpannersAttribute();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant SetSpannerTool::createEdit(const AttributePointer& attribute, const SharedObjectPointer& spanner) {
|
||||||
|
static_cast<Spanner*>(spanner.data())->setGranularity(_editor->getGridSpacing());
|
||||||
|
return QVariant::fromValue(SetSpannerEdit(spanner));
|
||||||
|
}
|
||||||
|
|
|
@ -139,13 +139,13 @@ private slots:
|
||||||
void apply();
|
void apply();
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Allows inserting a spanner into the scene.
|
/// Base class for insert/set spanner tools.
|
||||||
class InsertSpannerTool : public MetavoxelTool {
|
class PlaceSpannerTool : public MetavoxelTool {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
InsertSpannerTool(MetavoxelEditor* editor);
|
PlaceSpannerTool(MetavoxelEditor* editor, const QString& name, const QString& placeText);
|
||||||
|
|
||||||
virtual void simulate(float deltaTime);
|
virtual void simulate(float deltaTime);
|
||||||
|
|
||||||
|
@ -155,9 +155,26 @@ public:
|
||||||
|
|
||||||
virtual bool eventFilter(QObject* watched, QEvent* event);
|
virtual bool eventFilter(QObject* watched, QEvent* event);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
|
||||||
|
virtual QVariant createEdit(const AttributePointer& attribute, const SharedObjectPointer& spanner) = 0;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
|
|
||||||
void insert();
|
void place();
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Allows inserting a spanner into the scene.
|
||||||
|
class InsertSpannerTool : public PlaceSpannerTool {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
InsertSpannerTool(MetavoxelEditor* editor);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
|
||||||
|
virtual QVariant createEdit(const AttributePointer& attribute, const SharedObjectPointer& spanner);
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Allows removing a spanner from the scene.
|
/// Allows removing a spanner from the scene.
|
||||||
|
@ -188,4 +205,19 @@ private slots:
|
||||||
void clear();
|
void clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Allows setting the value by placing a spanner.
|
||||||
|
class SetSpannerTool : public PlaceSpannerTool {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
SetSpannerTool(MetavoxelEditor* editor);
|
||||||
|
|
||||||
|
virtual bool appliesTo(const AttributePointer& attribute) const;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
|
||||||
|
virtual QVariant createEdit(const AttributePointer& attribute, const SharedObjectPointer& spanner);
|
||||||
|
};
|
||||||
|
|
||||||
#endif /* defined(__interface__MetavoxelEditor__) */
|
#endif /* defined(__interface__MetavoxelEditor__) */
|
||||||
|
|
|
@ -493,6 +493,7 @@ void MetavoxelNode::read(MetavoxelStreamState& state) {
|
||||||
_children[i] = new MetavoxelNode(state.attribute);
|
_children[i] = new MetavoxelNode(state.attribute);
|
||||||
_children[i]->read(nextState);
|
_children[i]->read(nextState);
|
||||||
}
|
}
|
||||||
|
mergeChildren(state.attribute);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -549,7 +550,8 @@ void MetavoxelNode::readDelta(const MetavoxelNode& reference, MetavoxelStreamSta
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
mergeChildren(state.attribute);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1085,6 +1087,15 @@ void Spanner::setBounds(const Box& bounds) {
|
||||||
emit boundsChanged(_bounds = bounds);
|
emit boundsChanged(_bounds = bounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QVector<AttributePointer>& Spanner::getAttributes() const {
|
||||||
|
static QVector<AttributePointer> emptyVector;
|
||||||
|
return emptyVector;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Spanner::getAttributeValues(MetavoxelInfo& info) const {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool Spanner::testAndSetVisited() {
|
bool Spanner::testAndSetVisited() {
|
||||||
if (_lastVisit == _visit) {
|
if (_lastVisit == _visit) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -1164,6 +1175,43 @@ void Sphere::setColor(const QColor& color) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QVector<AttributePointer>& Sphere::getAttributes() const {
|
||||||
|
static QVector<AttributePointer> attributes = QVector<AttributePointer>() <<
|
||||||
|
AttributeRegistry::getInstance()->getColorAttribute() << AttributeRegistry::getInstance()->getNormalAttribute();
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Sphere::getAttributeValues(MetavoxelInfo& info) const {
|
||||||
|
// bounds check
|
||||||
|
Box bounds = info.getBounds();
|
||||||
|
if (!getBounds().intersects(bounds)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// count the points inside the sphere
|
||||||
|
int pointsWithin = 0;
|
||||||
|
for (int i = 0; i < Box::VERTEX_COUNT; i++) {
|
||||||
|
if (glm::distance(bounds.getVertex(i), getTranslation()) <= getScale()) {
|
||||||
|
pointsWithin++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pointsWithin == Box::VERTEX_COUNT) {
|
||||||
|
// entirely contained
|
||||||
|
info.outputValues[0] = AttributeValue(getAttributes().at(0), encodeInline<QRgb>(_color.rgba()));
|
||||||
|
getNormal(info);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (info.size <= getGranularity()) {
|
||||||
|
// best guess
|
||||||
|
if (pointsWithin > 0) {
|
||||||
|
info.outputValues[0] = AttributeValue(getAttributes().at(0), encodeInline<QRgb>(qRgba(
|
||||||
|
_color.red(), _color.green(), _color.blue(), _color.alpha() * pointsWithin / Box::VERTEX_COUNT)));
|
||||||
|
getNormal(info);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
QByteArray Sphere::getRendererClassName() const {
|
QByteArray Sphere::getRendererClassName() const {
|
||||||
return "SphereRenderer";
|
return "SphereRenderer";
|
||||||
}
|
}
|
||||||
|
@ -1173,6 +1221,24 @@ void Sphere::updateBounds() {
|
||||||
setBounds(Box(getTranslation() - extent, getTranslation() + extent));
|
setBounds(Box(getTranslation() - extent, getTranslation() + extent));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Sphere::getNormal(MetavoxelInfo& info) const {
|
||||||
|
glm::vec3 normal = info.getCenter() - getTranslation();
|
||||||
|
float length = glm::length(normal);
|
||||||
|
QRgb color;
|
||||||
|
if (length > EPSILON) {
|
||||||
|
const float NORMAL_SCALE = 127.0f;
|
||||||
|
float scale = NORMAL_SCALE / length;
|
||||||
|
const int BYTE_MASK = 0xFF;
|
||||||
|
color = qRgb((int)(normal.x * scale) & BYTE_MASK, (int)(normal.y * scale) & BYTE_MASK,
|
||||||
|
(int)(normal.z * scale) & BYTE_MASK);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const QRgb DEFAULT_NORMAL = 0x007F00;
|
||||||
|
color = DEFAULT_NORMAL;
|
||||||
|
}
|
||||||
|
info.outputValues[1] = AttributeValue(getAttributes().at(1), encodeInline<QRgb>(color));
|
||||||
|
}
|
||||||
|
|
||||||
StaticModel::StaticModel() {
|
StaticModel::StaticModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -188,6 +188,7 @@ public:
|
||||||
bool isLeaf;
|
bool isLeaf;
|
||||||
|
|
||||||
Box getBounds() const { return Box(minimum, minimum + glm::vec3(size, size, size)); }
|
Box getBounds() const { return Box(minimum, minimum + glm::vec3(size, size, size)); }
|
||||||
|
glm::vec3 getCenter() const { return minimum + glm::vec3(size, size, size) * 0.5f; }
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Interface for visitors to metavoxels.
|
/// Interface for visitors to metavoxels.
|
||||||
|
@ -374,6 +375,13 @@ public:
|
||||||
void setGranularity(float granularity) { _granularity = granularity; }
|
void setGranularity(float granularity) { _granularity = granularity; }
|
||||||
float getGranularity() const { return _granularity; }
|
float getGranularity() const { return _granularity; }
|
||||||
|
|
||||||
|
/// Returns a reference to the list of attributes associated with this spanner.
|
||||||
|
virtual const QVector<AttributePointer>& getAttributes() const;
|
||||||
|
|
||||||
|
/// Sets the attribute values associated with this spanner in the supplied info.
|
||||||
|
/// \return true to recurse, false to stop
|
||||||
|
virtual bool getAttributeValues(MetavoxelInfo& info) const;
|
||||||
|
|
||||||
/// Checks whether we've visited this object on the current traversal. If we have, returns false.
|
/// Checks whether we've visited this object on the current traversal. If we have, returns false.
|
||||||
/// If we haven't, sets the last visit identifier and returns true.
|
/// If we haven't, sets the last visit identifier and returns true.
|
||||||
bool testAndSetVisited();
|
bool testAndSetVisited();
|
||||||
|
@ -459,6 +467,9 @@ public:
|
||||||
void setColor(const QColor& color);
|
void setColor(const QColor& color);
|
||||||
const QColor& getColor() const { return _color; }
|
const QColor& getColor() const { return _color; }
|
||||||
|
|
||||||
|
virtual const QVector<AttributePointer>& getAttributes() const;
|
||||||
|
virtual bool getAttributeValues(MetavoxelInfo& info) const;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
|
||||||
void colorChanged(const QColor& color);
|
void colorChanged(const QColor& color);
|
||||||
|
@ -473,6 +484,8 @@ private slots:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
|
void getNormal(MetavoxelInfo& info) const;
|
||||||
|
|
||||||
QColor _color;
|
QColor _color;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -123,3 +123,40 @@ ClearSpannersEdit::ClearSpannersEdit(const AttributePointer& attribute) :
|
||||||
void ClearSpannersEdit::apply(MetavoxelData& data) const {
|
void ClearSpannersEdit::apply(MetavoxelData& data) const {
|
||||||
data.clear(attribute);
|
data.clear(attribute);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SetSpannerEditVisitor : public MetavoxelVisitor {
|
||||||
|
public:
|
||||||
|
|
||||||
|
SetSpannerEditVisitor(Spanner* spanner);
|
||||||
|
|
||||||
|
virtual int visit(MetavoxelInfo& info);
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
Spanner* _spanner;
|
||||||
|
};
|
||||||
|
|
||||||
|
SetSpannerEditVisitor::SetSpannerEditVisitor(Spanner* spanner) :
|
||||||
|
MetavoxelVisitor(QVector<AttributePointer>(), spanner->getAttributes()),
|
||||||
|
_spanner(spanner) {
|
||||||
|
}
|
||||||
|
|
||||||
|
int SetSpannerEditVisitor::visit(MetavoxelInfo& info) {
|
||||||
|
return _spanner->getAttributeValues(info) ? DEFAULT_ORDER : STOP_RECURSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetSpannerEdit::SetSpannerEdit(const SharedObjectPointer& spanner) :
|
||||||
|
spanner(spanner) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetSpannerEdit::apply(MetavoxelData& data) const {
|
||||||
|
Spanner* spanner = static_cast<Spanner*>(this->spanner.data());
|
||||||
|
|
||||||
|
// expand to fit the entire spanner
|
||||||
|
while (!data.getBounds().contains(spanner->getBounds())) {
|
||||||
|
data.expand();
|
||||||
|
}
|
||||||
|
|
||||||
|
SetSpannerEditVisitor visitor(spanner);
|
||||||
|
data.guide(visitor);
|
||||||
|
}
|
||||||
|
|
|
@ -161,4 +161,19 @@ public:
|
||||||
|
|
||||||
DECLARE_STREAMABLE_METATYPE(ClearSpannersEdit)
|
DECLARE_STREAMABLE_METATYPE(ClearSpannersEdit)
|
||||||
|
|
||||||
|
/// An edit that sets a spanner's attributes in the voxel tree.
|
||||||
|
class SetSpannerEdit : public MetavoxelEdit {
|
||||||
|
STREAMABLE
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
STREAM SharedObjectPointer spanner;
|
||||||
|
|
||||||
|
SetSpannerEdit(const SharedObjectPointer& spanner = SharedObjectPointer());
|
||||||
|
|
||||||
|
virtual void apply(MetavoxelData& data) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
DECLARE_STREAMABLE_METATYPE(SetSpannerEdit)
|
||||||
|
|
||||||
#endif /* defined(__interface__MetavoxelMessages__) */
|
#endif /* defined(__interface__MetavoxelMessages__) */
|
||||||
|
|
|
@ -162,6 +162,17 @@ bool Box::intersects(const Box& other) const {
|
||||||
other.maximum.z >= minimum.z && other.minimum.z <= maximum.z;
|
other.maximum.z >= minimum.z && other.minimum.z <= maximum.z;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const int X_MAXIMUM_FLAG = 1;
|
||||||
|
const int Y_MAXIMUM_FLAG = 2;
|
||||||
|
const int Z_MAXIMUM_FLAG = 4;
|
||||||
|
|
||||||
|
glm::vec3 Box::getVertex(int index) const {
|
||||||
|
return glm::vec3(
|
||||||
|
(index & X_MAXIMUM_FLAG) ? maximum.x : minimum.x,
|
||||||
|
(index & Y_MAXIMUM_FLAG) ? maximum.y : minimum.y,
|
||||||
|
(index & Z_MAXIMUM_FLAG) ? maximum.z : minimum.z);
|
||||||
|
}
|
||||||
|
|
||||||
Box operator*(const glm::mat4& matrix, const Box& box) {
|
Box operator*(const glm::mat4& matrix, const Box& box) {
|
||||||
// start with the constant component
|
// start with the constant component
|
||||||
Box newBox(glm::vec3(matrix[3][0], matrix[3][1], matrix[3][2]), glm::vec3(matrix[3][0], matrix[3][1], matrix[3][2]));
|
Box newBox(glm::vec3(matrix[3][0], matrix[3][1], matrix[3][2]), glm::vec3(matrix[3][0], matrix[3][1], matrix[3][2]));
|
||||||
|
|
|
@ -34,6 +34,8 @@ class Box {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
|
static const int VERTEX_COUNT = 8;
|
||||||
|
|
||||||
STREAM glm::vec3 minimum;
|
STREAM glm::vec3 minimum;
|
||||||
STREAM glm::vec3 maximum;
|
STREAM glm::vec3 maximum;
|
||||||
|
|
||||||
|
@ -44,6 +46,10 @@ public:
|
||||||
bool intersects(const Box& other) const;
|
bool intersects(const Box& other) const;
|
||||||
|
|
||||||
float getLongestSide() const { return qMax(qMax(maximum.x - minimum.x, maximum.y - minimum.y), maximum.z - minimum.z); }
|
float getLongestSide() const { return qMax(qMax(maximum.x - minimum.x, maximum.y - minimum.y), maximum.z - minimum.z); }
|
||||||
|
|
||||||
|
glm::vec3 getVertex(int index) const;
|
||||||
|
|
||||||
|
glm::vec3 getCenter() const { return (minimum + maximum) * 0.5f; }
|
||||||
};
|
};
|
||||||
|
|
||||||
DECLARE_STREAMABLE_METATYPE(Box)
|
DECLARE_STREAMABLE_METATYPE(Box)
|
||||||
|
|
Loading…
Reference in a new issue