From f33f10bb389fbbcdbf6c9555e20e0aaa66cb0bff Mon Sep 17 00:00:00 2001
From: Jeff Young <jeff@rokeby.ie>
Date: Sat, 15 Mar 2025 16:13:27 +0000
Subject: [PATCH] User-defined layers for 3D viewer.

---
 3d-viewer/3d_canvas/board_adapter.cpp         |  79 ++++++-----
 3d-viewer/3d_canvas/board_adapter.h           |  17 +--
 3d-viewer/3d_canvas/create_layer_items.cpp    |  47 ++++++-
 .../3d_rendering/opengl/render_3d_opengl.cpp  |  24 ++++
 .../3d_rendering/raytracing/create_scene.cpp  |  11 +-
 .../3d_viewer/eda_3d_viewer_settings.cpp      |   8 ++
 3d-viewer/3d_viewer/eda_3d_viewer_settings.h  |   1 +
 3d-viewer/dialogs/appearance_controls_3D.cpp  | 123 +++++++++++++-----
 common/layer_id.cpp                           | 112 ++++++++++++++++
 common/settings/color_settings.cpp            |  10 ++
 include/layer_ids.h                           |  49 +++++++
 11 files changed, 397 insertions(+), 84 deletions(-)

diff --git a/3d-viewer/3d_canvas/board_adapter.cpp b/3d-viewer/3d_canvas/board_adapter.cpp
index 17f14c387d..27821a5823 100644
--- a/3d-viewer/3d_canvas/board_adapter.cpp
+++ b/3d-viewer/3d_canvas/board_adapter.cpp
@@ -91,7 +91,6 @@ BOARD_ADAPTER::BOARD_ADAPTER() :
         m_IsPreviewer( false ),
         m_board( nullptr ),
         m_3dModelManager( nullptr ),
-        m_colors( nullptr ),
         m_layerZcoordTop(),
         m_layerZcoordBottom()
 {
@@ -139,6 +138,9 @@ BOARD_ADAPTER::BOARD_ADAPTER() :
     m_ECO1Color          = SFVEC4F( 0.70, 0.10, 0.10,  1.0 );
     m_ECO2Color          = SFVEC4F( 0.70, 0.10, 0.10,  1.0 );
 
+    for( int ii = 0; ii < 45; ++ii )
+        m_UserDefinedLayerColor[ii] = SFVEC4F( 0.70, 0.10, 0.10, 1.0 );
+
     m_platedPadsFront = nullptr;
     m_platedPadsBack = nullptr;
     m_offboardPadsFront = nullptr;
@@ -231,11 +233,12 @@ void BOARD_ADAPTER::ReloadColorSettings() noexcept
 
     SETTINGS_MANAGER& mgr = Pgm().GetSettingsManager();
     PCBNEW_SETTINGS*  cfg = mgr.GetAppSettings<PCBNEW_SETTINGS>( "pcbnew" );
+    COLOR_SETTINGS*   colors = mgr.GetColorSettings( cfg ? cfg->m_ColorTheme : wxString( "" ) );
 
-    if( cfg )
+    if( colors )
     {
-        m_colors = Pgm().GetSettingsManager().GetColorSettings( cfg->m_ColorTheme );
-        GetBoardEditorCopperLayerColors( cfg );
+        for( int layer = F_Cu; layer < PCB_LAYER_ID_COUNT; ++layer )
+            m_BoardEditorColors[ layer ] = colors->GetColor( layer );
     }
 }
 
@@ -264,7 +267,15 @@ bool BOARD_ADAPTER::Is3dLayerEnabled( PCB_LAYER_ID aLayer,
     case Cmts_User: return aVisibilityFlags.test( LAYER_3D_USER_COMMENTS );
     case Eco1_User: return aVisibilityFlags.test( LAYER_3D_USER_ECO1 );
     case Eco2_User: return aVisibilityFlags.test( LAYER_3D_USER_ECO2 );
-    default:        return m_board && m_board->IsLayerVisible( aLayer );
+    default:
+    {
+        int layer3D = MapPCBUserLayerTo3DLayer( aLayer );
+
+        if( layer3D != UNDEFINED_LAYER )
+            return aVisibilityFlags.test( layer3D );
+
+        return m_board && m_board->IsLayerVisible( aLayer );
+    }
     }
 }
 
@@ -587,6 +598,9 @@ void BOARD_ADAPTER::InitSettings( REPORTER* aStatusReporter, REPORTER* aWarningR
     m_UserCommentsColor  = to_SFVEC4F( colors[ LAYER_3D_USER_COMMENTS ] );
     m_ECO1Color          = to_SFVEC4F( colors[ LAYER_3D_USER_ECO1 ] );
     m_ECO2Color          = to_SFVEC4F( colors[ LAYER_3D_USER_ECO2 ] );
+
+    for( int layer = LAYER_3D_USER_1; layer <= LAYER_3D_USER_45; ++layer )
+        m_UserDefinedLayerColor[ layer - LAYER_3D_USER_1 ] = to_SFVEC4F( colors[ layer ] );
 }
 
 
@@ -609,28 +623,15 @@ std::map<int, COLOR4D> BOARD_ADAPTER::GetDefaultColors() const
     colors[ LAYER_3D_USER_ECO1 ]         = BOARD_ADAPTER::g_DefaultECOs;
     colors[ LAYER_3D_USER_ECO2 ]         = BOARD_ADAPTER::g_DefaultECOs;
 
+    COLOR_SETTINGS* settings = Pgm().GetSettingsManager().GetColorSettings( wxEmptyString );
+
+    for( int layer = LAYER_3D_USER_1; layer <= LAYER_3D_USER_45; ++layer )
+        colors[ layer ] = settings->GetColor( layer );
+
     return colors;
 }
 
 
