/* Copyright (C) 1999-2006 Id Software, Inc. and contributors. For a list of contributors, see the accompanying CONTRIBUTORS file. This file is part of GtkRadiant. GtkRadiant is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. GtkRadiant 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 General Public License for more details. You should have received a copy of the GNU General Public License along with GtkRadiant; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "modelwindow.h" #include #include #include "ifiletypes.h" #include "ifilesystem.h" #include "iarchive.h" #include "imodel.h" #include "igl.h" #include "irender.h" #include "renderable.h" #include "render.h" #include "renderer.h" #include "view.h" #include "os/path.h" #include "string/string.h" #include "stringio.h" #include "stream/stringstream.h" #include "generic/callback.h" #include #include "gtkutil/glwidget.h" #include "gtkutil/button.h" #include "gtkutil/toolbar.h" #include "gtkutil/cursor.h" #include "gtkmisc.h" #include "mainframe.h" #include "camwindow.h" #include "grid.h" #include "instancelib.h" #include "traverselib.h" #include "selectionlib.h" /* specialized copy of class CompiledGraph */ class ModelGraph final : public scene::Graph, public scene::Instantiable::Observer { typedef std::map InstanceMap; InstanceMap m_instances; scene::Path m_rootpath; scene::Instantiable::Observer& m_observer; public: ModelGraph( scene::Instantiable::Observer& observer ) : m_observer( observer ){ } void addSceneChangedCallback( const SignalHandler& handler ){ ASSERT_MESSAGE( 0, "Reached unreachable: addSceneChangedCallback()" ); } void sceneChanged(){ ASSERT_MESSAGE( 0, "Reached unreachable: sceneChanged()" ); } scene::Node& root(){ ASSERT_MESSAGE( !m_rootpath.empty(), "scenegraph root does not exist" ); return m_rootpath.top(); } void insert_root( scene::Node& root ){ //globalOutputStream() << "insert_root\n"; ASSERT_MESSAGE( m_rootpath.empty(), "scenegraph root already exists" ); root.IncRef(); Node_traverseSubgraph( root, InstanceSubgraphWalker( this, scene::Path(), 0 ) ); m_rootpath.push( makeReference( root ) ); } void erase_root(){ //globalOutputStream() << "erase_root\n"; ASSERT_MESSAGE( !m_rootpath.empty(), "scenegraph root does not exist" ); scene::Node& root = m_rootpath.top(); m_rootpath.pop(); Node_traverseSubgraph( root, UninstanceSubgraphWalker( this, scene::Path() ) ); root.DecRef(); } void boundsChanged(){ ASSERT_MESSAGE( 0, "Reached unreachable: boundsChanged()" ); } void traverse( const Walker& walker ){ ASSERT_MESSAGE( 0, "Reached unreachable: traverse()" ); } void traverse_subgraph( const Walker& walker, const scene::Path& start ){ ASSERT_MESSAGE( 0, "Reached unreachable: traverse_subgraph()" ); } scene::Instance* find( const scene::Path& path ){ ASSERT_MESSAGE( 0, "Reached unreachable: find()" ); return nullptr; } void insert( scene::Instance* instance ){ m_instances.insert( InstanceMap::value_type( PathConstReference( instance->path() ), instance ) ); m_observer.insert( instance ); } void erase( scene::Instance* instance ){ m_instances.erase( PathConstReference( instance->path() ) ); m_observer.erase( instance ); } SignalHandlerId addBoundsChangedCallback( const SignalHandler& boundsChanged ){ ASSERT_MESSAGE( 0, "Reached unreachable: addBoundsChangedCallback()" ); return Handle>( nullptr ); } void removeBoundsChangedCallback( SignalHandlerId id ){ ASSERT_MESSAGE( 0, "Reached unreachable: removeBoundsChangedCallback()" ); } TypeId getNodeTypeId( const char* name ){ ASSERT_MESSAGE( 0, "Reached unreachable: getNodeTypeId()" ); return 0; } TypeId getInstanceTypeId( const char* name ){ ASSERT_MESSAGE( 0, "Reached unreachable: getInstanceTypeId()" ); return 0; } void clear(){ DeleteSubgraph( root() ); } }; /* specialized copy of class TraversableNodeSet */ /// \brief A sequence of node references which notifies an observer of inserts and deletions, and uses the global undo system to provide undo for modifications. class TraversableModelNodeSet : public scene::Traversable { UnsortedNodeSet m_children; Observer* m_observer; void copy( const TraversableModelNodeSet& other ){ m_children = other.m_children; } void notifyInsertAll(){ if ( m_observer ) { for ( UnsortedNodeSet::iterator i = m_children.begin(); i != m_children.end(); ++i ) { m_observer->insert( *i ); } } } void notifyEraseAll(){ if ( m_observer ) { for ( UnsortedNodeSet::iterator i = m_children.begin(); i != m_children.end(); ++i ) { m_observer->erase( *i ); } } } public: TraversableModelNodeSet() : m_observer( 0 ){ } TraversableModelNodeSet( const TraversableModelNodeSet& other ) : scene::Traversable( other ), m_observer( 0 ){ copy( other ); notifyInsertAll(); } ~TraversableModelNodeSet(){ notifyEraseAll(); } TraversableModelNodeSet& operator=( const TraversableModelNodeSet& other ){ #if 1 // optimised change-tracking using diff algorithm if ( m_observer ) { nodeset_diff( m_children, other.m_children, m_observer ); } copy( other ); #else TraversableModelNodeSet tmp( other ); tmp.swap( *this ); #endif return *this; } void swap( TraversableModelNodeSet& other ){ std::swap( m_children, other.m_children ); std::swap( m_observer, other.m_observer ); } void attach( Observer* observer ){ ASSERT_MESSAGE( m_observer == 0, "TraversableModelNodeSet::attach: observer cannot be attached" ); m_observer = observer; notifyInsertAll(); } void detach( Observer* observer ){ ASSERT_MESSAGE( m_observer == observer, "TraversableModelNodeSet::detach: observer cannot be detached" ); notifyEraseAll(); m_observer = 0; } /// \brief \copydoc scene::Traversable::insert() void insert( scene::Node& node ){ ASSERT_MESSAGE( (volatile intptr_t)&node != 0, "TraversableModelNodeSet::insert: sanity check failed" ); ASSERT_MESSAGE( m_children.find( NodeSmartReference( node ) ) == m_children.end(), "TraversableModelNodeSet::insert - element already exists" ); m_children.insert( NodeSmartReference( node ) ); if ( m_observer ) { m_observer->insert( node ); } } /// \brief \copydoc scene::Traversable::erase() void erase( scene::Node& node ){ ASSERT_MESSAGE( (volatile intptr_t)&node != 0, "TraversableModelNodeSet::erase: sanity check failed" ); ASSERT_MESSAGE( m_children.find( NodeSmartReference( node ) ) != m_children.end(), "TraversableModelNodeSet::erase - failed to find element" ); if ( m_observer ) { m_observer->erase( node ); } m_children.erase( NodeSmartReference( node ) ); } /// \brief \copydoc scene::Traversable::traverse() void traverse( const Walker& walker ){ UnsortedNodeSet::iterator i = m_children.begin(); while ( i != m_children.end() ) { // post-increment the iterator Node_traverseSubgraph( *i++, walker ); // the Walker can safely remove the current node from // this container without invalidating the iterator } } /// \brief \copydoc scene::Traversable::empty() bool empty() const { return m_children.empty(); } }; /* specialized copy of class MapRoot */ class ModelGraphRoot : public scene::Node::Symbiot, public scene::Instantiable, public scene::Traversable::Observer { class TypeCasts { NodeTypeCastTable m_casts; public: TypeCasts(){ NodeStaticCast::install( m_casts ); NodeContainedCast::install( m_casts ); NodeContainedCast::install( m_casts ); } NodeTypeCastTable& get(){ return m_casts; } }; scene::Node m_node; IdentityTransform m_transform; TraversableModelNodeSet m_traverse; InstanceSet m_instances; public: typedef LazyStatic StaticTypeCasts; scene::Traversable& get( NullType){ return m_traverse; } TransformNode& get( NullType){ return m_transform; } ModelGraphRoot() : m_node( this, this, StaticTypeCasts::instance().get() ){ m_node.m_isRoot = true; m_traverse.attach( this ); } ~ModelGraphRoot(){ } void release(){ m_traverse.detach( this ); delete this; } scene::Node& node(){ return m_node; } void insert( scene::Node& child ){ m_instances.insert( child ); } void erase( scene::Node& child ){ m_instances.erase( child ); } scene::Node& clone() const { return ( new ModelGraphRoot( *this ) )->node(); } scene::Instance* create( const scene::Path& path, scene::Instance* parent ){ return new SelectableInstance( path, parent ); } void forEachInstance( const scene::Instantiable::Visitor& visitor ){ m_instances.forEachInstance( visitor ); } void insert( scene::Instantiable::Observer* observer, const scene::Path& path, scene::Instance* instance ){ m_instances.insert( observer, path, instance ); } scene::Instance* erase( scene::Instantiable::Observer* observer, const scene::Path& path ){ return m_instances.erase( observer, path ); } }; #include "../plugins/entity/model.h" class ModelNode : public scene::Node::Symbiot, public scene::Instantiable, public scene::Traversable::Observer { class TypeCasts { NodeTypeCastTable m_casts; public: TypeCasts(){ NodeStaticCast::install( m_casts ); NodeContainedCast::install( m_casts ); NodeContainedCast::install( m_casts ); } NodeTypeCastTable& get(){ return m_casts; } }; scene::Node m_node; InstanceSet m_instances; SingletonModel m_model; MatrixTransform m_transform; void construct(){ m_model.attach( this ); } void destroy(){ m_model.detach( this ); } public: typedef LazyStatic StaticTypeCasts; scene::Traversable& get( NullType){ return m_model.getTraversable(); } TransformNode& get( NullType){ return m_transform; } ModelNode() : m_node( this, this, StaticTypeCasts::instance().get() ){ construct(); } ModelNode( const ModelNode& other ) : scene::Node::Symbiot( other ), scene::Instantiable( other ), scene::Traversable::Observer( other ), m_node( this, this, StaticTypeCasts::instance().get() ){ construct(); } ~ModelNode(){ destroy(); } void release(){ delete this; } scene::Node& node(){ return m_node; } void insert( scene::Node& child ){ m_instances.insert( child ); } void erase( scene::Node& child ){ m_instances.erase( child ); } scene::Instance* create( const scene::Path& path, scene::Instance* parent ){ return new SelectableInstance( path, parent ); } void forEachInstance( const scene::Instantiable::Visitor& visitor ){ m_instances.forEachInstance( visitor ); } void insert( scene::Instantiable::Observer* observer, const scene::Path& path, scene::Instance* instance ){ m_instances.insert( observer, path, instance ); } scene::Instance* erase( scene::Instantiable::Observer* observer, const scene::Path& path ){ return m_instances.erase( observer, path ); } void setModel( const char* modelname ){ m_model.modelChanged( modelname ); } }; ModelGraph* g_modelGraph = nullptr; void ModelGraph_clear(){ g_modelGraph->clear(); } class ModelFS { public: const CopiedString m_folderName; ModelFS() = default; ModelFS( const StringRange range ) : m_folderName( range ){ } bool operator<( const ModelFS& other ) const { return string_less( m_folderName.c_str(), other.m_folderName.c_str() ); } mutable std::set m_folders; mutable std::set m_files; void insert( const char* filepath ) const { const char* slash = strchr( filepath, '/' ); if( slash == nullptr ){ m_files.emplace( filepath ); } else{ m_folders.emplace( StringRange( filepath, slash ) ).first->insert( slash + 1 ); } } }; class CellPos { const int m_cellSize; //half size of model square (radius) const int m_fontHeight; const int m_fontDescent; const int m_plusWidth; //pre offset on the left const int m_plusHeight; //above text const int m_cellsInRow; int m_index = 0; public: CellPos( int width, int cellSize, int fontHeight ) : m_cellSize( cellSize ), m_fontHeight( fontHeight ), m_fontDescent( GlobalOpenGL().m_font->getPixelDescent() ), m_plusWidth( 8 ), m_plusHeight( 0 ), m_cellsInRow( std::max( 1, ( width - m_plusWidth ) / ( m_cellSize * 2 + m_plusWidth ) ) ){ } void operator++(){ ++m_index; } Vector3 getOrigin( int index ) const { // origin of model square const int x = ( index % m_cellsInRow ) * m_cellSize * 2 + m_cellSize + ( index % m_cellsInRow + 1 ) * m_plusWidth; const int z = ( index / m_cellsInRow ) * m_cellSize * 2 + m_cellSize + ( index / m_cellsInRow + 1 ) * ( m_fontHeight + m_plusHeight ); return Vector3( x, 0, -z ); } Vector3 getOrigin() const { // origin of model square return getOrigin( m_index ); } Vector3 getTextPos( int index ) const { const int x = ( index % m_cellsInRow ) * m_cellSize * 2 + ( index % m_cellsInRow + 1 ) * m_plusWidth; const int z = ( index / m_cellsInRow ) * m_cellSize * 2 + ( index / m_cellsInRow + 1 ) * ( m_fontHeight + m_plusHeight ) - 1 + m_fontDescent; return Vector3( x, 0, -z ); } Vector3 getTextPos() const { return getTextPos( m_index ); } int getCellSize() const { return m_cellSize; } int totalHeight( int height, int cellCount ) const { return std::max( height, ( ( cellCount - 1 ) / m_cellsInRow + 1 ) * ( m_cellSize * 2 + m_fontHeight + m_plusHeight ) + m_fontHeight ); } int testSelect( int x, int z ) const { // index of cell at ( x, z ) return std::min( m_cellsInRow - 1, ( x / ( m_cellSize * 2 + m_plusWidth ) ) ) + ( m_cellsInRow * ( z / ( m_cellSize * 2 + m_fontHeight + m_plusHeight ) ) ); } }; void ModelBrowser_scrollChanged( void* data, gdouble value ); class ModelBrowser : public scene::Instantiable::Observer { // track instances in the order of insertion std::vector m_modelInstances; public: ModelFS m_modelFS; CopiedString m_prefFoldersToLoad = "*models/99*"; ModelBrowser() : m_scrollAdjustment( ModelBrowser_scrollChanged, this ){ } ~ModelBrowser(){ } FBO* m_fbo = nullptr; FBO* fbo_get(){ return m_fbo = m_fbo? m_fbo : GlobalOpenGL().support_ARB_framebuffer_object? new FBO : new FBO_fallback; } const int m_MSAA = 8; GtkWindow* m_parent = nullptr; GtkWidget* m_gl_widget = nullptr; GtkWidget* m_gl_scroll = nullptr; GtkWidget* m_treeViewTree = nullptr; guint m_sizeHandler; guint m_exposeHandler; int m_width; int m_height; int m_originZ = 0; // <= 0 DeferredAdjustment m_scrollAdjustment; int m_cellSize = 80; CopiedString m_currentFolderPath; const ModelFS* m_currentFolder = nullptr; int m_currentModelId = -1; // selected model index in m_modelInstances, m_currentFolder->m_files; these must be in sync! CellPos constructCellPos() const { return CellPos( m_width, m_cellSize, GlobalOpenGL().m_font->getPixelHeight() ); } void testSelect( int x, int z ){ m_currentModelId = constructCellPos().testSelect( x, z - m_originZ ); if( m_currentModelId >= static_cast( m_modelInstances.size() ) ) m_currentModelId = -1; } private: int totalHeight() const { return constructCellPos().totalHeight( m_height, m_modelInstances.size() ); } void updateScroll() const { GtkAdjustment *vadjustment = gtk_range_get_adjustment( GTK_RANGE( m_gl_scroll ) ); gtk_adjustment_set_value( vadjustment, -m_originZ ); gtk_adjustment_set_page_size( vadjustment, m_height ); gtk_adjustment_set_page_increment( vadjustment, m_height / 2 ); gtk_adjustment_set_step_increment( vadjustment, 20 ); gtk_adjustment_set_lower( vadjustment, 0 ); gtk_adjustment_set_upper( vadjustment, totalHeight() ); g_signal_emit_by_name( G_OBJECT( vadjustment ), "changed" ); } public: void setOriginZ( int origin ){ m_originZ = origin; m_originInvalid = true; queueDraw(); } void queueDraw() const { if ( m_gl_widget != nullptr ) gtk_widget_queue_draw( m_gl_widget ); } bool m_originInvalid = true; void validate(){ if( m_originInvalid ){ m_originInvalid = false; const int lowest = std::min( m_height - totalHeight(), 0 ); m_originZ = std::max( lowest, std::min( m_originZ, 0 ) ); updateScroll(); } } private: FreezePointer m_freezePointer; bool m_move_started = false; public: int m_move_amount; static void trackingDelta( int x, int y, unsigned int state, void* data ){ ModelBrowser& modelBrowser = *reinterpret_cast( data ); modelBrowser.m_move_amount += std::abs( x ) + std::abs( y ); if ( ( state & GDK_BUTTON3_MASK ) && y != 0 ) { // scroll view const int scale = ( state & GDK_SHIFT_MASK )? 4 : 1; modelBrowser.setOriginZ( modelBrowser.m_originZ + y * scale ); } else if ( ( state & GDK_BUTTON1_MASK ) && ( x != 0 || y != 0 ) && modelBrowser.m_currentModelId >= 0 ) { // rotate selected model ASSERT_MESSAGE( modelBrowser.m_currentModelId < static_cast( modelBrowser.m_modelInstances.size() ), "modelBrowser.m_currentModelId out of range" ); scene::Instance *instance = modelBrowser.m_modelInstances[modelBrowser.m_currentModelId]; if( TransformNode *transformNode = Node_getTransformNode( instance->path().parent() ) ){ Matrix4 rot( g_matrix4_identity ); matrix4_pivoted_rotate_by_euler_xyz_degrees( rot, Vector3( y, 0, x ) * ( 45.f / modelBrowser.m_cellSize ), modelBrowser.constructCellPos().getOrigin( modelBrowser.m_currentModelId ) ); matrix4_premultiply_by_matrix4( const_cast( transformNode->localToParent() ), rot ); instance->parent()->transformChangedLocal(); instance->transformChangedLocal(); modelBrowser.queueDraw(); } } } void tracking_MouseUp(){ if( m_move_started ){ m_move_started = false; m_freezePointer.unfreeze_pointer( false ); } } void tracking_MouseDown(){ tracking_MouseUp(); m_move_started = true; m_move_amount = 0; m_freezePointer.freeze_pointer( m_parent, m_gl_widget, trackingDelta, this ); } void insert( scene::Instance* instance ) override { if( instance->path().size() == 3 ) m_modelInstances.push_back( instance ); } void erase( scene::Instance* instance ) override { // just invalidate everything (also happens on resource flush and refresh) //FIXME: redraw on resource refresh m_modelInstances.clear(); m_currentFolder = nullptr; m_originZ = 0; m_originInvalid = true; } template void forEachModelInstance( const Functor& functor ) const { for( scene::Instance* instance : m_modelInstances ) functor( instance ); } }; ModelBrowser g_ModelBrowser; class models_set_transforms { mutable CellPos m_cellPos = g_ModelBrowser.constructCellPos(); public: void operator()( scene::Instance* instance ) const { if( TransformNode *transformNode = Node_getTransformNode( instance->path().parent() ) ){ if( Bounded *bounded = Instance_getBounded( *instance ) ){ AABB aabb = bounded->localAABB(); const float scale = m_cellPos.getCellSize() / aabb.extents[ vector3_max_abs_component_index( aabb.extents ) ]; aabb.extents.z() *= 2; // prioritize Z for orientation const Matrix4 rotation = matrix4_rotation_for_euler_xyz_degrees( vector3_min_abs_component_index( aabb.extents ) == 0? Vector3( 0, 0, -90 ) : vector3_min_abs_component_index( aabb.extents ) == 2? Vector3( 90, 0, 0 ) : g_vector3_identity ); const_cast( transformNode->localToParent() ) = matrix4_multiplied_by_matrix4( matrix4_translation_for_vec3( m_cellPos.getOrigin() ), matrix4_multiplied_by_matrix4( rotation, matrix4_multiplied_by_matrix4( matrix4_scale_for_vec3( Vector3( scale, scale, scale ) ), matrix4_translation_for_vec3( -aabb.origin ) ) ) ); instance->parent()->transformChangedLocal(); instance->transformChangedLocal(); //% globalOutputStream() << transformNode->localToParent() << " transformNode->localToParent()\n"; ++m_cellPos; } } } }; class ModelRenderer : public Renderer { struct state_type { state_type() : m_state( 0 ){ } Shader* m_state; }; public: ModelRenderer( RenderStateFlags globalstate ) : m_globalstate( globalstate ){ m_state_stack.push_back( state_type() ); } void SetState( Shader* state, EStyle style ){ ASSERT_NOTNULL( state ); if ( style == eFullMaterials ) { m_state_stack.back().m_state = state; } } EStyle getStyle() const { return eFullMaterials; } void PushState(){ m_state_stack.push_back( m_state_stack.back() ); } void PopState(){ ASSERT_MESSAGE( !m_state_stack.empty(), "popping empty stack" ); m_state_stack.pop_back(); } void Highlight( EHighlightMode mode, bool bEnable = true ){ } void addRenderable( const OpenGLRenderable& renderable, const Matrix4& localToWorld ){ m_state_stack.back().m_state->addRenderable( renderable, localToWorld ); } void render( const Matrix4& modelview, const Matrix4& projection ){ GlobalShaderCache().render( m_globalstate, modelview, projection ); } private: std::vector m_state_stack; RenderStateFlags m_globalstate; }; /* x=0, z<=0 origin -----> +x | -- | | | | -- \/ -z */ void ModelBrowser_render(){ g_ModelBrowser.validate(); g_ModelBrowser.fbo_get()->start(); const int W = g_ModelBrowser.m_width; const int H = g_ModelBrowser.m_height; glViewport( 0, 0, W, H ); // enable depth buffer writes glDepthMask( GL_TRUE ); glPolygonMode( GL_FRONT_AND_BACK, GL_FILL ); glClearColor( .25f, .25f, .25f, 0 ); glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); const unsigned int globalstate = RENDER_DEPTHTEST | RENDER_COLOURWRITE | RENDER_DEPTHWRITE | RENDER_ALPHATEST | RENDER_BLEND | RENDER_CULLFACE | RENDER_COLOURARRAY | RENDER_POLYGONSMOOTH | RENDER_LINESMOOTH | RENDER_FOG | RENDER_COLOURCHANGE | RENDER_FILL | RENDER_LIGHTING | RENDER_TEXTURE | RENDER_SMOOTH | RENDER_SCALED; Matrix4 m_projection; m_projection[0] = 1.0f / static_cast( W / 2.f ); m_projection[5] = 1.0f / static_cast( H / 2.f ); m_projection[10] = 1.0f / ( 9999 ); m_projection[12] = 0.0f; m_projection[13] = 0.0f; m_projection[14] = -1.0f; m_projection[1] = m_projection[2] = m_projection[3] = m_projection[4] = m_projection[6] = m_projection[7] = m_projection[8] = m_projection[9] = m_projection[11] = 0.0f; m_projection[15] = 1.0f; Matrix4 m_modelview; // translation m_modelview[12] = -W / 2.f; m_modelview[13] = H / 2.f - g_ModelBrowser.m_originZ; m_modelview[14] = 9999; // axis base //XZ: m_modelview[0] = 1; m_modelview[1] = 0; m_modelview[2] = 0; m_modelview[4] = 0; m_modelview[5] = 0; m_modelview[6] = 1; m_modelview[8] = 0; m_modelview[9] = 1; m_modelview[10] = 0; m_modelview[3] = m_modelview[7] = m_modelview[11] = 0; m_modelview[15] = 1; View m_view( true ); m_view.Construct( m_projection, m_modelview, W, H ); glMatrixMode( GL_PROJECTION ); glLoadMatrixf( reinterpret_cast( &m_projection ) ); glMatrixMode( GL_MODELVIEW ); glLoadMatrixf( reinterpret_cast( &m_modelview ) ); if( g_ModelBrowser.m_currentFolder != nullptr ){ { // prepare for 2d stuff glDisable( GL_BLEND ); if ( GlobalOpenGL().GL_1_3() ) { glClientActiveTexture( GL_TEXTURE0 ); glActiveTexture( GL_TEXTURE0 ); } glDisableClientState( GL_TEXTURE_COORD_ARRAY ); glDisableClientState( GL_NORMAL_ARRAY ); glDisableClientState( GL_COLOR_ARRAY ); glDisable( GL_TEXTURE_2D ); glDisable( GL_LIGHTING ); glDisable( GL_COLOR_MATERIAL ); glDisable( GL_DEPTH_TEST ); } { // brighter background squares glColor4f( 0.3f, 0.3f, 0.3f, 1.f ); glDepthMask( GL_FALSE ); glPolygonMode( GL_FRONT_AND_BACK, GL_FILL ); glDisable( GL_CULL_FACE ); CellPos cellPos = g_ModelBrowser.constructCellPos(); glBegin( GL_QUADS ); for( std::size_t i = g_ModelBrowser.m_currentFolder->m_files.size(); i != 0; --i ){ const Vector3 origin = cellPos.getOrigin(); const float minx = origin.x() - cellPos.getCellSize(); const float maxx = origin.x() + cellPos.getCellSize(); const float minz = origin.z() - cellPos.getCellSize(); const float maxz = origin.z() + cellPos.getCellSize(); glVertex3f( minx, 0, maxz ); glVertex3f( minx, 0, minz ); glVertex3f( maxx, 0, minz ); glVertex3f( maxx, 0, maxz ); ++cellPos; } glEnd(); } // one directional light source directly behind the viewer { GLfloat inverse_cam_dir[4], ambient[4], diffuse[4]; ambient[0] = ambient[1] = ambient[2] = 0.4f; ambient[3] = 1.0f; diffuse[0] = diffuse[1] = diffuse[2] = 0.4f; diffuse[3] = 1.0f; inverse_cam_dir[0] = -m_view.getViewDir()[0]; inverse_cam_dir[1] = -m_view.getViewDir()[1]; inverse_cam_dir[2] = -m_view.getViewDir()[2]; inverse_cam_dir[3] = 0; glLightfv( GL_LIGHT0, GL_POSITION, inverse_cam_dir ); glLightfv( GL_LIGHT0, GL_AMBIENT, ambient ); glLightfv( GL_LIGHT0, GL_DIFFUSE, diffuse ); glEnable( GL_LIGHT0 ); } { ModelRenderer renderer( globalstate ); g_ModelBrowser.forEachModelInstance( [&renderer, &m_view]( scene::Instance* instance ){ if( Renderable *renderable = Instance_getRenderable( *instance ) ) renderable->renderSolid( renderer, m_view ); } ); renderer.render( m_modelview, m_projection ); } { // prepare for 2d stuff glColor4f( 1, 1, 1, 1 ); glDisable( GL_BLEND ); if ( GlobalOpenGL().GL_1_3() ) { glClientActiveTexture( GL_TEXTURE0 ); glActiveTexture( GL_TEXTURE0 ); } glDisableClientState( GL_TEXTURE_COORD_ARRAY ); glDisableClientState( GL_NORMAL_ARRAY ); glDisableClientState( GL_COLOR_ARRAY ); glDisable( GL_TEXTURE_2D ); glDisable( GL_LIGHTING ); glDisable( GL_COLOR_MATERIAL ); glDisable( GL_DEPTH_TEST ); glLineWidth( 1 ); } { // render model file names CellPos cellPos = g_ModelBrowser.constructCellPos(); for( const CopiedString& string : g_ModelBrowser.m_currentFolder->m_files ){ const Vector3 pos = cellPos.getTextPos(); if( m_view.TestPoint( pos ) ){ glRasterPos3f( pos.x(), pos.y(), pos.z() ); GlobalOpenGL().drawString( string.c_str() ); } ++cellPos; } } } // bind back to the default texture so that we don't have problems // elsewhere using/modifying texture maps between contexts glBindTexture( GL_TEXTURE_2D, 0 ); g_ModelBrowser.fbo_get()->save(); } gboolean ModelBrowser_size_allocate( GtkWidget* widget, GtkAllocation* allocation, ModelBrowser* modelBrowser ){ modelBrowser->fbo_get()->reset( allocation->width, allocation->height, modelBrowser->m_MSAA, true ); modelBrowser->m_width = allocation->width; modelBrowser->m_height = allocation->height; modelBrowser->m_originInvalid = true; modelBrowser->forEachModelInstance( models_set_transforms() ); modelBrowser->queueDraw(); return FALSE; } gboolean ModelBrowser_expose( GtkWidget* widget, GdkEventExpose* event, ModelBrowser* modelBrowser ){ if ( glwidget_make_current( modelBrowser->m_gl_widget ) ) { GlobalOpenGL_debugAssertNoErrors(); ModelBrowser_render(); GlobalOpenGL_debugAssertNoErrors(); glwidget_swap_buffers( modelBrowser->m_gl_widget ); } return FALSE; } gboolean ModelBrowser_mouseScroll( GtkWidget* widget, GdkEventScroll* event, ModelBrowser* modelBrowser ){ gtk_widget_grab_focus( widget ); if( !gtk_window_is_active( modelBrowser->m_parent ) ) gtk_window_present( modelBrowser->m_parent ); if ( event->direction == GDK_SCROLL_UP ) { modelBrowser->setOriginZ( modelBrowser->m_originZ + 64 ); } else if ( event->direction == GDK_SCROLL_DOWN ) { modelBrowser->setOriginZ( modelBrowser->m_originZ - 64 ); } return FALSE; } void ModelBrowser_scrollChanged( void* data, gdouble value ){ //globalOutputStream() << "vertical scroll\n"; reinterpret_cast( data )->setOriginZ( -(int)value ); } static void ModelBrowser_scrollbarScroll( GtkAdjustment *adjustment, ModelBrowser* modelBrowser ){ modelBrowser->m_scrollAdjustment.value_changed( gtk_adjustment_get_value( adjustment ) ); } gboolean ModelBrowser_button_press( GtkWidget* widget, GdkEventButton* event, ModelBrowser* modelBrowser ){ if ( event->type == GDK_BUTTON_PRESS ) { gtk_widget_grab_focus( widget ); if ( event->button == 1 || event->button == 3 ) { modelBrowser->tracking_MouseDown(); } if ( event->button == 1 ) { modelBrowser->testSelect( static_cast( event->x ), static_cast( event->y ) ); } } /* create misc_model */ else if ( event->type == GDK_2BUTTON_PRESS && event->button == 1 && modelBrowser->m_currentFolder != nullptr && modelBrowser->m_currentModelId >= 0 ) { UndoableCommand undo( "insertModel" ); // todo // GlobalEntityClassManager() seach for "misc_model" // otherwise search for entityClass->miscmodel_is // otherwise go with GlobalEntityClassManager().findOrInsert( "misc_model", false ); EntityClass* entityClass = GlobalEntityClassManager().findOrInsert( "misc_model", false ); NodeSmartReference node( GlobalEntityCreator().createEntity( entityClass ) ); Node_getTraversable( GlobalSceneGraph().root() )->insert( node ); scene::Path entitypath( makeReference( GlobalSceneGraph().root() ) ); entitypath.push( makeReference( node.get() ) ); scene::Instance& instance = findInstance( entitypath ); if ( Transformable* transform = Instance_getTransformable( instance ) ) { // might be cool to consider model aabb here transform->setType( TRANSFORM_PRIMITIVE ); transform->setTranslation( vector3_snapped( Camera_getOrigin( *g_pParentWnd->GetCamWnd() ) - Camera_getViewVector( *g_pParentWnd->GetCamWnd() ) * 128.f, GetSnapGridSize() ) ); transform->freezeTransform(); } GlobalSelectionSystem().setSelectedAll( false ); Instance_setSelected( instance, true ); StringOutputStream sstream( 128 ); sstream << modelBrowser->m_currentFolderPath.c_str() << std::next( modelBrowser->m_currentFolder->m_files.begin(), modelBrowser->m_currentModelId )->c_str(); Node_getEntity( node )->setKeyValue( entityClass->miscmodel_key(), sstream.c_str() ); } return FALSE; } gboolean ModelBrowser_button_release( GtkWidget* widget, GdkEventButton* event, ModelBrowser* modelBrowser ){ if ( event->type == GDK_BUTTON_RELEASE ) { if ( event->button == 1 || event->button == 3 ) { modelBrowser->tracking_MouseUp(); } if ( event->button == 1 && modelBrowser->m_move_amount < 16 && modelBrowser->m_currentFolder != nullptr && modelBrowser->m_currentModelId >= 0 ) { // assign model to selected entity nodes StringOutputStream sstream( 128 ); sstream << modelBrowser->m_currentFolderPath.c_str() << std::next( modelBrowser->m_currentFolder->m_files.begin(), modelBrowser->m_currentModelId )->c_str(); class EntityVisitor : public SelectionSystem::Visitor { const char* m_filePath; public: EntityVisitor( const char* filePath ) : m_filePath( filePath ){ } void visit( scene::Instance& instance ) const override { if( Entity* entity = Node_getEntity( instance.path().top() ) ){ entity->setKeyValue( entity->getEntityClass().miscmodel_key(), m_filePath ); } } } visitor( sstream.c_str() ); UndoableCommand undo( "entityAssignModel" ); GlobalSelectionSystem().foreachSelected( visitor ); } } return FALSE; } static void TreeView_onRowActivated( GtkTreeView* treeview, GtkTreePath* path, GtkTreeViewColumn* col, gpointer userdata ){ GtkTreeModel* model = gtk_tree_view_get_model( GTK_TREE_VIEW( treeview ) ); GtkTreeIter iter; if ( gtk_tree_model_get_iter( model, &iter, path ) ) { std::deque iters; iters.push_front( iter ); GtkTreeIter parent; while( gtk_tree_model_iter_parent( model, &parent, &iters.front() ) ) iters.push_front( parent ); StringOutputStream sstream( 64 ); const ModelFS *modelFS = &g_ModelBrowser.m_modelFS; for( GtkTreeIter& i : iters ){ gchar* buffer; gtk_tree_model_get( model, &i, 0, &buffer, -1 ); const auto found = modelFS->m_folders.find( ModelFS( StringRange( buffer, buffer + strlen( buffer ) ) ) ); if( found != modelFS->m_folders.end() ){ // ok to not find, while loading root modelFS = &( *found ); sstream << buffer << "/"; } g_free( buffer ); } //% globalOutputStream() << sstream.c_str() << " sstream.c_str()\n"; ModelGraph_clear(); // this goes 1st: resets m_currentFolder g_ModelBrowser.m_currentFolder = modelFS; g_ModelBrowser.m_currentFolderPath = sstream.c_str(); ScopeDisableScreenUpdates disableScreenUpdates( g_ModelBrowser.m_currentFolderPath.c_str(), "Loading Models" ); { for( const CopiedString& filename : g_ModelBrowser.m_currentFolder->m_files ){ sstream.clear(); sstream << g_ModelBrowser.m_currentFolderPath.c_str() << filename.c_str(); ModelNode *modelNode = new ModelNode; modelNode->setModel( sstream.c_str() ); NodeSmartReference node( modelNode->node() ); Node_getTraversable( g_modelGraph->root() )->insert( node ); } g_ModelBrowser.forEachModelInstance( models_set_transforms() ); } g_ModelBrowser.queueDraw(); //deactivate, so SPACE and RETURN wont be broken for 2d gtk_window_set_focus( GTK_WINDOW( gtk_widget_get_toplevel( GTK_WIDGET( treeview ) ) ), NULL ); } } #if 0 void modelFS_traverse( const ModelFS& modelFS ){ static int depth = -1; ++depth; for( int i = 0; i < depth; ++i ){ globalOutputStream() << "\t"; } globalOutputStream() << modelFS.m_folderName.c_str() << "\n"; for( const ModelFS& m : modelFS.m_folders ) modelFS_traverse( m ); --depth; } #endif void ModelBrowser_constructTreeModel( const ModelFS& modelFS, GtkTreeStore* store, GtkTreeIter* parent ){ GtkTreeIter iter; gtk_tree_store_append( store, &iter, parent ); gtk_tree_store_set( store, &iter, 0, modelFS.m_folderName.c_str(), -1 ); for( const ModelFS& m : modelFS.m_folders ) ModelBrowser_constructTreeModel( m, store, &iter ); //recursion } typedef std::map ModelFoldersMap; class ModelFolders { public: ModelFoldersMap m_modelFoldersMap; // parse string of format *pathToLoad/depth*path2ToLoad/depth* // */depth* for root path ModelFolders( const char* pathsString ){ StringOutputStream str( 128 ); str << PathCleaned( pathsString ); const char* start = str.c_str(); while( 1 ){ while( *start == '*' ) ++start; const char* end = start; while( *end && *end != '*' ) ++end; if( start == end ) break; const char* slash = nullptr; for( const char* s = start; s != end; ++s ) if( *s == '/' ) slash = s; if( slash != nullptr && end - slash > 1 ){ std::size_t depth; Size_importString( depth, CopiedString( StringRange( slash + 1, end ) ).c_str() ); StringRange folder( start, ( start == slash )? slash : slash + 1 ); m_modelFoldersMap.emplace( folder, depth ); } start = end; } if( m_modelFoldersMap.empty() ) m_modelFoldersMap.emplace( "models/", 99 ); } }; typedef std::set StringSetWithLambda; class ModelPaths_ArchiveVisitor : public Archive::Visitor { const StringSetWithLambda& m_modelExtensions; ModelFS& m_modelFS; public: const ModelFoldersMap& m_modelFoldersMap; bool m_avoid_pk3dir; ModelPaths_ArchiveVisitor( const StringSetWithLambda& modelExtensions, ModelFS& modelFS, const ModelFoldersMap& modelFoldersMap ) : m_modelExtensions( modelExtensions ), m_modelFS( modelFS ), m_modelFoldersMap( modelFoldersMap ){ } void visit( const char* name ) override { if( m_modelExtensions.find( path_get_extension( name ) ) != m_modelExtensions.end() && ( !m_avoid_pk3dir || !string_in_string_nocase( name, ".pk3dir/" ) ) ){ m_modelFS.insert( name ); //% globalOutputStream() << name << " name\n"; } } }; void ModelPaths_addFromArchive( ModelPaths_ArchiveVisitor& visitor, const char *archiveName ){ //% globalOutputStream() << "\t\t" << archiveName << " archiveName\n"; Archive *archive = GlobalFileSystem().getArchive( archiveName, false ); if ( archive != nullptr ) { for( const auto& folder : visitor.m_modelFoldersMap ){ /* should better avoid .pk3dir traversal right in archive implementation for normal folders */ visitor.m_avoid_pk3dir = string_empty( folder.first.c_str() ) // root && folder.second > 1 // deep nuff && string_equal_suffix( archiveName, "/" ) // normal folder, not archive && !string_equal_suffix_nocase( archiveName, ".pk3dir/" ); // not .pk3dir archive->forEachFile( Archive::VisitorFunc( visitor, Archive::eFiles, folder.second ), folder.first.c_str() ); } } } typedef ReferenceCaller1 ModelPaths_addFromArchiveCaller; void ModelBrowser_constructTree(){ g_ModelBrowser.m_modelFS.m_folders.clear(); g_ModelBrowser.m_modelFS.m_files.clear(); ModelGraph_clear(); g_ModelBrowser.queueDraw(); class : public IFileTypeList { public: StringSetWithLambda m_modelExtensions{ []( const CopiedString& lhs, const CopiedString& rhs )->bool{ return string_less_nocase( lhs.c_str(), rhs.c_str() ); } }; void addType( const char* moduleName, filetype_t type ) override { m_modelExtensions.emplace( moduleName ); } } typelist; GlobalFiletypes().getTypeList( ModelLoader::Name(), &typelist, true, false, false ); ModelFolders modelFolders( g_ModelBrowser.m_prefFoldersToLoad.c_str() ); ModelPaths_ArchiveVisitor visitor( typelist.m_modelExtensions, g_ModelBrowser.m_modelFS, modelFolders.m_modelFoldersMap ); GlobalFileSystem().forEachArchive( ModelPaths_addFromArchiveCaller( visitor ), false, false ); //% modelFS_traverse( g_ModelBrowser.m_modelFS ); GtkTreeStore* store = gtk_tree_store_new( 1, G_TYPE_STRING ); { if( !g_ModelBrowser.m_modelFS.m_files.empty() ){ // models in the root GtkTreeIter iter; gtk_tree_store_append( store, &iter, nullptr ); gtk_tree_store_set( store, &iter, 0, "", -1 ); } for( const ModelFS& m : g_ModelBrowser.m_modelFS.m_folders ) ModelBrowser_constructTreeModel( m, store, nullptr ); } gtk_tree_view_set_model( GTK_TREE_VIEW( g_ModelBrowser.m_treeViewTree ), GTK_TREE_MODEL( store ) ); g_object_unref( G_OBJECT( store ) ); } GtkWidget* ModelBrowser_constructWindow( GtkWindow* toplevel ){ g_ModelBrowser.m_parent = toplevel; GtkWidget* table = gtk_table_new( 1, 3, FALSE ); GtkWidget* vbox = gtk_vbox_new( FALSE, 0 ); gtk_table_attach( GTK_TABLE( table ), vbox, 0, 1, 0, 1, GTK_FILL, GTK_FILL, 0, 0 ); gtk_widget_show( vbox ); { // menu bar GtkToolbar* toolbar = GTK_TOOLBAR( gtk_toolbar_new() ); gtk_box_pack_start( GTK_BOX( vbox ), GTK_WIDGET( toolbar ), FALSE, FALSE, 0 ); GtkButton* button = toolbar_append_button( toolbar, "Reload Model Folders Tree View", "texbro_refresh.png", FreeCaller() ); gtk_widget_set_size_request( GTK_WIDGET( button ), 22, 22 ); gtk_widget_show( GTK_WIDGET( toolbar ) ); } { // TreeView GtkWidget* scr = gtk_scrolled_window_new( NULL, NULL ); gtk_container_set_border_width( GTK_CONTAINER( scr ), 0 ); // vertical only scrolling for treeview gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW( scr ), GTK_POLICY_NEVER, GTK_POLICY_ALWAYS ); gtk_widget_show( scr ); g_ModelBrowser.m_treeViewTree = gtk_tree_view_new(); GtkTreeView* treeview = GTK_TREE_VIEW( g_ModelBrowser.m_treeViewTree ); //gtk_tree_view_set_enable_search( treeview, FALSE ); gtk_tree_view_set_headers_visible( treeview, FALSE ); g_signal_connect( treeview, "row-activated", (GCallback) TreeView_onRowActivated, NULL ); GtkCellRenderer* renderer = gtk_cell_renderer_text_new(); //g_object_set( G_OBJECT( renderer ), "ellipsize", PANGO_ELLIPSIZE_START, NULL ); gtk_tree_view_insert_column_with_attributes( treeview, -1, "", renderer, "text", 0, NULL ); ModelBrowser_constructTree(); //gtk_scrolled_window_add_with_viewport( GTK_SCROLLED_WINDOW( scr ), g_ModelBrowser.m_treeViewTree ); gtk_container_add( GTK_CONTAINER( scr ), g_ModelBrowser.m_treeViewTree ); //GtkTreeView has native scrolling support; should not be used with the GtkViewport proxy. gtk_widget_show( g_ModelBrowser.m_treeViewTree ); gtk_box_pack_start( GTK_BOX( vbox ), scr, TRUE, TRUE, 0 ); } { // gl_widget scrollbar GtkWidget* w = g_ModelBrowser.m_gl_scroll = gtk_vscrollbar_new( GTK_ADJUSTMENT( gtk_adjustment_new( 0, 0, 0, 1, 1, 0 ) ) ); gtk_table_attach( GTK_TABLE( table ), w, 2, 3, 0, 1, GTK_SHRINK, GTK_FILL, 0, 0 ); gtk_widget_show( w ); GtkAdjustment *vadjustment = gtk_range_get_adjustment( GTK_RANGE( w ) ); g_signal_connect( G_OBJECT( vadjustment ), "value_changed", G_CALLBACK( ModelBrowser_scrollbarScroll ), &g_ModelBrowser ); } { // gl_widget GtkWidget* w = g_ModelBrowser.m_gl_widget = glwidget_new( TRUE ); g_object_ref( G_OBJECT( w ) ); gtk_widget_set_events( w, GDK_DESTROY | GDK_EXPOSURE_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK | GDK_SCROLL_MASK ); gtk_widget_set_can_focus( w, TRUE ); gtk_table_attach_defaults( GTK_TABLE( table ), w, 1, 2, 0, 1 ); gtk_widget_show( w ); g_ModelBrowser.m_sizeHandler = g_signal_connect( G_OBJECT( w ), "size_allocate", G_CALLBACK( ModelBrowser_size_allocate ), &g_ModelBrowser ); g_ModelBrowser.m_exposeHandler = g_signal_connect( G_OBJECT( w ), "expose_event", G_CALLBACK( ModelBrowser_expose ), &g_ModelBrowser ); g_signal_connect( G_OBJECT( w ), "button_press_event", G_CALLBACK( ModelBrowser_button_press ), &g_ModelBrowser ); g_signal_connect( G_OBJECT( w ), "button_release_event", G_CALLBACK( ModelBrowser_button_release ), &g_ModelBrowser ); g_signal_connect( G_OBJECT( w ), "scroll_event", G_CALLBACK( ModelBrowser_mouseScroll ), &g_ModelBrowser ); } //prevent focusing on filter entry or tex dirs treeview after click on tab of floating group dialog (np, if called via hotkey) gtk_container_set_focus_chain( GTK_CONTAINER( table ), NULL ); return table; } void ModelBrowser_destroyWindow(){ g_signal_handler_disconnect( G_OBJECT( g_ModelBrowser.m_gl_widget ), g_ModelBrowser.m_sizeHandler ); g_signal_handler_disconnect( G_OBJECT( g_ModelBrowser.m_gl_widget ), g_ModelBrowser.m_exposeHandler ); g_object_unref( G_OBJECT( g_ModelBrowser.m_gl_widget ) ); g_ModelBrowser.m_gl_widget = nullptr; delete g_ModelBrowser.m_fbo; } #include "preferencesystem.h" #include "preferences.h" #include "stringio.h" void CellSizeImport( int& oldvalue, int value ){ if( oldvalue != value ){ oldvalue = value; g_ModelBrowser.forEachModelInstance( models_set_transforms() ); g_ModelBrowser.m_originInvalid = true; g_ModelBrowser.queueDraw(); } } typedef ReferenceCaller1 CellSizeImportCaller; void FoldersToLoadImport( CopiedString& self, const char* value ){ if( self != value ){ self = value; ModelBrowser_constructTree(); } } typedef ReferenceCaller1 FoldersToLoadImportCaller; void ModelBrowser_constructPage( PreferenceGroup& group ){ PreferencesPage page( group.createPage( "Model Browser", "Model Browser Preferences" ) ); page.appendSpinner( "Model View Size", 80.0, 16.0, 8192.0, IntImportCallback( CellSizeImportCaller( g_ModelBrowser.m_cellSize ) ), IntExportCallback( IntExportCaller( g_ModelBrowser.m_cellSize ) ) ); page.appendEntry( "List of *folderToLoad/depth*", StringImportCallback( FoldersToLoadImportCaller( g_ModelBrowser.m_prefFoldersToLoad ) ), StringExportCallback( StringExportCaller( g_ModelBrowser.m_prefFoldersToLoad ) ) ); } void ModelBrowser_registerPreferencesPage(){ PreferencesDialog_addSettingsPage( FreeCaller1() ); } void ModelBrowser_Construct(){ GlobalPreferenceSystem().registerPreference( "ModelBrowserFolders", CopiedStringImportStringCaller( g_ModelBrowser.m_prefFoldersToLoad ), CopiedStringExportStringCaller( g_ModelBrowser.m_prefFoldersToLoad ) ); GlobalPreferenceSystem().registerPreference( "ModelBrowserCellSize", IntImportStringCaller( g_ModelBrowser.m_cellSize ), IntExportStringCaller( g_ModelBrowser.m_cellSize ) ); ModelBrowser_registerPreferencesPage(); g_modelGraph = new ModelGraph( g_ModelBrowser ); g_modelGraph->insert_root( ( new ModelGraphRoot )->node() ); } void ModelBrowser_Destroy(){ g_modelGraph->erase_root(); delete g_modelGraph; } GtkWidget* ModelBrowser_getGLWidget(){ return g_ModelBrowser.m_gl_widget; } void ModelBrowser_flushReferences(){ ModelGraph_clear(); g_ModelBrowser.queueDraw(); }