Index: trunk/nv/gfx/mesh_creator.hh
===================================================================
--- trunk/nv/gfx/mesh_creator.hh	(revision 294)
+++ trunk/nv/gfx/mesh_creator.hh	(revision 295)
@@ -23,6 +23,10 @@
 		// TODO: this could generate normals too
 		void generate_tangents();
+		void flip_normals();
+		bool is_same_format( mesh_data* other );
+		void merge( mesh_data* other );
 	private:
 		mesh_raw_channel* merge_channels( mesh_raw_channel* a, mesh_raw_channel* b );
+		mesh_raw_channel* append_channels( mesh_raw_channel* a, mesh_raw_channel* b, uint32 frame_count = 1 );
 
 		mesh_data* m_data;
@@ -47,5 +51,9 @@
 		mesh_creator( mesh_data_pack* pack ) : m_pack( pack ) {}
 		// assumes that keys are equally spaced
-		void pre_transform_keys() { mesh_nodes_creator( m_pack->m_nodes ).pre_transform_keys(); }
+		void pre_transform_keys() 
+		{
+			if ( m_pack->m_nodes )
+				mesh_nodes_creator( m_pack->m_nodes ).pre_transform_keys();
+		}
 		void generate_tangents()
 		{
@@ -57,4 +65,9 @@
 		void merge_keys() { mesh_nodes_creator( m_pack->m_nodes ).merge_keys(); }
 		// assumes that position and normal is vec3, tangent is vec4
+		void flip_normals() 
+		{
+			for ( size_t m = 0; m < m_pack->m_count; ++m )
+				mesh_data_creator( &(m_pack->m_meshes[m]) ).flip_normals();
+		}
 		void transform( float scale, const mat3& r33 ) 
 		{
@@ -64,4 +77,19 @@
 				mesh_nodes_creator( m_pack->m_nodes ).transform( scale, r33 );
 		}
+		// assumes models have same format
+		// currently only data merge
+		bool merge( mesh_data_pack* other )
+		{
+			for ( size_t m = 0; m < m_pack->m_count; ++m )
+			{
+				if (!mesh_data_creator( &(m_pack->m_meshes[m]) ).is_same_format( &(other->m_meshes[m]) ) ) 
+					return false;
+			}
+			for ( size_t m = 0; m < m_pack->m_count; ++m )
+			{
+				mesh_data_creator(&(m_pack->m_meshes[m]) ).merge( &(other->m_meshes[m]) );
+			}
+			return true;
+		}
 	private:
 		mesh_data_pack* m_pack;
Index: trunk/nv/gl/gl_context.hh
===================================================================
--- trunk/nv/gl/gl_context.hh	(revision 294)
+++ trunk/nv/gl/gl_context.hh	(revision 295)
@@ -64,4 +64,5 @@
 	public:
 		native_gl_context( device* a_device, void* a_native_win_handle );
+		void* get_native_handle() { return m_handle; }
 		~native_gl_context();
 	private:
Index: trunk/nv/gl/gl_window.hh
===================================================================
--- trunk/nv/gl/gl_window.hh	(revision 294)
+++ trunk/nv/gl/gl_window.hh	(revision 295)
@@ -32,5 +32,4 @@
 		virtual context* get_context() { return m_context; }
 		virtual device* get_device() { return m_device; }
-
 		virtual void swap_buffers();
 		virtual ~gl_window();
@@ -41,4 +40,5 @@
 		string      m_title;
 		void*       m_handle;
+		void*       m_hwnd;
 		bool        m_adopted;
 		gl_context* m_context;
Index: trunk/nv/handle.hh
===================================================================
--- trunk/nv/handle.hh	(revision 294)
+++ trunk/nv/handle.hh	(revision 295)
@@ -147,4 +147,5 @@
 			resize_indexes_to( h.index() );
 			m_indexes[ h.index() ] = m_data.size();
+			m_handles.push_back( h );
 			m_data.emplace_back();
 			return &(m_data.back());
@@ -170,7 +171,7 @@
 			if ( dead_eindex != (sint32)m_data.size()-1 )
 			{
-				m_data[ dead_eindex ]    = m_data.back();
-				m_handles[ dead_eindex ] = m_handles.back();
-				m_indexes[ h.index() ]   = m_indexes[ swap_handle.index() ];
+				m_data[ dead_eindex ]            = m_data.back();
+				m_handles[ dead_eindex ]         = swap_handle;
+				m_indexes[ swap_handle.index() ] = dead_eindex;
 			}
 			m_data.pop_back();
Index: trunk/nv/interface/animated_mesh.hh
===================================================================
--- trunk/nv/interface/animated_mesh.hh	(revision 294)
+++ trunk/nv/interface/animated_mesh.hh	(revision 295)
@@ -33,4 +33,14 @@
 		float get_end() const  { return m_end; }
 		bool is_looping() const { return m_looping; }
+		void set_range( float a_start, float a_end )
+		{
+			m_start = a_start;
+			m_end   = a_end;
+			m_duration = m_end - m_start;
+		}
+		void set_frame_rate( uint32 rate )
+		{
+			m_frame_rate = rate;
+		}
 		virtual ~animation_entry() {}
 	protected:
Index: trunk/nv/interface/mesh_data.hh
===================================================================
--- trunk/nv/interface/mesh_data.hh	(revision 294)
+++ trunk/nv/interface/mesh_data.hh	(revision 295)
@@ -107,4 +107,32 @@
 			return nullptr;
 		}