-void BOARD_ADAPTER::GetBoardEditorCopperLayerColors( PCBNEW_SETTINGS* aCfg )
-{
-    m_BoardEditorColors.clear();
-
-    if( m_copperLayersCount <= 0 )
-        return;
-
-    COLOR_SETTINGS* settings = Pgm().GetSettingsManager().GetColorSettings( aCfg->m_ColorTheme );
-
-    LSET copperLayers = LSET::AllCuMask();
-
-    for( auto layer : LAYER_RANGE( F_Cu, B_Cu, m_copperLayersCount ) )
-    {
-        m_BoardEditorColors[ layer ] = settings->GetColor( layer );
-    }
-}
-
-
 std::map<int, COLOR4D> BOARD_ADAPTER::GetLayerColors() const
 {
     std::map<int, COLOR4D> colors;
@@ -754,8 +755,13 @@ void BOARD_ADAPTER::SetLayerColors( const std::map<int, COLOR4D>& aColors )
     COLOR_SETTINGS* settings = Pgm().GetSettingsManager().GetColorSettings();
 
     for( const auto& [ layer, color ] : aColors )
+    {
         settings->SetColor( layer, color );
 
+        if( layer >= LAYER_3D_USER_1 && layer <= LAYER_3D_USER_45 )
+            m_UserDefinedLayerColor[ layer - LAYER_3D_USER_1 ] = GetColor( color );
+    }
+
     Pgm().GetSettingsManager().SaveColorSettings( settings, "3d_viewer" );
 }
 
@@ -776,6 +782,9 @@ void BOARD_ADAPTER::SetVisibleLayers( const std::bitset<LAYER_3D_END>& aLayers )
     m_Cfg->m_Render.show_eco1                      = aLayers.test( LAYER_3D_USER_ECO1 );
     m_Cfg->m_Render.show_eco2                      = aLayers.test( LAYER_3D_USER_ECO2 );
 
+    for( int layer = LAYER_3D_USER_1; layer <= LAYER_3D_USER_45; ++layer )
+        m_Cfg->m_Render.show_user[ layer - LAYER_3D_USER_1 ] = aLayers.test( layer );
+
     m_Cfg->m_Render.show_footprints_normal         = aLayers.test( LAYER_3D_TH_MODELS );
     m_Cfg->m_Render.show_footprints_insert         = aLayers.test( LAYER_3D_SMD_MODELS );
     m_Cfg->m_Render.show_footprints_virtual        = aLayers.test( LAYER_3D_VIRTUAL_MODELS );
@@ -810,6 +819,9 @@ std::bitset<LAYER_3D_END> BOARD_ADAPTER::GetVisibleLayers() const
     ret.set( LAYER_3D_USER_ECO1,         m_Cfg->m_Render.show_eco1 );
     ret.set( LAYER_3D_USER_ECO2,         m_Cfg->m_Render.show_eco2 );
 
+    for( int layer = LAYER_3D_USER_1; layer <= LAYER_3D_USER_45; ++layer )
+        ret.set( layer, m_Cfg->m_Render.show_user[ layer - LAYER_3D_USER_1 ] );
+
     ret.set( LAYER_FP_REFERENCES,        m_Cfg->m_Render.show_fp_references );
     ret.set( LAYER_FP_VALUES,            m_Cfg->m_Render.show_fp_values );
     ret.set( LAYER_FP_TEXT,              m_Cfg->m_Render.show_fp_text );
@@ -871,6 +883,9 @@ std::bitset<LAYER_3D_END> BOARD_ADAPTER::GetVisibleLayers() const
         ret.set( LAYER_3D_USER_ECO1,         layers.test( Eco1_User ) );
         ret.set( LAYER_3D_USER_ECO2,         layers.test( Eco2_User ) );
 
+        for( int layer = LAYER_3D_USER_1; layer <= LAYER_3D_USER_45; ++layer )
+            ret.set( layer, layers.test( Map3DUserLayerToPCBLayer( layer ) ) );
+
         ret.set( LAYER_FP_REFERENCES, plotParams.GetPlotReference() );
         ret.set( LAYER_FP_VALUES,     plotParams.GetPlotValue() );
         ret.set( LAYER_FP_TEXT,       plotParams.GetPlotFPText() );
@@ -902,6 +917,9 @@ std::bitset<LAYER_3D_END> BOARD_ADAPTER::GetDefaultVisibleLayers() const
     ret.set( LAYER_3D_USER_ECO1,         false );
     ret.set( LAYER_3D_USER_ECO2,         false );
 
+    for( int layer = LAYER_3D_USER_1; layer <= LAYER_3D_USER_45; ++layer )
+        ret.set( layer, false );
+
     ret.set( LAYER_FP_REFERENCES,        true );
     ret.set( LAYER_FP_VALUES,            true );
     ret.set( LAYER_FP_TEXT,              true );
@@ -982,19 +1000,14 @@ float BOARD_ADAPTER::GetFootprintZPos( bool aIsFlipped ) const
 }
 
 
-SFVEC4F BOARD_ADAPTER::GetLayerColor( PCB_LAYER_ID aLayerId ) const
+SFVEC4F BOARD_ADAPTER::GetLayerColor( int aLayerId ) const
 {
+    if( aLayerId >= LAYER_3D_USER_1 && aLayerId <= LAYER_3D_USER_45 )
+        aLayerId = Map3DUserLayerToPCBLayer( aLayerId );
+
     wxASSERT( aLayerId < PCB_LAYER_ID_COUNT );
 
-    const COLOR4D color = m_colors->GetColor( aLayerId );
-
-    return SFVEC4F( color.r, color.g, color.b, color.a );
-}
-
-
-SFVEC4F BOARD_ADAPTER::GetItemColor( int aItemId ) const
-{
-    return GetColor( m_colors->GetColor( aItemId ) );
+    return GetColor( m_BoardEditorColors.at( aLayerId ) );
 }
 
 
diff --git a/3d-viewer/3d_canvas/board_adapter.h b/3d-viewer/3d_canvas/board_adapter.h
index bf79b4c4fe..2c6918a394 100644
--- a/3d-viewer/3d_canvas/board_adapter.h
+++ b/3d-viewer/3d_canvas/board_adapter.h
@@ -112,11 +112,6 @@ public:
      */
     std::map<int, COLOR4D> GetLayerColors() const;
 
