#include "VoxelEditor.hpp" #include "GridSystem.hpp" #include #include #include #include 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::getPlacedModels () const { return mPlacedModels; } void VoxelEditor::processInput ( const glm::vec3& rayOrigin, const glm::vec3& rayDir, bool leftMouseDown, bool rightMouseDown, bool shiftPressed, const std::vector>& 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( 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( std::round( hitNormal.x ) ), static_cast( std::round( hitNormal.y ) ), static_cast( 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& VoxelEditor::getPlacedCells () const { return mPlacedList; } const std::vector& 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( 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 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> VoxelEditor::getAllCollisionBoxes ( const std::vector>& baseWorldBoxes ) const { std::vector> 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 VoxelEditor::rasterizeGridBox ( const glm::ivec3& a, const glm::ivec3& b ) const { std::vector 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 VoxelEditor::rasterizeCircle ( const glm::ivec3& centerCell, int radiusCells, int heightLayers ) const { std::vector 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( 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(), voxelJson["y"].get(), voxelJson["z"].get() ); 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