+
+		const mesh_raw_channel* get_channel( slot s ) const 
+		{ 
+			for ( auto ch : m_channels )
+			{
+				for ( uint32 i = 0; i < ch->desc.count; ++i )
+					if ( ch->desc.slots[i].vslot == s )
+					{
+						return ch;
+					}
+			}
+			return nullptr;
+		}
+
+		int get_channel_index( slot s ) const 
+		{ 
+			for ( uint32 c = 0; c < m_channels.size(); ++c )
+			{
+				const mesh_raw_channel* ch = m_channels[c];
+				for ( uint32 i = 0; i < ch->desc.count; ++i )
+					if ( ch->desc.slots[i].vslot == s )
+					{
+						return c;
+					}
+			}
+			return -1;
+		}
+
 
 		size_t get_count() const 
Index: trunk/nv/interface/vertex.hh
===================================================================
--- trunk/nv/interface/vertex.hh	(revision 294)
+++ trunk/nv/interface/vertex.hh	(revision 295)
@@ -261,5 +261,10 @@
 		}
 
-		bool operator==( const vertex_descriptor& rhs )
+		bool operator!=( const vertex_descriptor& rhs ) const
+		{
+			return !( *this == rhs );
+		}
+
+		bool operator==( const vertex_descriptor& rhs ) const
 		{
 			if ( size  != rhs.size )  return false;
Index: trunk/nv/lib/detail/gl_types.inc
===================================================================
--- trunk/nv/lib/detail/gl_types.inc	(revision 294)
+++ trunk/nv/lib/detail/gl_types.inc	(revision 295)
@@ -1,2 +1,5 @@
+#ifndef NV_LIB_GL_TYPES_HH
+#define NV_LIB_GL_TYPES_HH
+
 typedef unsigned int GLenum;
 typedef unsigned int GLbitfield;
@@ -21,4 +24,5 @@
 
 /* OpenGL 1.1 non-deprecated defines */
+#define GL_NO_ERROR 0
 #define GL_DEPTH_BUFFER_BIT 0x00000100
 #define GL_STENCIL_BUFFER_BIT 0x00000400
@@ -517,2 +521,4 @@
 #define GL_RENDERBUFFER_DEPTH_SIZE 0x8D54
 #define GL_RENDERBUFFER_STENCIL_SIZE 0x8D55
+
+#endif // NV_LIB_GL_TYPES_HH
Index: trunk/nv/lib/wx.hh
===================================================================
--- trunk/nv/lib/wx.hh	(revision 295)
+++ trunk/nv/lib/wx.hh	(revision 295)
@@ -0,0 +1,89 @@
+// Copyright (C) 2014 ChaosForge Ltd 
+// http://chaosforge.org/
+//
+// This file is part of NV Libraries.
+// For conditions of distribution and use, see copyright notice in nv.hh
+//
+// NOTE: this file is header only, not to have NV depend on WX
+#ifndef NV_LIB_WX_HH
+#define NV_LIB_WX_HH
+
+#if defined( NV_COMMON_HH ) || defined( _WX_WX_H_ )
+#error For proper side-by-side usage WX header needs to be included first!
+#endif
+
+#define __GL_H__
+#include <stddef.h>
+#include <nv/lib/detail/gl_types.inc>
+#include "wx/wx.h"
+#undef near
+#undef far
+#include <nv/common.hh>
+#include <nv/lib/gl.hh>
+#include <nv/gl/gl_device.hh>
+#include <nv/interface/context.hh>
+#include <nv/interface/window.hh>
+
+namespace nv
+{
+	class gl_canvas : public wxWindow
+	{
+	public:
+		explicit gl_canvas( wxWindow *parent );
+		virtual void OnUpdate() = 0;
+		virtual void Stop() { m_update_timer->Stop(); }
+		virtual void Start() { m_update_timer->Start(10); }
+		~gl_canvas();
+	protected:
+		void OnPaint( wxPaintEvent& event );
+
+		nv::device*  m_device;
+		nv::context* m_context;
+		nv::window*  m_window;
+		wxTimer*     m_update_timer;
+		wxDECLARE_EVENT_TABLE();
+	};
+
+	class gl_update_timer : public wxTimer
+	{
+	public:
+		explicit gl_update_timer( gl_canvas* canvas )
+			: wxTimer(), m_canvas( canvas ) {}
+		virtual void Notify() { m_canvas->OnUpdate(); }
+	private:
+		gl_canvas* m_canvas;
+	};
+
+// 	wxBEGIN_EVENT_TABLE(gl_canvas, wxWindow)
+// 		EVT_PAINT(gl_canvas::OnPaint)
+// 	wxEND_EVENT_TABLE()
+
+	inline gl_canvas::gl_canvas( wxWindow *parent )
+		: wxWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize,
+	wxFULL_REPAINT_ON_RESIZE)
+	{
+		nv::load_gl_no_context();
+		wxClientDC dc(this);
+		m_device = new nv::gl_device();
+		m_window = m_device->adopt_window( GetHWND(), dc.GetHDC() );
+		m_context = m_window->get_context();
+		m_update_timer = new gl_update_timer( this );
+	}
+
+	inline gl_canvas::~gl_canvas()
+	{
+		delete m_update_timer;
+		delete m_window;
+		delete m_device;
+	}
+
+	inline void gl_canvas::OnPaint( wxPaintEvent& )
+	{
+		const wxSize client_size = GetClientSize();
+		m_context->set_viewport( nv::ivec4( nv::ivec2(), client_size.x, client_size.y ) );
+		OnUpdate();
+	}
+
+}
+
+#endif NV_LIB_WX_HH
Index: trunk/nv/logger.hh
===================================================================
--- trunk/nv/logger.hh	(revision 294)
+++ trunk/nv/logger.hh	(revision 295)
@@ -41,4 +41,12 @@
 		const char* timestamp() const;
 		/**
+         * Log level name (unpadded)
+		 */
+		const char* level_name( log_level level ) const;
+		/**
+         * Log level name (padded)
+		 */
+		const char* padded_level_name( log_level level ) const;
+		/**
 		 * Enforcement of virtual destructor.
 		 */