-    /**
-     * Build the copper color list used by the board editor, and store it in m_BoardEditorColors
-     */
-    void GetBoardEditorCopperLayerColors( PCBNEW_SETTINGS* aCfg );
-
     std::map<int, COLOR4D> GetDefaultColors() const;
     void SetLayerColors( const std::map<int, COLOR4D>& aColors );
 
@@ -210,15 +205,7 @@ public:
      * @param aLayerId the layer to get the color information.
      * @return the color in SFVEC3F format.
      */
-    SFVEC4F GetLayerColor( PCB_LAYER_ID aLayerId ) const;
-
-    /**
-     * Get the technical color of a layer.
-     *
-     * @param aItemId the item id to get the color information.
-     * @return the color in SFVEC3F format.
-     */
-    SFVEC4F GetItemColor( int aItemId ) const;
+    SFVEC4F GetLayerColor( int aLayerId ) const;
 
     /**
      * @param[in] aColor is the color mapped.
@@ -455,6 +442,7 @@ public:
     SFVEC4F           m_UserCommentsColor;
     SFVEC4F           m_ECO1Color;
     SFVEC4F           m_ECO2Color;
+    SFVEC4F           m_UserDefinedLayerColor[45];
 
     std::map<int, COLOR4D> m_ColorOverrides;  ///< allows to override color scheme colors
     std::map<int, COLOR4D> m_BoardEditorColors; ///< list of colors used by the board editor
@@ -462,7 +450,6 @@ public:
 private:
     BOARD*            m_board;
     S3D_CACHE*        m_3dModelManager;
-    COLOR_SETTINGS*   m_colors;
 
     VECTOR2I          m_boardPos;             ///< Board center position in board internal units.
     VECTOR2I          m_boardSize;            ///< Board size in board internal units.
diff --git a/3d-viewer/3d_canvas/create_layer_items.cpp b/3d-viewer/3d_canvas/create_layer_items.cpp
index 9fcacd2f28..bccbda6594 100644
--- a/3d-viewer/3d_canvas/create_layer_items.cpp
+++ b/3d-viewer/3d_canvas/create_layer_items.cpp
@@ -892,7 +892,52 @@ void BOARD_ADAPTER::createLayers( REPORTER* aStatusReporter )
             Dwgs_User,
             Cmts_User,
             Eco1_User,
-            Eco2_User
+            Eco2_User,
+            User_1,
+            User_2,
+            User_3,
+            User_4,
+            User_5,
+            User_6,
+            User_7,
+            User_8,
+            User_9,
+            User_10,
+            User_11,
+            User_12,
+            User_13,
+            User_14,
+            User_15,
+            User_16,
+            User_17,
+            User_18,
+            User_19,
+            User_20,
+            User_21,
+            User_22,
+            User_23,
+            User_24,
+            User_25,
+            User_26,
+            User_27,
+            User_28,
+            User_29,
+            User_30,
+            User_31,
+            User_32,
+            User_33,
+            User_34,
+            User_35,
+            User_36,
+            User_37,
+            User_38,
+            User_39,
+            User_40,
+            User_41,
+            User_42,
+            User_43,
+            User_44,
+            User_45,
         } );
 
     std::bitset<LAYER_3D_END> enabledFlags = visibilityFlags;
diff --git a/3d-viewer/3d_rendering/opengl/render_3d_opengl.cpp b/3d-viewer/3d_rendering/opengl/render_3d_opengl.cpp
index c1051e45f2..89f525cb1e 100644
--- a/3d-viewer/3d_rendering/opengl/render_3d_opengl.cpp
+++ b/3d-viewer/3d_rendering/opengl/render_3d_opengl.cpp
@@ -362,10 +362,34 @@ void RENDER_3D_OPENGL::setLayerMaterial( PCB_LAYER_ID aLayerID )
         break;
 
     default:
+    {
+        int layer3D = MapPCBUserLayerTo3DLayer( aLayerID );
+
+        if( layer3D != UNDEFINED_LAYER )
+        {
+            // Note: MUST do this in LAYER_3D space; User_1..User_45 are NOT contiguous
+            int user_idx = layer3D - LAYER_3D_USER_1;
+
+            m_materials.m_Plastic.m_Diffuse = m_boardAdapter.m_UserDefinedLayerColor[ user_idx ];
+            m_materials.m_Plastic.m_Ambient = SFVEC3F( m_materials.m_Plastic.m_Diffuse.r * 0.05f,
+                                                       m_materials.m_Plastic.m_Diffuse.g * 0.05f,
+                                                       m_materials.m_Plastic.m_Diffuse.b * 0.05f );
+
+            m_materials.m_Plastic.m_Specular = SFVEC3F( m_materials.m_Plastic.m_Diffuse.r * 0.7f,
+                                                        m_materials.m_Plastic.m_Diffuse.g * 0.7f,
+                                                        m_materials.m_Plastic.m_Diffuse.b * 0.7f );
+
+            m_materials.m_Plastic.m_Shininess = 0.078125f * 128.0f;
+            m_materials.m_Plastic.m_Emissive  = SFVEC3F( 0.0f, 0.0f, 0.0f );
+            OglSetMaterial( m_materials.m_Plastic, 1.0f );
+            break;
+        }
+
         m_materials.m_Copper.m_Diffuse = m_boardAdapter.m_CopperColor;
         OglSetMaterial( m_materials.m_Copper, 1.0f );
         break;
     }
+    }
 }
 
 
diff --git a/3d-viewer/3d_rendering/raytracing/create_scene.cpp b/3d-viewer/3d_rendering/raytracing/create_scene.cpp
index 0ec83f2448..66e305cab7 100644
--- a/3d-viewer/3d_rendering/raytracing/create_scene.cpp
+++ b/3d-viewer/3d_rendering/raytracing/create_scene.cpp
@@ -632,7 +632,15 @@ void RENDER_3D_RAYTRACE_BASE::Reload( REPORTER* aStatusReporter, REPORTER* aWarn
             break;
 
         default:
-            if( m_boardAdapter.m_Cfg->m_Render.differentiate_plated_copper )
+        {
+            int layer3D = MapPCBUserLayerTo3DLayer( layer_id );
+
+            if( layer3D != UNDEFINED_LAYER )
+            {
+                // Note: MUST do this in LAYER_3D space; User_1..User_45 are NOT contiguous
+                layerColor = m_boardAdapter.m_UserDefinedLayerColor[ layer3D - LAYER_3D_USER_1 ];
+            }
+            else if( m_boardAdapter.m_Cfg->m_Render.differentiate_plated_copper )
             {
                 layerColor = SFVEC3F( 184.0f / 255.0f, 115.0f / 255.0f, 50.0f / 255.0f );
                 materialLayer = &m_materials.m_NonPlatedCopper;
@@ -645,6 +653,7 @@ void RENDER_3D_RAYTRACE_BASE::Reload( REPORTER* aStatusReporter, REPORTER* aWarn
 
             break;
         }
+        }
 
         createItemsFromContainer( container2d, layer_id, materialLayer, layerColor, 0.0f );
     } // for each layer on map
diff --git a/3d-viewer/3d_viewer/eda_3d_viewer_settings.cpp b/3d-viewer/3d_viewer/eda_3d_viewer_settings.cpp
index 3cfb182a17..4abf9fea0f 100644
--- a/3d-viewer/3d_viewer/eda_3d_viewer_settings.cpp
+++ b/3d-viewer/3d_viewer/eda_3d_viewer_settings.cpp
@@ -19,6 +19,7 @@
  * with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
+#include <fmt/format.h>
 #include <3d_enums.h>
 #include <common_ogl/ogl_attr_list.h>
 #include <settings/parameters.h>
@@ -353,6 +354,13 @@ EDA_3D_VIEWER_SETTINGS::EDA_3D_VIEWER_SETTINGS() :
                                             &m_Render.show_eco1, true ) );
     m_params.emplace_back( new PARAM<bool>( "render.show_eco2",
                                             &m_Render.show_eco2, true ) );
+
+    for( int layer = 0; layer < 45; ++layer )
+    {
+        m_params.emplace_back( new PARAM<bool>( fmt::format( "render.show_user{}", layer + 1 ),
+                                                &m_Render.show_user[layer], false ) );
+    }
+
     m_params.emplace_back( new PARAM<bool>( "render.show_footprints_insert",
                                             &m_Render.show_footprints_insert, true ) );
     m_params.emplace_back( new PARAM<bool>( "render.show_footprints_normal",
diff --git a/3d-viewer/3d_viewer/eda_3d_viewer_settings.h b/3d-viewer/3d_viewer/eda_3d_viewer_settings.h
index 9701210574..79bb73ef80 100644
--- a/3d-viewer/3d_viewer/eda_3d_viewer_settings.h
+++ b/3d-viewer/3d_viewer/eda_3d_viewer_settings.h
@@ -132,6 +132,7 @@ public:
         bool show_drawings;
         bool show_eco1;
         bool show_eco2;
+        bool show_user[45];
         bool show_footprints_insert;
         bool show_footprints_normal;
         bool show_footprints_virtual;
diff --git a/3d-viewer/dialogs/appearance_controls_3D.cpp b/3d-viewer/dialogs/appearance_controls_3D.cpp
index b3bd7dee04..f73fa727ba 100644
--- a/3d-viewer/dialogs/appearance_controls_3D.cpp
+++ b/3d-viewer/dialogs/appearance_controls_3D.cpp
@@ -29,8 +29,7 @@
 #include <eda_3d_viewer_frame.h>
 #include <pcbnew_settings.h>
 #include <project.h>
-#include <settings/settings_manager.h>
-#include <settings/color_settings.h>
+#include <board.h>
 #include <tool/tool_manager.h>
 #include <tools/pcb_actions.h>
 #include <tools/eda_3d_actions.h>
@@ -48,44 +47,87 @@
 /// Render Row abbreviation to reduce source width.
 #define RR  APPEARANCE_CONTROLS_3D::APPEARANCE_SETTING_3D
 
+// clang-format off
 /// Template for object appearance settings
 const APPEARANCE_CONTROLS_3D::APPEARANCE_SETTING_3D APPEARANCE_CONTROLS_3D::s_layerSettings[] = {
 
-    //     text                              id                           tooltip
-    RR( _HKI( "Board Body" ), LAYER_3D_BOARD, _HKI( "Show board body" ) ),
-    RR( _HKI( "F.Cu" ), LAYER_3D_COPPER_TOP, _HKI( "Show front copper / surface finish color" ) ),
-    RR( _HKI( "B.Cu" ), LAYER_3D_COPPER_BOTTOM, _HKI( "Show back copper / surface finish color" ) ),
-    RR( _HKI( "Adhesive" ), LAYER_3D_ADHESIVE, _HKI( "Show adhesive" ) ),
-    RR( _HKI( "Solder Paste" ), LAYER_3D_SOLDERPASTE, _HKI( "Show solder paste" ) ),
-    RR( _HKI( "F.Silkscreen" ), LAYER_3D_SILKSCREEN_TOP, _HKI( "Show front silkscreen" ) ),
-    RR( _HKI( "B.Silkscreen" ), LAYER_3D_SILKSCREEN_BOTTOM, _HKI( "Show back silkscreen" ) ),
-    RR( _HKI( "F.Mask" ), LAYER_3D_SOLDERMASK_TOP, _HKI( "Show front solder mask" ) ),
-    RR( _HKI( "B.Mask" ), LAYER_3D_SOLDERMASK_BOTTOM, _HKI( "Show back solder mask" ) ),
-    RR( _HKI( "User.Drawings" ), LAYER_3D_USER_DRAWINGS, _HKI( "Show user drawings layer" ) ),
-    RR( _HKI( "User.Comments" ), LAYER_3D_USER_COMMENTS, _HKI( "Show user comments layer" ) ),
-    RR( _HKI( "User.Eco1" ), LAYER_3D_USER_ECO1, _HKI( "Show user ECO1 layer" ) ),
-    RR( _HKI( "User.Eco2" ), LAYER_3D_USER_ECO2, _HKI( "Show user ECO2 layer" ) ),
+    //        text                           id                        tooltip
+    RR( _HKI( "Board Body" ),    LAYER_3D_BOARD,             _HKI( "Show board body" ) ),
+    RR( _HKI( "F.Cu" ),          LAYER_3D_COPPER_TOP,        _HKI( "Show front copper / surface finish color" ) ),
+    RR( _HKI( "B.Cu" ),          LAYER_3D_COPPER_BOTTOM,     _HKI( "Show back copper / surface finish color" ) ),
+    RR( _HKI( "Adhesive" ),      LAYER_3D_ADHESIVE,          _HKI( "Show adhesive" ) ),
+    RR( _HKI( "Solder Paste" ),  LAYER_3D_SOLDERPASTE,       _HKI( "Show solder paste" ) ),
+    RR( _HKI( "F.Silkscreen" ),  LAYER_3D_SILKSCREEN_TOP,    _HKI( "Show front silkscreen" ) ),
+    RR( _HKI( "B.Silkscreen" ),  LAYER_3D_SILKSCREEN_BOTTOM, _HKI( "Show back silkscreen" ) ),
+    RR( _HKI( "F.Mask" ),        LAYER_3D_SOLDERMASK_TOP,    _HKI( "Show front solder mask" ) ),
+    RR( _HKI( "B.Mask" ),        LAYER_3D_SOLDERMASK_BOTTOM, _HKI( "Show back solder mask" ) ),
+    RR( _HKI( "User.Drawings" ), LAYER_3D_USER_DRAWINGS,     _HKI( "Show user drawings layer" ) ),
+    RR( _HKI( "User.Comments" ), LAYER_3D_USER_COMMENTS,     _HKI( "Show user comments layer" ) ),
+    RR( _HKI( "User.Eco1" ),     LAYER_3D_USER_ECO1,         _HKI( "Show user ECO1 layer" ) ),
+    RR( _HKI( "User.Eco2" ),     LAYER_3D_USER_ECO2,         _HKI( "Show user ECO2 layer" ) ),
+    RR( _HKI( "User.1" ),        LAYER_3D_USER_1,            _HKI( "Show user defined layer 1" ) ),
+    RR( _HKI( "User.2" ),        LAYER_3D_USER_2,            _HKI( "Show user defined layer 2" ) ),
+    RR( _HKI( "User.3" ),        LAYER_3D_USER_3,            _HKI( "Show user defined layer 3" ) ),
+    RR( _HKI( "User.4" ),        LAYER_3D_USER_4,            _HKI( "Show user defined layer 4" ) ),
+    RR( _HKI( "User.5" ),        LAYER_3D_USER_5,            _HKI( "Show user defined layer 5" ) ),
+    RR( _HKI( "User.6" ),        LAYER_3D_USER_6,            _HKI( "Show user defined layer 6" ) ),
+    RR( _HKI( "User.7" ),        LAYER_3D_USER_7,            _HKI( "Show user defined layer 7" ) ),
+    RR( _HKI( "User.8" ),        LAYER_3D_USER_8,            _HKI( "Show user defined layer 8" ) ),
+    RR( _HKI( "User.9" ),        LAYER_3D_USER_9,            _HKI( "Show user defined layer 9" ) ),
+    RR( _HKI( "User.10" ),       LAYER_3D_USER_10,           _HKI( "Show user defined layer 10" ) ),
+    RR( _HKI( "User.11" ),       LAYER_3D_USER_11,           _HKI( "Show user defined layer 11" ) ),
+    RR( _HKI( "User.12" ),       LAYER_3D_USER_12,           _HKI( "Show user defined layer 12" ) ),
+    RR( _HKI( "User.13" ),       LAYER_3D_USER_13,           _HKI( "Show user defined layer 13" ) ),
+    RR( _HKI( "User.14" ),       LAYER_3D_USER_14,           _HKI( "Show user defined layer 14" ) ),
+    RR( _HKI( "User.15" ),       LAYER_3D_USER_15,           _HKI( "Show user defined layer 15" ) ),
+    RR( _HKI( "User.16" ),       LAYER_3D_USER_16,           _HKI( "Show user defined layer 16" ) ),
+    RR( _HKI( "User.17" ),       LAYER_3D_USER_17,           _HKI( "Show user defined layer 17" ) ),
+    RR( _HKI( "User.18" ),       LAYER_3D_USER_18,           _HKI( "Show user defined layer 18" ) ),
+    RR( _HKI( "User.19" ),       LAYER_3D_USER_19,           _HKI( "Show user defined layer 19" ) ),
+    RR( _HKI( "User.20" ),       LAYER_3D_USER_20,           _HKI( "Show user defined layer 20" ) ),
+    RR( _HKI( "User.21" ),       LAYER_3D_USER_21,           _HKI( "Show user defined layer 21" ) ),
+    RR( _HKI( "User.22" ),       LAYER_3D_USER_22,           _HKI( "Show user defined layer 22" ) ),
+    RR( _HKI( "User.23" ),       LAYER_3D_USER_23,           _HKI( "Show user defined layer 23" ) ),
+    RR( _HKI( "User.24" ),       LAYER_3D_USER_24,           _HKI( "Show user defined layer 24" ) ),
+    RR( _HKI( "User.25" ),       LAYER_3D_USER_25,           _HKI( "Show user defined layer 25" ) ),
+    RR( _HKI( "User.26" ),       LAYER_3D_USER_26,           _HKI( "Show user defined layer 26" ) ),
+    RR( _HKI( "User.27" ),       LAYER_3D_USER_27,           _HKI( "Show user defined layer 27" ) ),
+    RR( _HKI( "User.28" ),       LAYER_3D_USER_28,           _HKI( "Show user defined layer 28" ) ),
+    RR( _HKI( "User.29" ),       LAYER_3D_USER_29,           _HKI( "Show user defined layer 29" ) ),
+    RR( _HKI( "User.30" ),       LAYER_3D_USER_30,           _HKI( "Show user defined layer 30" ) ),
+    RR( _HKI( "User.31" ),       LAYER_3D_USER_31,           _HKI( "Show user defined layer 31" ) ),
+    RR( _HKI( "User.32" ),       LAYER_3D_USER_32,           _HKI( "Show user defined layer 32" ) ),
+    RR( _HKI( "User.33" ),       LAYER_3D_USER_33,           _HKI( "Show user defined layer 33" ) ),
+    RR( _HKI( "User.34" ),       LAYER_3D_USER_34,           _HKI( "Show user defined layer 34" ) ),
+    RR( _HKI( "User.35" ),       LAYER_3D_USER_35,           _HKI( "Show user defined layer 35" ) ),
+    RR( _HKI( "User.36" ),       LAYER_3D_USER_36,           _HKI( "Show user defined layer 36" ) ),
+    RR( _HKI( "User.37" ),       LAYER_3D_USER_37,           _HKI( "Show user defined layer 37" ) ),
+    RR( _HKI( "User.38" ),       LAYER_3D_USER_38,           _HKI( "Show user defined layer 38" ) ),
+    RR( _HKI( "User.39" ),       LAYER_3D_USER_39,           _HKI( "Show user defined layer 39" ) ),
+    RR( _HKI( "User.40" ),       LAYER_3D_USER_40,           _HKI( "Show user defined layer 40" ) ),
+    RR( _HKI( "User.41" ),       LAYER_3D_USER_41,           _HKI( "Show user defined layer 41" ) ),
+    RR( _HKI( "User.42" ),       LAYER_3D_USER_42,           _HKI( "Show user defined layer 42" ) ),
+    RR( _HKI( "User.43" ),       LAYER_3D_USER_43,           _HKI( "Show user defined layer 43" ) ),
+    RR( _HKI( "User.44" ),       LAYER_3D_USER_44,           _HKI( "Show user defined layer 44" ) ),
+    RR( _HKI( "User.45" ),       LAYER_3D_USER_45,           _HKI( "Show user defined layer 45" ) ),
     RR(),
-    RR( _HKI( "Through-hole Models" ), LAYER_3D_TH_MODELS, EDA_3D_ACTIONS::showTHT ),
-    RR( _HKI( "SMD Models" ), LAYER_3D_SMD_MODELS, EDA_3D_ACTIONS::showSMD ),
-    RR( _HKI( "Virtual Models" ), LAYER_3D_VIRTUAL_MODELS, EDA_3D_ACTIONS::showVirtual ),
-    RR( _HKI( "Models not in POS File" ), LAYER_3D_MODELS_NOT_IN_POS,
-        EDA_3D_ACTIONS::showNotInPosFile ),
-    RR( _HKI( "Models marked DNP" ), LAYER_3D_MODELS_MARKED_DNP, EDA_3D_ACTIONS::showDNP ),
-    RR( _HKI( "Model Bounding Boxes" ), LAYER_3D_BOUNDING_BOXES, EDA_3D_ACTIONS::showBBoxes ),
+    RR( _HKI( "Through-hole Models" ),    LAYER_3D_TH_MODELS,         EDA_3D_ACTIONS::showTHT ),
+    RR( _HKI( "SMD Models" ),             LAYER_3D_SMD_MODELS,        EDA_3D_ACTIONS::showSMD ),
+    RR( _HKI( "Virtual Models" ),         LAYER_3D_VIRTUAL_MODELS,    EDA_3D_ACTIONS::showVirtual ),
+    RR( _HKI( "Models not in POS File" ), LAYER_3D_MODELS_NOT_IN_POS, EDA_3D_ACTIONS::showNotInPosFile ),
+    RR( _HKI( "Models marked DNP" ),      LAYER_3D_MODELS_MARKED_DNP, EDA_3D_ACTIONS::showDNP ),
+    RR( _HKI( "Model Bounding Boxes" ),   LAYER_3D_BOUNDING_BOXES,    EDA_3D_ACTIONS::showBBoxes ),
     RR(),
-    RR( _HKI( "Values" ), LAYER_FP_VALUES, _HKI( "Show footprint values" ) ),
-    RR( _HKI( "References" ), LAYER_FP_REFERENCES, _HKI( "Show footprint references" ) ),
-    RR( _HKI( "Footprint Text" ), LAYER_FP_TEXT, _HKI( "Show all footprint text" ) ),
-    RR( _HKI( "Off-board Silkscreen" ), LAYER_3D_OFF_BOARD_SILK,
-        _HKI( "Do not clip silk layers to board outline" ) ),
+    RR( _HKI( "Values" ),               LAYER_FP_VALUES,            _HKI( "Show footprint values" ) ),
+    RR( _HKI( "References" ),           LAYER_FP_REFERENCES,        _HKI( "Show footprint references" ) ),
+    RR( _HKI( "Footprint Text" ),       LAYER_FP_TEXT,              _HKI( "Show all footprint text" ) ),
+    RR( _HKI( "Off-board Silkscreen" ), LAYER_3D_OFF_BOARD_SILK,    _HKI( "Do not clip silk layers to board outline" ) ),
     RR(),
-    RR( _HKI( "3D Axis" ), LAYER_3D_AXES, EDA_3D_ACTIONS::showAxis ),
-    RR( _HKI( "Background Start" ), LAYER_3D_BACKGROUND_TOP,
-        _HKI( "Background gradient start color" ) ),
-    RR( _HKI( "Background End" ), LAYER_3D_BACKGROUND_BOTTOM,
-        _HKI( "Background gradient end color" ) ),
+    RR( _HKI( "3D Axis" ),              LAYER_3D_AXES,              EDA_3D_ACTIONS::showAxis ),
+    RR( _HKI( "Background Start" ),     LAYER_3D_BACKGROUND_TOP,    _HKI( "Background gradient start color" ) ),
+    RR( _HKI( "Background End" ),       LAYER_3D_BACKGROUND_BOTTOM, _HKI( "Background gradient end color" ) ),
 };
+// clang-format on
 
 // The list of IDs that can have colors coming from the board stackup, and cannot be
 // modified if use colors from stackup is activated
@@ -410,6 +452,10 @@ void APPEARANCE_CONTROLS_3D::OnLayerVisibilityChanged( int aLayer, bool isVisibl
 
     default:
         visibleLayers.set( aLayer, isVisible );
+
+        if( aLayer >= LAYER_3D_USER_1 && aLayer <= LAYER_3D_USER_45 )
+            killFollow = true;
+
         break;
     }
 
@@ -443,7 +489,6 @@ void APPEARANCE_CONTROLS_3D::onColorSwatchChanged( COLOR_SWATCH* aSwatch )
     std::map<int, COLOR4D>    colors = m_frame->GetAdapter().GetLayerColors();
 
     m_frame->GetAdapter().SetVisibleLayers( visibleLayers );
-    m_frame->GetAdapter().SetLayerColors( colors );
 
     int     layer = aSwatch->GetId();
     COLOR4D newColor = aSwatch->GetSwatchColor();
@@ -470,6 +515,7 @@ void APPEARANCE_CONTROLS_3D::rebuildLayers()
     std::bitset<LAYER_3D_END> visibleLayers = m_frame->GetAdapter().GetVisibleLayers();
     std::map<int, COLOR4D>    colors = m_frame->GetAdapter().GetLayerColors();
     std::map<int, COLOR4D>    defaultColors = m_frame->GetAdapter().GetDefaultColors();
+    LSET                      enabled = m_frame->GetBoard()->GetEnabledLayers();
 
     m_layerSettings.clear();
     m_layersOuterSizer->Clear( true );
@@ -570,9 +616,18 @@ void APPEARANCE_CONTROLS_3D::rebuildLayers()
         std::unique_ptr<APPEARANCE_SETTING_3D>& setting = m_layerSettings.back();
 
         if( setting->m_Spacer )
+        {
             m_layersOuterSizer->AddSpacer( m_pointSize );
+        }
+        else if( setting->m_Id >= LAYER_3D_USER_1 && setting->m_Id <= LAYER_3D_USER_45 )
+        {
+            if( enabled.test( Map3DUserLayerToPCBLayer( setting->m_Id ) ) )
+                appendLayer( setting );
+        }
         else
+        {
             appendLayer( setting );
+        }
 
         m_layerSettingsMap[setting->m_Id] = setting.get();
     }
diff --git a/common/layer_id.cpp b/common/layer_id.cpp
index a34a55d08f..dddd64da61 100644
--- a/common/layer_id.cpp
+++ b/common/layer_id.cpp
@@ -260,3 +260,115 @@ PCB_LAYER_ID BoardLayerFromLegacyId( int aLegacyId )
         }
     }
 }
+
+
+PCB_LAYER_ID Map3DUserLayerToPCBLayer( int aLayer )
+{
+    // NOTE: User_1..User45 are NOT consecutive numbers!
+
+    switch( aLayer )
+    {
+    case LAYER_3D_USER_1:  return User_1;
+    case LAYER_3D_USER_2:  return User_2;
+    case LAYER_3D_USER_3:  return User_3;
+    case LAYER_3D_USER_4:  return User_4;
+    case LAYER_3D_USER_5:  return User_5;
+    case LAYER_3D_USER_6:  return User_6;
+    case LAYER_3D_USER_7:  return User_7;
+    case LAYER_3D_USER_8:  return User_8;
+    case LAYER_3D_USER_9:  return User_9;
+    case LAYER_3D_USER_10: return User_10;
+    case LAYER_3D_USER_11: return User_11;
+    case LAYER_3D_USER_12: return User_12;
+    case LAYER_3D_USER_13: return User_13;
+    case LAYER_3D_USER_14: return User_14;
+    case LAYER_3D_USER_15: return User_15;
+    case LAYER_3D_USER_16: return User_16;
+    case LAYER_3D_USER_17: return User_17;
+    case LAYER_3D_USER_18: return User_18;
+    case LAYER_3D_USER_19: return User_19;
+    case LAYER_3D_USER_20: return User_20;
+    case LAYER_3D_USER_21: return User_21;
+    case LAYER_3D_USER_22: return User_22;
+    case LAYER_3D_USER_23: return User_23;
+    case LAYER_3D_USER_24: return User_24;
+    case LAYER_3D_USER_25: return User_25;
+    case LAYER_3D_USER_26: return User_26;
+    case LAYER_3D_USER_27: return User_27;
+    case LAYER_3D_USER_28: return User_28;
+    case LAYER_3D_USER_29: return User_29;
+    case LAYER_3D_USER_30: return User_30;
+    case LAYER_3D_USER_31: return User_31;
+    case LAYER_3D_USER_32: return User_32;
+    case LAYER_3D_USER_33: return User_33;
+    case LAYER_3D_USER_34: return User_34;
+    case LAYER_3D_USER_35: return User_35;
+    case LAYER_3D_USER_36: return User_36;
+    case LAYER_3D_USER_37: return User_37;
+    case LAYER_3D_USER_38: return User_38;
+    case LAYER_3D_USER_39: return User_39;
+    case LAYER_3D_USER_40: return User_40;
+    case LAYER_3D_USER_41: return User_41;
+    case LAYER_3D_USER_42: return User_42;
+    case LAYER_3D_USER_43: return User_43;
+    case LAYER_3D_USER_44: return User_44;
+    case LAYER_3D_USER_45: return User_45;
+    default:               return UNDEFINED_LAYER;
+    }
+}
+
+
+int MapPCBUserLayerTo3DLayer( PCB_LAYER_ID aLayer )
+{
+    // NOTE: User_1..User45 are NOT consecutive numbers!
+
+    switch( aLayer )
+    {
+    case User_1:  return LAYER_3D_USER_1;
+    case User_2:  return LAYER_3D_USER_2;
+    case User_3:  return LAYER_3D_USER_3;
+    case User_4:  return LAYER_3D_USER_4;
+    case User_5:  return LAYER_3D_USER_5;
+    case User_6:  return LAYER_3D_USER_6;
+    case User_7:  return LAYER_3D_USER_7;
+    case User_8:  return LAYER_3D_USER_8;
+    case User_9:  return LAYER_3D_USER_9;
+    case User_10: return LAYER_3D_USER_10;
+    case User_11: return LAYER_3D_USER_11;
+    case User_12: return LAYER_3D_USER_12;
+    case User_13: return LAYER_3D_USER_13;
+    case User_14: return LAYER_3D_USER_14;
+    case User_15: return LAYER_3D_USER_15;
+    case User_16: return LAYER_3D_USER_16;
+    case User_17: return LAYER_3D_USER_17;
+    case User_18: return LAYER_3D_USER_18;
+    case User_19: return LAYER_3D_USER_19;
+    case User_20: return LAYER_3D_USER_20;
+    case User_21: return LAYER_3D_USER_21;
+    case User_22: return LAYER_3D_USER_22;
+    case User_23: return LAYER_3D_USER_23;
+    case User_24: return LAYER_3D_USER_24;
+    case User_25: return LAYER_3D_USER_25;
+    case User_26: return LAYER_3D_USER_26;
+    case User_27: return LAYER_3D_USER_27;
+    case User_28: return LAYER_3D_USER_28;
+    case User_29: return LAYER_3D_USER_29;
+    case User_30: return LAYER_3D_USER_30;
+    case User_31: return LAYER_3D_USER_31;
+    case User_32: return LAYER_3D_USER_32;
+    case User_33: return LAYER_3D_USER_33;
+    case User_34: return LAYER_3D_USER_34;
+    case User_35: return LAYER_3D_USER_35;
+    case User_36: return LAYER_3D_USER_36;
+    case User_37: return LAYER_3D_USER_37;
+    case User_38: return LAYER_3D_USER_38;
+    case User_39: return LAYER_3D_USER_39;
+    case User_40: return LAYER_3D_USER_40;
+    case User_41: return LAYER_3D_USER_41;
+    case User_42: return LAYER_3D_USER_42;
+    case User_43: return LAYER_3D_USER_43;
+    case User_44: return LAYER_3D_USER_44;
+    case User_45: return LAYER_3D_USER_45;
+    default:      return UNDEFINED_LAYER;
+    }
+}
diff --git a/common/settings/color_settings.cpp b/common/settings/color_settings.cpp
index 07db99ea02..d58c530f4e 100644
--- a/common/settings/color_settings.cpp
+++ b/common/settings/color_settings.cpp
@@ -250,6 +250,16 @@ COLOR_SETTINGS::COLOR_SETTINGS( const wxString& aFilename, bool aAbsolutePath )
     CLR( "3d_viewer.soldermask_top",    LAYER_3D_SOLDERMASK_TOP    );
     CLR( "3d_viewer.solderpaste",       LAYER_3D_SOLDERPASTE       );
 
+    for( int layer = LAYER_3D_USER_1; layer <= LAYER_3D_USER_45; ++layer )
+    {
+        int          idx = layer - LAYER_3D_USER_1;
+        PCB_LAYER_ID pcb_layer = Map3DUserLayerToPCBLayer( layer );
+
+        m_params.emplace_back( new COLOR_MAP_PARAM( "3d_viewer.user_" + std::to_string( idx + 1 ),
+                                                    layer, s_defaultTheme.at( pcb_layer ),
+                                                    &m_colors ) );
+    }
+
     registerMigration( 0, 1, std::bind( &COLOR_SETTINGS::migrateSchema0to1, this ) );
 
     registerMigration( 1, 2,
diff --git a/include/layer_ids.h b/include/layer_ids.h
index af2108cd3c..5b48675d13 100644
--- a/include/layer_ids.h
+++ b/include/layer_ids.h
@@ -551,6 +551,51 @@ enum LAYER_3D_ID : int
         LAYER_3D_USER_DRAWINGS,
         LAYER_3D_USER_ECO1,
         LAYER_3D_USER_ECO2,
+        LAYER_3D_USER_1,
+        LAYER_3D_USER_2,
+        LAYER_3D_USER_3,
+        LAYER_3D_USER_4,
+        LAYER_3D_USER_5,
+        LAYER_3D_USER_6,
+        LAYER_3D_USER_7,
+        LAYER_3D_USER_8,
+        LAYER_3D_USER_9,
+        LAYER_3D_USER_10,
+        LAYER_3D_USER_11,
+        LAYER_3D_USER_12,
+        LAYER_3D_USER_13,
+        LAYER_3D_USER_14,
+        LAYER_3D_USER_15,
+        LAYER_3D_USER_16,
+        LAYER_3D_USER_17,
+        LAYER_3D_USER_18,
+        LAYER_3D_USER_19,
+        LAYER_3D_USER_20,
+        LAYER_3D_USER_21,
+        LAYER_3D_USER_22,
+        LAYER_3D_USER_23,
+        LAYER_3D_USER_24,
+        LAYER_3D_USER_25,
+        LAYER_3D_USER_26,
+        LAYER_3D_USER_27,
+        LAYER_3D_USER_28,
+        LAYER_3D_USER_29,
+        LAYER_3D_USER_30,
+        LAYER_3D_USER_31,
+        LAYER_3D_USER_32,
+        LAYER_3D_USER_33,
+        LAYER_3D_USER_34,
+        LAYER_3D_USER_35,
+        LAYER_3D_USER_36,
+        LAYER_3D_USER_37,
+        LAYER_3D_USER_38,
+        LAYER_3D_USER_39,
+        LAYER_3D_USER_40,
+        LAYER_3D_USER_41,
+        LAYER_3D_USER_42,
+        LAYER_3D_USER_43,
+        LAYER_3D_USER_44,
+        LAYER_3D_USER_45,
         LAYER_3D_TH_MODELS,
         LAYER_3D_SMD_MODELS,
         LAYER_3D_VIRTUAL_MODELS,
@@ -863,6 +908,10 @@ inline size_t CopperLayerToOrdinal( PCB_LAYER_ID aLayer )
 KICOMMON_API PCB_LAYER_ID BoardLayerFromLegacyId( int aLegacyId );
 
 
+KICOMMON_API PCB_LAYER_ID Map3DUserLayerToPCBLayer( int aLayer );
+KICOMMON_API int MapPCBUserLayerTo3DLayer( PCB_LAYER_ID aLayer );
+
+
 KICOMMON_API PCB_LAYER_ID ToLAYER_ID( int aLayer );
 
 #endif // LAYER_IDS_H