aboutsummaryrefslogtreecommitdiff
path: root/apps/openmb/scene/VoxelEditor.cpp
diff options
context:
space:
mode:
authorEthan Morgan <ethan@gweithio.com>2026-02-14 16:44:06 +0000
committerEthan Morgan <ethan@gweithio.com>2026-02-14 16:44:06 +0000
commit54409423f767d8b1cf30cb7d0efca6b4ca138823 (patch)
treed915ac7828703ce4b963efdd9728a1777ba18c1e /apps/openmb/scene/VoxelEditor.cpp
move to own git serverHEADmaster
Diffstat (limited to 'apps/openmb/scene/VoxelEditor.cpp')
-rw-r--r--apps/openmb/scene/VoxelEditor.cpp602
1 files changed, 602 insertions, 0 deletions
diff --git a/apps/openmb/scene/VoxelEditor.cpp b/apps/openmb/scene/VoxelEditor.cpp
new file mode 100644
index 0000000..b113580
--- /dev/null
+++ b/apps/openmb/scene/VoxelEditor.cpp
@@ -0,0 +1,602 @@
+#include "VoxelEditor.hpp"
+
+#include "GridSystem.hpp"
+
+#include <algorithm>
+#include <cmath>
+#include <fstream>
+
+#include <nlohmann/json.hpp>
+
+namespace scene {
+VoxelEditor::VoxelEditor ( const GridSystem& gridSystem )
+ : mGridSystem( gridSystem ), mPlacedSet(), mPlacedList(), mCellTextureIds(), mCellCollidable(), mUndoStack(),
+ mRedoStack(), mPlacedModels(), mDragging( false ), mDragButton( -1 ), mDragStartCell( 0 ), mPreviewCells(),
+ mPrevLeftDown( false ), mPrevRightDown( false ) {
+}
+
+void VoxelEditor::addModelInstance ( const ModelInstance& mi ) {
+
+ Action action;
+ Action::ModelInstance ami;
+ ami.modelIndex = mi.modelIndex;
+ ami.pos = mi.pos;
+ ami.yaw = mi.yaw;
+ ami.scale = mi.scale;
+ ami.mCollidable = mi.mCollidable;
+ action.mAddedModels.push_back( ami );
+ mPlacedModels.push_back( mi );
+
+ mUndoStack.push_back( action );
+ if( mUndoStack.size() > mMaxUndoSteps )
+ mUndoStack.erase( mUndoStack.begin() );
+ mRedoStack.clear();
+}
+
+const std::vector<VoxelEditor::ModelInstance>& VoxelEditor::getPlacedModels () const {
+ return mPlacedModels;
+}
+
+void VoxelEditor::processInput ( const glm::vec3& rayOrigin, const glm::vec3& rayDir, bool leftMouseDown,
+ bool rightMouseDown, bool shiftPressed,
+ const std::vector<std::pair<glm::vec3, glm::vec3>>& baseWorldBoxes,
+ int currentTextureId, bool placeCollidable ) {
+ bool hitExisting = false;
+ float bestT = FLT_MAX;
+ glm::ivec3 hitCell( 0 );
+ glm::vec3 hitNormal( 0.0f );
+
+ for( const auto& c : mPlacedList ) {
+ auto aabb = cellToAABB( c );
+ float t = 0.0f;
+ if( rayAABBIntersect( rayOrigin, rayDir, aabb.first, aabb.second, t ) ) {
+ if( t >= 0.0f && t < bestT ) {
+ bestT = t;
+ hitExisting = true;
+ hitCell = c;
+ glm::vec3 hitPoint = rayOrigin + rayDir * t;
+ glm::vec3 center = ( aabb.first + aabb.second ) * 0.5f;
+ glm::vec3 diff = hitPoint - center;
+ glm::vec3 ad = glm::abs( diff );
+ if( ad.x > ad.y && ad.x > ad.z )
+ hitNormal = glm::vec3( diff.x > 0.0f ? 1.0f : -1.0f, 0.0f, 0.0f );
+ else if( ad.y > ad.x && ad.y > ad.z )
+ hitNormal = glm::vec3( 0.0f, diff.y > 0.0f ? 1.0f : -1.0f, 0.0f );
+ else
+ hitNormal = glm::vec3( 0.0f, 0.0f, diff.z > 0.0f ? 1.0f : -1.0f );
+ }
+ }
+ }
+
+ glm::ivec3 placeCell( 0 );
+ bool validHit = false;
+
+ if( !hitExisting ) {
+ float wallT = FLT_MAX;
+ glm::vec3 wallHitPoint( 0.0f );
+ glm::vec3 wallHitNormal( 0.0f );
+ bool hitWall = false;
+
+ for( const auto& box : baseWorldBoxes ) {
+ float t = 0.0f;
+ if( rayAABBIntersect( rayOrigin, rayDir, box.first, box.second, t ) ) {
+ if( t > 0.0f && t < wallT ) {
+ wallT = t;
+ wallHitPoint = rayOrigin + rayDir * t;
+ glm::vec3 center = ( box.first + box.second ) * 0.5f;
+ glm::vec3 diff = wallHitPoint - center;
+ glm::vec3 ad = glm::abs( diff );
+ if( ad.x > ad.y && ad.x > ad.z )
+ wallHitNormal = glm::vec3( diff.x > 0.0f ? 1.0f : -1.0f, 0.0f, 0.0f );
+ else if( ad.y > ad.x && ad.y > ad.z )
+ wallHitNormal = glm::vec3( 0.0f, diff.y > 0.0f ? 1.0f : -1.0f, 0.0f );
+ else
+ wallHitNormal = glm::vec3( 0.0f, 0.0f, diff.z > 0.0f ? 1.0f : -1.0f );
+ hitWall = true;
+ }
+ }
+ }
+
+ if( hitWall ) {
+
+ glm::vec3 offsetPoint = wallHitPoint + wallHitNormal * 0.01f;
+
+ int xi = 0, zi = 0;
+ mGridSystem.worldToGrid( offsetPoint, xi, zi );
+ int yi = static_cast<int>( std::floor( offsetPoint.y / mGridSystem.getCellSize() ) );
+
+ hitCell = glm::ivec3( xi, yi, zi );
+ hitNormal = wallHitNormal;
+ placeCell = hitCell;
+ validHit = true;
+ } else if( std::abs( rayDir.y ) > 1e-6f ) {
+ float t = ( 0.0f - rayOrigin.y ) / rayDir.y;
+ if( t > 0.0f ) {
+ glm::vec3 hitPoint = rayOrigin + rayDir * t;
+ hitCell = worldPosToCell( hitPoint );
+ hitNormal = glm::vec3( 0.0f, 1.0f, 0.0f );
+ placeCell = hitCell;
+ validHit = true;
+ }
+ }
+ }
+
+ if( hitExisting ) {
+ validHit = true;
+ placeCell = hitCell + glm::ivec3( static_cast<int>( std::round( hitNormal.x ) ),
+ static_cast<int>( std::round( hitNormal.y ) ),
+ static_cast<int>( std::round( hitNormal.z ) ) );
+ placeCell.x = std::clamp( placeCell.x, 0, mGridSystem.getGridWidth() - 1 );
+ placeCell.z = std::clamp( placeCell.z, 0, mGridSystem.getGridDepth() - 1 );
+ } else if( validHit ) {
+ placeCell.x = std::clamp( placeCell.x, 0, mGridSystem.getGridWidth() - 1 );
+ placeCell.z = std::clamp( placeCell.z, 0, mGridSystem.getGridDepth() - 1 );
+ }
+
+ if( leftMouseDown && !mPrevLeftDown ) {
+ mDragging = true;
+ mDragButton = 0;
+ mDragStartCell = placeCell;
+ }
+ if( rightMouseDown && !mPrevRightDown ) {
+ mDragging = true;
+ mDragButton = 1;
+ mDragStartCell = placeCell;
+ }
+
+ if( mDragging ) {
+ glm::ivec3 endCell = placeCell;
+ if( shiftPressed ) {
+ endCell.y = mDragStartCell.y;
+ }
+ mPreviewCells = rasterizeGridBox( mDragStartCell, endCell );
+ }
+
+ if( !leftMouseDown && mPrevLeftDown && mDragButton == 0 ) {
+ Action action;
+ for( const auto& cell : mPreviewCells ) {
+ if( mPlacedSet.insert( cell ).second ) {
+ mPlacedList.push_back( cell );
+ mCellTextureIds[cell] = currentTextureId;
+ mCellCollidable[cell] = placeCollidable;
+ VoxelData voxelData;
+ voxelData.mCell = cell;
+ voxelData.mTextureId = currentTextureId;
+ voxelData.mCollidable = placeCollidable;
+ action.mAddedCells.push_back( voxelData );
+ }
+ }
+ if( !action.mAddedCells.empty() ) {
+ mUndoStack.push_back( action );
+ if( mUndoStack.size() > mMaxUndoSteps )
+ mUndoStack.erase( mUndoStack.begin() );
+ mRedoStack.clear();
+ }
+ mDragging = false;
+ }
+
+ if( !rightMouseDown && mPrevRightDown && mDragButton == 1 ) {
+ Action action;
+ for( const auto& cell : mPreviewCells ) {
+ if( mPlacedSet.erase( cell ) > 0 ) {
+ mPlacedList.erase( std::remove( mPlacedList.begin(), mPlacedList.end(), cell ), mPlacedList.end() );
+ auto texIt = mCellTextureIds.find( cell );
+ VoxelData voxelData;
+ voxelData.mCell = cell;
+ voxelData.mTextureId = ( texIt != mCellTextureIds.end() ) ? texIt->second : 0;
+ auto collIt = mCellCollidable.find( cell );
+ voxelData.mCollidable = ( collIt != mCellCollidable.end() ) ? collIt->second : true;
+ action.mRemovedCells.push_back( voxelData );
+ mCellTextureIds.erase( cell );
+ mCellCollidable.erase( cell );
+ }
+ }
+ if( !action.mRemovedCells.empty() ) {
+ mUndoStack.push_back( action );
+ if( mUndoStack.size() > mMaxUndoSteps )
+ mUndoStack.erase( mUndoStack.begin() );
+ mRedoStack.clear();
+ }
+ mDragging = false;
+ }
+
+ if( !mDragging ) {
+ if( validHit ) {
+ mPreviewCells.clear();
+ mPreviewCells.push_back( placeCell );
+ } else {
+ mPreviewCells.clear();
+ }
+ }
+
+ mPrevLeftDown = leftMouseDown;
+ mPrevRightDown = rightMouseDown;
+}
+
+void VoxelEditor::undo () {
+ if( mUndoStack.empty() )
+ return;
+
+ Action action = mUndoStack.back();
+ mUndoStack.pop_back();
+
+ for( const auto& voxelData : action.mAddedCells ) {
+ mPlacedSet.erase( voxelData.mCell );
+ mPlacedList.erase( std::remove( mPlacedList.begin(), mPlacedList.end(), voxelData.mCell ),
+ mPlacedList.end() );
+ mCellTextureIds.erase( voxelData.mCell );
+ mCellCollidable.erase( voxelData.mCell );
+ }
+ for( const auto& voxelData : action.mRemovedCells ) {
+ if( mPlacedSet.insert( voxelData.mCell ).second ) {
+ mPlacedList.push_back( voxelData.mCell );
+ mCellTextureIds[voxelData.mCell] = voxelData.mTextureId;
+ mCellCollidable[voxelData.mCell] = voxelData.mCollidable;
+ }
+ }
+
+ for( const auto& mi : action.mAddedModels ) {
+
+ auto it = std::find_if( mPlacedModels.begin(), mPlacedModels.end(),
+ [&] ( const ModelInstance& m ) {
+ return m.modelIndex == mi.modelIndex && m.pos == mi.pos && m.yaw == mi.yaw &&
+ m.scale == mi.scale;
+ } );
+ if( it != mPlacedModels.end() )
+ mPlacedModels.erase( it );
+ }
+ for( const auto& mi : action.mRemovedModels ) {
+ ModelInstance m;
+ m.modelIndex = mi.modelIndex;
+ m.pos = mi.pos;
+ m.yaw = mi.yaw;
+ m.scale = mi.scale;
+ m.mCollidable = mi.mCollidable;
+ mPlacedModels.push_back( m );
+ }
+
+ mRedoStack.push_back( action );
+}
+
+void VoxelEditor::redo () {
+ if( mRedoStack.empty() )
+ return;
+
+ Action action = mRedoStack.back();
+ mRedoStack.pop_back();
+
+ for( const auto& voxelData : action.mAddedCells ) {
+ if( mPlacedSet.insert( voxelData.mCell ).second ) {
+ mPlacedList.push_back( voxelData.mCell );
+ mCellTextureIds[voxelData.mCell] = voxelData.mTextureId;
+ mCellCollidable[voxelData.mCell] = voxelData.mCollidable;
+ }
+ }
+ for( const auto& voxelData : action.mRemovedCells ) {
+ mPlacedSet.erase( voxelData.mCell );
+ mPlacedList.erase( std::remove( mPlacedList.begin(), mPlacedList.end(), voxelData.mCell ),
+ mPlacedList.end() );
+ mCellTextureIds.erase( voxelData.mCell );
+ mCellCollidable.erase( voxelData.mCell );
+ }
+
+ for( const auto& mi : action.mAddedModels ) {
+ ModelInstance m;
+ m.modelIndex = mi.modelIndex;
+ m.pos = mi.pos;
+ m.yaw = mi.yaw;
+ m.scale = mi.scale;
+ m.mCollidable = mi.mCollidable;
+ mPlacedModels.push_back( m );
+ }
+ for( const auto& mi : action.mRemovedModels ) {
+ auto it = std::find_if( mPlacedModels.begin(), mPlacedModels.end(),
+ [&] ( const ModelInstance& m ) {
+ return m.modelIndex == mi.modelIndex && m.pos == mi.pos && m.yaw == mi.yaw &&
+ m.scale == mi.scale;
+ } );
+ if( it != mPlacedModels.end() )
+ mPlacedModels.erase( it );
+ }
+
+ mUndoStack.push_back( action );
+}
+
+const std::vector<glm::ivec3>& VoxelEditor::getPlacedCells () const {
+ return mPlacedList;
+}
+
+const std::vector<glm::ivec3>& VoxelEditor::getPreviewCells () const {
+ return mPreviewCells;
+}
+
+size_t VoxelEditor::getUndoStackSize () const {
+ return mUndoStack.size();
+}
+
+size_t VoxelEditor::getRedoStackSize () const {
+ return mRedoStack.size();
+}
+
+glm::ivec3 VoxelEditor::worldPosToCell ( const glm::vec3& pos ) const {
+ int xi = 0, zi = 0;
+ mGridSystem.worldToGrid( pos, xi, zi );
+ int yi = static_cast<int>( std::floor( ( pos.y - mGridSystem.getFloorY() ) / mGridSystem.getCellSize() ) );
+ return glm::ivec3( xi, yi, zi );
+}
+
+glm::vec3 VoxelEditor::cellToWorldCenter ( const glm::ivec3& cell ) const {
+ return mGridSystem.getCellCenter( cell.x, cell.z, cell.y );
+}
+
+std::pair<glm::vec3, glm::vec3> VoxelEditor::cellToAABB ( const glm::ivec3& cell ) const {
+ glm::vec3 center = cellToWorldCenter( cell );
+ float halfSize = mGridSystem.getCellSize() * 0.5f;
+ glm::vec3 he( halfSize, halfSize, halfSize );
+ return { center - he, center + he };
+}
+
+std::vector<std::pair<glm::vec3, glm::vec3>>
+VoxelEditor::getAllCollisionBoxes ( const std::vector<std::pair<glm::vec3, glm::vec3>>& baseWorldBoxes ) const {
+ std::vector<std::pair<glm::vec3, glm::vec3>> boxes = baseWorldBoxes;
+ for( const auto& c : mPlacedList ) {
+ auto it = mCellCollidable.find( c );
+ bool coll = true;
+ if( it != mCellCollidable.end() )
+ coll = it->second;
+ if( coll )
+ boxes.push_back( cellToAABB( c ) );
+ }
+
+ return boxes;
+}
+
+bool VoxelEditor::rayAABBIntersect ( const glm::vec3& ro, const glm::vec3& rd, const glm::vec3& bmin,
+ const glm::vec3& bmax, float& outT ) const {
+ float tmin = -FLT_MAX;
+ float tmax = FLT_MAX;
+ for( int i = 0; i < 3; ++i ) {
+ float origin = ro[i];
+ float dir = rd[i];
+ float minB = bmin[i];
+ float maxB = bmax[i];
+ if( std::abs( dir ) < 1e-6f ) {
+ if( origin < minB || origin > maxB )
+ return false;
+ } else {
+ float t1 = ( minB - origin ) / dir;
+ float t2 = ( maxB - origin ) / dir;
+ if( t1 > t2 )
+ std::swap( t1, t2 );
+ tmin = std::max( tmin, t1 );
+ tmax = std::min( tmax, t2 );
+ if( tmin > tmax )
+ return false;
+ }
+ }
+ if( tmax < 0.0f )
+ return false;
+ outT = tmin >= 0.0f ? tmin : tmax;
+ return true;
+}
+
+std::vector<glm::ivec3> VoxelEditor::rasterizeGridBox ( const glm::ivec3& a, const glm::ivec3& b ) const {
+ std::vector<glm::ivec3> out;
+ glm::ivec3 minC( std::min( a.x, b.x ), std::min( a.y, b.y ), std::min( a.z, b.z ) );
+ glm::ivec3 maxC( std::max( a.x, b.x ), std::max( a.y, b.y ), std::max( a.z, b.z ) );
+
+ minC.x = std::clamp( minC.x, 0, mGridSystem.getGridWidth() - 1 );
+ maxC.x = std::clamp( maxC.x, 0, mGridSystem.getGridWidth() - 1 );
+ minC.z = std::clamp( minC.z, 0, mGridSystem.getGridDepth() - 1 );
+ maxC.z = std::clamp( maxC.z, 0, mGridSystem.getGridDepth() - 1 );
+
+ for( int x = minC.x; x <= maxC.x; ++x ) {
+ for( int y = minC.y; y <= maxC.y; ++y ) {
+ for( int z = minC.z; z <= maxC.z; ++z ) {
+ out.emplace_back( x, y, z );
+ }
+ }
+ }
+
+ return out;
+}
+
+std::vector<glm::ivec3> VoxelEditor::rasterizeCircle ( const glm::ivec3& centerCell, int radiusCells,
+ int heightLayers ) const {
+ std::vector<glm::ivec3> out;
+
+ if( radiusCells < 1 )
+ radiusCells = 1;
+ if( heightLayers < 1 )
+ heightLayers = 1;
+
+ int minX = std::max( 0, centerCell.x - radiusCells );
+ int maxX = std::min( mGridSystem.getGridWidth() - 1, centerCell.x + radiusCells );
+ int minZ = std::max( 0, centerCell.z - radiusCells );
+ int maxZ = std::min( mGridSystem.getGridDepth() - 1, centerCell.z + radiusCells );
+
+ int maxY = mGridSystem.getWallHeight() - 1;
+
+ int r2 = radiusCells * radiusCells;
+
+ for( int x = minX; x <= maxX; ++x ) {
+ for( int z = minZ; z <= maxZ; ++z ) {
+ int dx = x - centerCell.x;
+ int dz = z - centerCell.z;
+ if( dx * dx + dz * dz <= r2 ) {
+ for( int h = 0; h < heightLayers; ++h ) {
+ int y = centerCell.y + h;
+ y = std::clamp( y, 0, maxY );
+ out.emplace_back( x, y, z );
+ }
+ }
+ }
+ }
+
+ return out;
+}
+
+void VoxelEditor::applyCircularBrush ( const glm::vec3& centerWorld, float radiusWorld, float /*heightWorld*/,
+ int textureId, bool placeCollidable ) {
+ float cellSize = mGridSystem.getCellSize();
+
+ int radiusCells = std::max( 1, static_cast<int>( std::ceil( radiusWorld / cellSize ) ) );
+
+ glm::ivec3 centerCell = worldPosToCell( centerWorld );
+
+ int minX = std::max( 0, centerCell.x - radiusCells );
+ int maxX = std::min( mGridSystem.getGridWidth() - 1, centerCell.x + radiusCells );
+ int minZ = std::max( 0, centerCell.z - radiusCells );
+ int maxZ = std::min( mGridSystem.getGridDepth() - 1, centerCell.z + radiusCells );
+
+ int targetY = centerCell.y;
+ int maxY = mGridSystem.getWallHeight() - 1;
+ targetY = std::clamp( targetY, 0, maxY );
+
+ float r2 = radiusWorld * radiusWorld;
+
+ Action action;
+
+ for( int x = minX; x <= maxX; ++x ) {
+ for( int z = minZ; z <= maxZ; ++z ) {
+
+ glm::vec3 cellCenter = mGridSystem.getCellCenter( x, z, targetY );
+ float dx = cellCenter.x - centerWorld.x;
+ float dz = cellCenter.z - centerWorld.z;
+ if( dx * dx + dz * dz <= r2 ) {
+ glm::ivec3 cell( x, targetY, z );
+ if( mPlacedSet.insert( cell ).second ) {
+ mPlacedList.push_back( cell );
+ mCellTextureIds[cell] = textureId;
+ mCellCollidable[cell] = placeCollidable;
+ VoxelData vd;
+ vd.mCell = cell;
+ vd.mTextureId = textureId;
+ vd.mCollidable = placeCollidable;
+ action.mAddedCells.push_back( vd );
+ }
+ }
+ }
+ }
+
+ if( !action.mAddedCells.empty() ) {
+ mUndoStack.push_back( action );
+ if( mUndoStack.size() > mMaxUndoSteps )
+ mUndoStack.erase( mUndoStack.begin() );
+ mRedoStack.clear();
+ }
+}
+
+int VoxelEditor::getTextureIdForCell ( const glm::ivec3& cell ) const {
+ auto it = mCellTextureIds.find( cell );
+ if( it != mCellTextureIds.end() )
+ return it->second;
+ return -1;
+}
+
+bool VoxelEditor::saveToFile ( const std::string& filepath ) const {
+ try {
+ nlohmann::json j;
+ j["version"] = 1;
+
+ nlohmann::json voxelsArray = nlohmann::json::array();
+ for( const auto& cell : mPlacedList ) {
+ auto it = mCellTextureIds.find( cell );
+ int textureId = ( it != mCellTextureIds.end() ) ? it->second : 0;
+
+ nlohmann::json voxel;
+ voxel["x"] = cell.x;
+ voxel["y"] = cell.y;
+ voxel["z"] = cell.z;
+ voxel["textureId"] = textureId;
+ auto cit = mCellCollidable.find( cell );
+ voxel["collidable"] = ( cit != mCellCollidable.end() ) ? cit->second : true;
+ voxelsArray.push_back( voxel );
+ }
+
+ j["voxels"] = voxelsArray;
+
+ nlohmann::json modelsArray = nlohmann::json::array();
+ for( const auto& m : mPlacedModels ) {
+ nlohmann::json mj;
+ mj["modelIndex"] = m.modelIndex;
+ mj["x"] = m.pos.x;
+ mj["y"] = m.pos.y;
+ mj["z"] = m.pos.z;
+ mj["yaw"] = m.yaw;
+ mj["scale"] = m.scale;
+ mj["collidable"] = m.mCollidable;
+ modelsArray.push_back( mj );
+ }
+ j["models"] = modelsArray;
+
+ std::ofstream file( filepath );
+ if( !file.is_open() )
+ return false;
+
+ file << j.dump( 2 );
+ file.close();
+ return true;
+ } catch( ... ) {
+ return false;
+ }
+}
+
+bool VoxelEditor::loadFromFile ( const std::string& filepath ) {
+ try {
+ std::ifstream file( filepath );
+ if( !file.is_open() )
+ return false;
+
+ nlohmann::json j;
+ file >> j;
+ file.close();
+
+ if( !j.contains( "version" ) || !j.contains( "voxels" ) )
+ return false;
+
+ clear();
+
+ for( const auto& voxelJson : j["voxels"] ) {
+ glm::ivec3 cell( voxelJson["x"].get<int>(), voxelJson["y"].get<int>(),
+ voxelJson["z"].get<int>() );
+
+ int textureId = voxelJson.value( "textureId", 0 );
+ bool collidable = voxelJson.value( "collidable", true );
+
+ if( mPlacedSet.insert( cell ).second ) {
+ mPlacedList.push_back( cell );
+ mCellTextureIds[cell] = textureId;
+ mCellCollidable[cell] = collidable;
+ }
+ }
+
+ if( j.contains( "models" ) && j["models"].is_array() ) {
+ for( const auto& mj : j["models"] ) {
+ ModelInstance mi;
+ mi.modelIndex = mj.value( "modelIndex", 0 );
+ mi.pos.x = mj.value( "x", 0.0f );
+ mi.pos.y = mj.value( "y", 0.0f );
+ mi.pos.z = mj.value( "z", 0.0f );
+ mi.yaw = mj.value( "yaw", 0.0f );
+ mi.scale = mj.value( "scale", 1.0f );
+ mi.mCollidable = mj.value( "collidable", true );
+ mPlacedModels.push_back( mi );
+ }
+ }
+
+ return true;
+ } catch( ... ) {
+ return false;
+ }
+}
+
+void VoxelEditor::clear () {
+ mPlacedSet.clear();
+ mPlacedList.clear();
+ mCellTextureIds.clear();
+ mUndoStack.clear();
+ mRedoStack.clear();
+ mPlacedModels.clear();
+}
+} // namespace scene