Index: trunk/src/gfx/mesh_creator.cc
===================================================================
--- trunk/src/gfx/mesh_creator.cc	(revision 294)
+++ trunk/src/gfx/mesh_creator.cc	(revision 295)
@@ -168,4 +168,24 @@
 };
 
+void nv::mesh_data_creator::flip_normals()
+{
+	int ch_n  = m_data->get_channel_index( slot::NORMAL );
+	size_t n_offset = 0;
+	if ( ch_n == -1 ) return;
+	mesh_raw_channel* channel = m_data->m_channels[ch_n];
+	for ( uint32 i = 0; i < channel->desc.count; ++i )
+		if ( channel->desc.slots[i].vslot == slot::NORMAL )
+		{
+			n_offset  = channel->desc.slots[i].offset; 
+		}
+
+	for ( uint32 i = 0; i < channel->count; ++i )
+	{
+		vec3& normal = *(vec3*)(channel->data + channel->desc.size * i + n_offset);
+		normal = -normal;
+	}
+}
+
+
 void nv::mesh_data_creator::generate_tangents()
 {
@@ -339,2 +359,103 @@
 	return result;
 }
+
+nv::mesh_raw_channel* nv::mesh_data_creator::append_channels( mesh_raw_channel* a, mesh_raw_channel* b, uint32 frame_count )
+{
+	if ( a->desc != b->desc ) return nullptr;
+	if ( a->count % frame_count != 0 ) return nullptr;
+	if ( b->count % frame_count != 0 ) return nullptr;
+	size_t vtx_size = a->desc.size;
+
+	uint8* data = new uint8[ ( a->count + b->count ) * vtx_size ];
+	
+
+	if ( frame_count == 1 )
+	{
+		size_t a_size = vtx_size * a->count;
+		std::copy_n( a->data, a_size, data );
+		std::copy_n( b->data, vtx_size * b->count, data + a_size );		
+	}
+	else
+	{
+		size_t frame_size_a = ( a->count / frame_count ) * vtx_size;
+		size_t frame_size_b = ( b->count / frame_count ) * vtx_size;
+		size_t pos_a = 0;
+		size_t pos_b = 0;
+		size_t pos   = 0;
+		for ( size_t i = 0; i < frame_count; ++i )
+		{
+			std::copy_n( a->data + pos_a, frame_size_a, data + pos );
+			std::copy_n( b->data + pos_b, frame_size_b, data + pos + frame_size_a );				pos_a += frame_size_a; 
+			pos_b += frame_size_b; 
+			pos   += frame_size_a + frame_size_b;
+		}
+	}
+
+	mesh_raw_channel* result = new mesh_raw_channel;
+	result->count = a->count + b->count;
+	result->desc  = a->desc;
+	result->data  = data;
+	return result;
+}
+
+
+
+bool nv::mesh_data_creator::is_same_format( mesh_data* other )
+{
+	if ( m_data->get_channel_count() != other->get_channel_count() ) return false;
+	for ( uint32 c = 0; c < m_data->get_channel_count(); ++c )
+	{
+		if ( m_data->get_channel(c)->desc != other->get_channel(c)->desc )
+			return false;
+	}
+	return true;
+}
+
+void nv::mesh_data_creator::merge( mesh_data* other )
+{
+	if ( !is_same_format( other ) ) return;
+	int ch_pi  = m_data->get_channel_index( slot::POSITION );
+	int ch_ti  = m_data->get_channel_index( slot::TEXCOORD );
+	int och_pi = other->get_channel_index( slot::POSITION );
+	int och_ti = other->get_channel_index( slot::TEXCOORD );
+	if ( ch_pi == -1 || ch_ti == -1 ) return;
+	size_t size   = m_data->m_channels[ ch_ti ]->count;
+	size_t osize  =  other->m_channels[ och_ti ]->count;
+	size_t count  = m_data->m_channels[ ch_pi ]->count;
+	size_t ocount =  other->m_channels[ och_pi ]->count;
+	if ( count % size != 0 || ocount % osize != 0 ) return;
+	if ( count / size != ocount / osize ) return;
+	
+	for ( uint32 c = 0; c < m_data->get_channel_count(); ++c )
+	{
+		mesh_raw_channel* old = m_data->m_channels[c];
+		size_t frame_count = ( old->is_index() ? 1 : old->count / size );
+		m_data->m_channels[c] = append_channels( old, other->m_channels[c], frame_count );
+		NV_ASSERT( m_data->m_channels[c], "Merge problem!" );
+		if ( old->is_index() )
+		{
+			switch ( old->desc.slots[0].etype )
+			{
+			case USHORT : 
+				{
+					NV_ASSERT( size + osize < uint16(-1), "Index out of range!" );
+					uint16* indexes = (uint16*)m_data->m_channels[c]->data;
+					for ( uint16 i = (uint16)old->count; i < m_data->m_channels[c]->count; ++i )
+						indexes[i] += (uint16)size;
+
+				}
+				break;
+			case UINT   : 
+				{
+					uint32* indexes = (uint32*)m_data->m_channels[c]->data;
+					for ( uint32 i = old->count; i < m_data->m_channels[c]->count; ++i )
+						indexes[i] += size;
+				}
+				break;
+			default : NV_ASSERT( false, "Unsupported index type!" ); break;
+			}
+			m_data->m_index_channel = m_data->m_channels[c];
+		}
+		delete old;
+	}
+}
Index: trunk/src/gfx/skeletal_mesh.cc
===================================================================
--- trunk/src/gfx/skeletal_mesh.cc	(revision 294)
+++ trunk/src/gfx/skeletal_mesh.cc	(revision 295)
@@ -225,5 +225,5 @@
 		skeletal_animation_entry_gpu * anim = (skeletal_animation_entry_gpu*)(a_anim);
 		anim->prepare( m_bone_data );
