/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2021 3Dconnexion * Copyright (C) 2021 KiCad Developers, see AUTHORS.txt for contributors. * * This program 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 3 of the License, or (at your * option) any later version. * * This program 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 this program. If not, see . */ #include "nl_3d_viewer_plugin_impl.h" // 3d-viewer #include <3d-viewer/3d_rendering/track_ball.h> #include <3d-viewer/3d_canvas/eda_3d_canvas.h> // KiCAD includes #include #include // stdlib #include #include #include #include /** * Flag to enable the NL_3D_VIEWER_PLUGIN debug tracing. * * Use "KI_TRACE_NL_3D_VIEWER_PLUGIN" to enable. * * @ingroup trace_env_vars */ const wxChar* NL_3D_VIEWER_PLUGIN_IMPL::m_logTrace = wxT( "KI_TRACE_NL_3D_VIEWER_PLUGIN" ); /** * Template to compare two floating point values for equality within a required epsilon. * * @param aFirst value to compare. * @param aSecond value to compare. * @param aEpsilon allowed error. * @return true if the values considered equal within the specified epsilon, otherwise false. */ template bool equals( T aFirst, T aSecond, T aEpsilon = static_cast( FLT_EPSILON ) ) { T diff = fabs( aFirst - aSecond ); if( diff < aEpsilon ) { return true; } aFirst = fabs( aFirst ); aSecond = fabs( aSecond ); T largest = aFirst > aSecond ? aFirst : aSecond; if( diff <= largest * aEpsilon ) { return true; } return false; } /** * Template to compare two glm::mat values for equality within a required epsilon. * * @param aFirst value to compare. * @param aSecond value to compare. * @param aEpsilon allowed error. * @return true if the values considered equal within the specified epsilon, otherwise false. */ template bool equals( glm::mat const& aFirst, glm::mat const& aSecond, T aEpsilon = static_cast( FLT_EPSILON * 10 ) ) { T const* first = glm::value_ptr( aFirst ); T const* second = glm::value_ptr( aSecond ); for( glm::length_t j = 0; j < L * C; ++j ) { if( !equals( first[j], second[j], aEpsilon ) ) { return false; } } return true; } NL_3D_VIEWER_PLUGIN_IMPL::NL_3D_VIEWER_PLUGIN_IMPL( EDA_3D_CANVAS* aCanvas ) : NAV_3D( false, false ), m_canvas( aCanvas ), m_capIsMoving( false ) { m_camera = dynamic_cast( m_canvas->GetCamera() ); PutProfileHint( "KiCAD 3D" ); EnableNavigation( true ); PutFrameTimingSource( TimingSource::SpaceMouse ); exportCommandsAndImages(); } NL_3D_VIEWER_PLUGIN_IMPL::~NL_3D_VIEWER_PLUGIN_IMPL() { EnableNavigation( false ); } void NL_3D_VIEWER_PLUGIN_IMPL::SetFocus( bool aFocus ) { wxLogTrace( m_logTrace, wxT( "NL_3D_VIEWER_PLUGIN_IMPL::SetFocus %d" ), aFocus ); NAV_3D::Write( navlib::focus_k, aFocus ); } // temporary store for the categories typedef std::map CATEGORY_STORE; /** * Add a category to the store. * * The function adds category paths of the format "A.B" where B is a sub-category of A. * * @param aCategoryPath is the std::string representation of the category. * @param aCategoryStore is the CATEGORY_STORE instance to add to. * @return a CATEGORY_STORE::iterator where the category was added. */ CATEGORY_STORE::iterator add_category( std::string aCategoryPath, CATEGORY_STORE& aCategoryStore ) { using TDx::SpaceMouse::CCategory; CATEGORY_STORE::iterator parent_iter = aCategoryStore.begin(); std::string::size_type pos = aCategoryPath.find_last_of( '.' ); if( pos != std::string::npos ) { std::string parentPath = aCategoryPath.substr( 0, pos ); parent_iter = aCategoryStore.find( parentPath ); if( parent_iter == aCategoryStore.end() ) { parent_iter = add_category( parentPath, aCategoryStore ); } } std::string name = aCategoryPath.substr( pos + 1 ); std::unique_ptr categoryNode = std::make_unique( aCategoryPath.c_str(), name.c_str() ); CATEGORY_STORE::iterator iter = aCategoryStore.insert( aCategoryStore.end(), CATEGORY_STORE::value_type( aCategoryPath, categoryNode.get() ) ); parent_iter->second->push_back( std::move( categoryNode ) ); return iter; } void NL_3D_VIEWER_PLUGIN_IMPL::exportCommandsAndImages() { wxLogTrace( m_logTrace, wxT( "NL_3D_VIEWER_PLUGIN_IMPL::exportCommandsAndImages" ) ); std::list actions = ACTION_MANAGER::GetActionList(); if( actions.size() == 0 ) { return; } using TDx::SpaceMouse::CCommand; using TDx::SpaceMouse::CCommandSet; // The root action set node CCommandSet commandSet( "EDA_3D_CANVAS", "3D Viewer" ); // Activate the command set NAV_3D::PutActiveCommands( commandSet.GetId() ); // temporary store for the categories CATEGORY_STORE categoryStore; std::vector vImages; // add the action set to the category_store CATEGORY_STORE::iterator iter = categoryStore.insert( categoryStore.end(), CATEGORY_STORE::value_type( ".", &commandSet ) ); std::list::const_iterator it; for( it = actions.begin(); it != actions.end(); ++it ) { const TOOL_ACTION* action = *it; std::string label = action->GetLabel().ToStdString(); if( label.empty() ) { continue; } std::string name = action->GetName(); // Do no export commands for the pcbnew app. if( name.rfind( "pcbnew.", 0 ) == 0 ) { continue; } std::string strCategory = action->GetToolName(); CATEGORY_STORE::iterator iter = categoryStore.find( strCategory ); if( iter == categoryStore.end() ) { iter = add_category( std::move( strCategory ), categoryStore ); } std::string description = action->GetDescription().ToStdString(); // Arbitrary 8-bit data stream wxMemoryOutputStream imageStream; if( action->GetIcon() != BITMAPS::INVALID_BITMAP ) { wxImage image = KiBitmap( action->GetIcon() ).ConvertToImage(); image.SaveFile( imageStream, wxBitmapType::wxBITMAP_TYPE_PNG ); image.Destroy(); if( imageStream.GetSize() ) { wxStreamBuffer* streamBuffer = imageStream.GetOutputStreamBuffer(); TDx::CImage image = TDx::CImage::FromData( "", 0, name.c_str() ); image.AssignImage( std::string( reinterpret_cast( streamBuffer->GetBufferStart() ), streamBuffer->GetBufferSize() ), 0 ); wxLogTrace( m_logTrace, "Adding image for : %s", name ); vImages.push_back( std::move( image ) ); } } wxLogTrace( m_logTrace, "Inserting command: %s, description: %s, in category: %s", name, description, iter->first ); iter->second->push_back( CCommand( std::move( name ), std::move( label ), std::move( description ) ) ); } NAV_3D::AddCommandSet( commandSet ); NAV_3D::AddImages( vImages ); } long NL_3D_VIEWER_PLUGIN_IMPL::GetCameraMatrix( navlib::matrix_t& matrix ) const { // cache the camera matrix so that we can tell if the view has been moved and // calculate a delta transform if required. m_cameraMatrix = m_camera->GetViewMatrix(); std::copy_n( glm::value_ptr( glm::inverse( m_cameraMatrix ) ), 16, matrix.m ); return 0; } long NL_3D_VIEWER_PLUGIN_IMPL::GetPointerPosition( navlib::point_t& position ) const { SFVEC3F origin, direction; m_camera->MakeRayAtCurrentMousePosition( origin, direction ); position = { origin.x, origin.y, origin.z }; return 0; } long NL_3D_VIEWER_PLUGIN_IMPL::GetViewExtents( navlib::box_t& extents ) const { if( m_camera->GetProjection() == PROJECTION_TYPE::PERSPECTIVE ) { return navlib::make_result_code( navlib::navlib_errc::invalid_operation ); } const CAMERA_FRUSTUM& f = m_camera->GetFrustum(); double half_width = f.fw / 2.; double half_height = f.fh / 2.; extents = { -half_width, -half_height, f.nearD, half_width, half_height, f.farD }; return 0; } long NL_3D_VIEWER_PLUGIN_IMPL::GetViewFOV( double& aFov ) const { const CAMERA_FRUSTUM& f = m_camera->GetFrustum(); aFov = glm::radians( f.angle ); return 0; } long NL_3D_VIEWER_PLUGIN_IMPL::GetViewFrustum( navlib::frustum_t& aFrustum ) const { if( m_camera->GetProjection() != PROJECTION_TYPE::PERSPECTIVE ) { return navlib::make_result_code( navlib::navlib_errc::invalid_operation ); } const CAMERA_FRUSTUM& f = m_camera->GetFrustum(); double half_width = f.nw / 2.; double half_height = f.nh / 2.; aFrustum = { -half_width, half_width, -half_height, half_height, f.nearD, f.farD }; return 0; } long NL_3D_VIEWER_PLUGIN_IMPL::GetIsViewPerspective( navlib::bool_t& perspective ) const { perspective = m_camera->GetProjection() == PROJECTION_TYPE::PERSPECTIVE ? 1 : 0; return 0; } long NL_3D_VIEWER_PLUGIN_IMPL::SetCameraMatrix( const navlib::matrix_t& aCameraMatrix ) { long result = 0; glm::mat4 cam, viewMatrix; std::copy_n( aCameraMatrix.m, 16, glm::value_ptr( cam ) ); viewMatrix = glm::inverse( cam ); glm::mat4 camera = m_camera->GetViewMatrix(); // The navlib does not move the camera in its z-axis in an orthographic projection // as this does not change the viewed object size. However ... if( m_camera->GetProjection() == PROJECTION_TYPE::ORTHO ) { // ... the CAMERA class couples zoom and distance to the object: we need to // ensure that The CAMERA's z position relative to the lookat_pos is not changed // in an orthographic projection. glm::vec4 lookat( m_camera->GetLookAtPos(), 1.0f ); glm::vec4 lookat_new = viewMatrix * lookat; glm::vec4 lookat_old = camera * lookat; viewMatrix[3].z += lookat_old.z - lookat_new.z; } if( !equals( camera, m_cameraMatrix ) ) { // Some other input device has moved the camera. Apply only the intended delta // transform ... m_camera->SetViewMatrix( viewMatrix * glm::inverse( m_cameraMatrix ) * camera ); m_camera->Update(); // .., cache the intended camera matrix so that we can calculate the delta // transform when needed ... m_cameraMatrix = viewMatrix; // ... and let the 3DMouse controller know, that something is amiss. return navlib::make_result_code( navlib::navlib_errc::error ); } m_camera->SetViewMatrix( viewMatrix ); m_camera->Update(); // cache the view matrix so that we know when it has changed. m_cameraMatrix = m_camera->GetViewMatrix(); // The camera has a constraint on the z position so we need to check that ... if( !equals( m_cameraMatrix[3].z, viewMatrix[3].z ) ) { // ... and let the 3DMouse controller know, when something is amiss. return navlib::make_result_code( navlib::navlib_errc::error ); } return 0; } long NL_3D_VIEWER_PLUGIN_IMPL::SetViewExtents( const navlib::box_t& extents ) { const CAMERA_FRUSTUM& f = m_camera->GetFrustum(); float factor = 2 * f.nw / ( extents.max_x - extents.min_x ); float zoom = m_camera->GetZoom() / factor; m_camera->Zoom( factor ); // The camera auto positions the camera to match the zoom values. We need to // update our cached camera matrix to match the new z value m_cameraMatrix[3].z = m_camera->GetViewMatrix()[3].z; // The camera has a constraint on the zoom factor so we need to check that ... if( zoom != m_camera->GetZoom() ) { // ... and let the 3DMouse controller know, when something is amiss. return navlib::make_result_code( navlib::navlib_errc::error ); } return 0; } long NL_3D_VIEWER_PLUGIN_IMPL::SetViewFOV( double fov ) { return navlib::make_result_code( navlib::navlib_errc::function_not_supported ); } long NL_3D_VIEWER_PLUGIN_IMPL::SetViewFrustum( const navlib::frustum_t& frustum ) { return navlib::make_result_code( navlib::navlib_errc::permission_denied ); } long NL_3D_VIEWER_PLUGIN_IMPL::GetModelExtents( navlib::box_t& extents ) const { SFVEC3F min = m_canvas->GetBoardAdapter().GetBBox().Min(); SFVEC3F max = m_canvas->GetBoardAdapter().GetBBox().Max(); extents = { min.x, min.y, min.z, max.x, max.y, max.z }; return 0; } long NL_3D_VIEWER_PLUGIN_IMPL::GetSelectionExtents( navlib::box_t& extents ) const { return navlib::make_result_code( navlib::navlib_errc::no_data_available ); } long NL_3D_VIEWER_PLUGIN_IMPL::GetSelectionTransform( navlib::matrix_t& transform ) const { return navlib::make_result_code( navlib::navlib_errc::no_data_available ); } long NL_3D_VIEWER_PLUGIN_IMPL::GetIsSelectionEmpty( navlib::bool_t& empty ) const { empty = true; return 0; } long NL_3D_VIEWER_PLUGIN_IMPL::SetSelectionTransform( const navlib::matrix_t& matrix ) { return navlib::make_result_code( navlib::navlib_errc::invalid_operation ); } long NL_3D_VIEWER_PLUGIN_IMPL::GetPivotPosition( navlib::point_t& position ) const { SFVEC3F lap = m_camera->GetLookAtPos(); position = { lap.x, lap.y, lap.z }; return 0; } long NL_3D_VIEWER_PLUGIN_IMPL::IsUserPivot( navlib::bool_t& userPivot ) const { userPivot = false; return 0; } long NL_3D_VIEWER_PLUGIN_IMPL::SetPivotPosition( const navlib::point_t& position ) { SFVEC3F pivotPos = SFVEC3F( position.x, position.y, position.z ); // Set the pivot icon position . m_camera->SetLookAtPos_T1( pivotPos ); #if 0 // Set the trackball pivot to the same position as the 3DMouse pivot. glm::mat4 m = m_camera->GetViewMatrix(); m_camera->SetLookAtPos( pivotPos ); m_camera->SetViewMatrix( std::move( m ) ); m_camera->Update(); #endif m_canvas->Request_refresh(); return 0; } long NL_3D_VIEWER_PLUGIN_IMPL::GetPivotVisible( navlib::bool_t& visible ) const { visible = m_canvas->GetRenderPivot(); return 0; } long NL_3D_VIEWER_PLUGIN_IMPL::SetPivotVisible( bool visible ) { m_canvas->SetRenderPivot( visible ); m_canvas->Request_refresh(); return 0; } long NL_3D_VIEWER_PLUGIN_IMPL::GetHitLookAt( navlib::point_t& position ) const { RAY mouseRay; mouseRay.Init( m_rayOrigin, m_rayDirection ); float hit; glm::vec3 vec; // Test it with the board bounding box if( m_canvas->GetBoardAdapter().GetBBox().Intersect( mouseRay, &hit ) ) { vec = mouseRay.at( hit ); position = { vec.x, vec.y, vec.z }; return 0; } return navlib::make_result_code( navlib::navlib_errc::no_data_available ); } long NL_3D_VIEWER_PLUGIN_IMPL::SetHitAperture( double aperture ) { return navlib::make_result_code( navlib::navlib_errc::function_not_supported ); } long NL_3D_VIEWER_PLUGIN_IMPL::SetHitDirection( const navlib::vector_t& direction ) { m_rayDirection = { direction.x, direction.y, direction.z }; return 0; } long NL_3D_VIEWER_PLUGIN_IMPL::SetHitLookFrom( const navlib::point_t& eye ) { m_rayOrigin = { eye.x, eye.y, eye.z }; return 0; } long NL_3D_VIEWER_PLUGIN_IMPL::SetHitSelectionOnly( bool onlySelection ) { return navlib::make_result_code( navlib::navlib_errc::function_not_supported ); } long NL_3D_VIEWER_PLUGIN_IMPL::SetActiveCommand( std::string commandId ) { if( commandId.empty() ) { return 0; } std::list actions = ACTION_MANAGER::GetActionList(); TOOL_ACTION* context = nullptr; for( std::list::const_iterator it = actions.begin(); it != actions.end(); it++ ) { TOOL_ACTION* action = *it; std::string nm = action->GetName(); if( commandId == nm ) { context = action; } } if( context != nullptr ) { wxWindow* parent = m_canvas->GetParent(); // Only allow command execution if the window is enabled. i.e. there is not a modal dialog // currently active. if( parent->IsEnabled() ) { TOOL_MANAGER* tool_manager = static_cast( parent )->GetToolManager(); if( tool_manager == nullptr ) { return navlib::make_result_code( navlib::navlib_errc::invalid_operation ); } // Get the selection to use to test if the action is enabled SELECTION& sel = tool_manager->GetToolHolder()->GetCurrentSelection(); bool runAction = true; if( const ACTION_CONDITIONS* aCond = tool_manager->GetActionManager()->GetCondition( *context ) ) { runAction = aCond->enableCondition( sel ); } if( runAction ) { tool_manager->RunAction( *context, true ); m_canvas->Request_refresh(); } } else { return navlib::make_result_code( navlib::navlib_errc::invalid_operation ); } } return 0; } long NL_3D_VIEWER_PLUGIN_IMPL::SetSettingsChanged( long change ) { return 0; } long NL_3D_VIEWER_PLUGIN_IMPL::SetMotionFlag( bool value ) { m_capIsMoving = value; return 0; } long NL_3D_VIEWER_PLUGIN_IMPL::SetTransaction( long value ) { if( value != 0L ) { } else { m_canvas->Request_refresh( true ); wxLogTrace( m_logTrace, wxT( "End of transaction" ) ); } return 0; } long NL_3D_VIEWER_PLUGIN_IMPL::SetCameraTarget( const navlib::point_t& position ) { return navlib::make_result_code( navlib::navlib_errc::function_not_supported ); } long NL_3D_VIEWER_PLUGIN_IMPL::GetFrontView( navlib::matrix_t& matrix ) const { matrix = { 1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1 }; return 0; } long NL_3D_VIEWER_PLUGIN_IMPL::GetCoordinateSystem( navlib::matrix_t& matrix ) const { // Use the right-handed coordinate system X-right, Z-up, Y-in (row vectors) matrix = { 1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1 }; return 0; } long NL_3D_VIEWER_PLUGIN_IMPL::GetIsViewRotatable( navlib::bool_t& isRotatable ) const { isRotatable = true; return 0; }