-		update_animation( a_anim, uint32( anim->get_start() * 1000.f * anim->get_frame_rate() * 1000 ) );
+		update_animation( a_anim, 0 );
 	}
 }
Index: trunk/src/gl/gl_window.cc
===================================================================
--- trunk/src/gl/gl_window.cc	(revision 294)
+++ trunk/src/gl/gl_window.cc	(revision 295)
@@ -206,5 +206,5 @@
 
 gl_window::gl_window( device* dev, uint16 width, uint16 height, bool fullscreen )
-	: m_device( dev ), m_width( width ), m_height( height ), m_title("NV Engine"), m_handle( nullptr ), m_adopted( false )
+	: m_device( dev ), m_width( width ), m_height( height ), m_title("NV Engine"), m_handle( nullptr ), m_hwnd( 0 ), m_adopted( false )
 {
 	//	bpp = m_info->vfmt->BitsPerPixel;
@@ -298,5 +298,13 @@
 void gl_window::swap_buffers()
 {
-	if ( m_adopted ) return; // NOT SURE
+	if ( m_adopted )
+	{
+#if NV_PLATFORM == NV_WINDOWS
+		::SwapBuffers( (HDC)m_hwnd );
+#else
+	NV_ASSERT( false, "Native GL context only working with SDL 2.0!" );
+#endif
+
+	}
 #if NV_SDL_VERSION == NV_SDL_20
 	SDL_GL_SwapWindow( static_cast<SDL_Window*>( m_handle ) );
@@ -328,4 +336,5 @@
 //  	m_height  = (uint16)rect.bottom;
 	m_handle  = window;
+	m_hwnd    = ::GetDC( (HWND)handle );
 	m_context->set_viewport( nv::ivec4( 0, 0, m_width, m_height ) );
 #else
Index: trunk/src/library.cc
===================================================================
--- trunk/src/library.cc	(revision 294)
+++ trunk/src/library.cc	(revision 295)
@@ -120,5 +120,4 @@
     }
     m_handle = NULL;
-    NV_LOG( LOG_NOTICE, "library : '" + m_name + "' closed." );
 }
 
Index: trunk/src/logger.cc
===================================================================
--- trunk/src/logger.cc	(revision 294)
+++ trunk/src/logger.cc	(revision 295)
@@ -251,2 +251,12 @@
 	return buffer;
 }
+
+const char* nv::log_sink::level_name( log_level level ) const
+{
+	return NV_LOG_LEVEL_NAME( level );
+}
+
+const char* nv::log_sink::padded_level_name( log_level level ) const
+{
+	return NV_LOG_LEVEL_NAME_PAD( level );
+}
