KiCad PCB EDA Suite
Loading...
Searching...
No Matches
panel_fp_properties_3d_model.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright (C) 2018 Jean-Pierre Charras, jp.charras at wanadoo.fr
5 * Copyright (C) 2015 Dick Hollenbeck, [email protected]
6 * Copyright (C) 2008 Wayne Stambaugh <[email protected]>
7 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
8 *
9 * This program is free software; you can redistribute it and/or
10 * modify it under the terms of the GNU General Public License
11 * as published by the Free Software Foundation; either version 2
12 * of the License, or (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 */
22
24
25#include <algorithm>
28#include <env_vars.h>
29#include <bitmaps.h>
32#include <widgets/wx_grid.h>
34#include <board.h>
35#include <footprint.h>
40#include "filename_resolver.h"
41#include <pgm_base.h>
42#include <kiplatform/ui.h>
48#include <project_pcb.h>
53
54#include <reporter.h>
55
56#include <wx/defs.h>
57#include <wx/filedlg.h>
58#include <wx/msgdlg.h>
59#include <wx/notebook.h>
60
67
68
69wxDEFINE_EVENT( wxCUSTOM_PANEL_SHOWN_EVENT, wxCommandEvent );
70
72 FOOTPRINT* aFootprint,
73 DIALOG_SHIM* aDialogParent,
74 PANEL_EMBEDDED_FILES* aFilesPanel,
75 wxWindow* aParent ) :
77 m_parentDialog( aDialogParent ),
78 m_frame( aFrame ),
79 m_footprint( aFootprint ),
80 m_filesPanel( aFilesPanel ),
81 m_inSelect( false )
82{
83 m_splitter1->SetSashPosition( FromDIP( m_splitter1->GetSashPosition() ) );
84 m_splitter1->SetMinimumPaneSize( FromDIP( m_splitter1->GetMinimumPaneSize() ) );
85
86 GRID_TRICKS* trick = new GRID_TRICKS( m_modelsGrid, [this]( wxCommandEvent& aEvent )
87 {
88 OnAdd3DRow( aEvent );
89 } );
91 m_modelsGrid->PushEventHandler( trick );
92 m_modelsGrid->SetupColumnAutosizer( COL_FILENAME );
93
94 // Get the last 3D directory
96
97 if( cfg && cfg->m_LastFootprint3dDir.IsEmpty() )
98 {
99 wxGetEnv( ENV_VAR::GetVersionedEnvVarName( wxS( "3DMODEL_DIR" ) ),
100 &cfg->m_LastFootprint3dDir );
101 }
102
103 // Icon showing warning/error information
104 wxGridCellAttr* attr = new wxGridCellAttr;
105 attr->SetReadOnly();
106 m_modelsGrid->SetColAttr( COL_PROBLEM, attr );
107
108 // Filename
109 attr = new wxGridCellAttr;
110
111 attr->SetEditor( new GRID_CELL_RUN_FUNCTION_EDITOR(
113 [this]( int aRow, int )
114 {
115 if( aRow < 0 || aRow >= static_cast<int>( m_shapes3D_list.size() ) )
116 return;
117
119
120 if( pick3DModel( m_modelsGrid->GetCellValue( aRow, COL_FILENAME ), model ) )
121 {
122 m_modelsGrid->CallAfter(
123 [this, aRow, model]
124 {
125 m_modelsGrid->DisableCellEditControl();
126 set3DModelRow( aRow, model );
127 } );
128 }
129 } ) );
130
131 m_modelsGrid->SetColAttr( COL_FILENAME, attr );
132
133 // Show checkbox
134 attr = new wxGridCellAttr;
135 attr->SetRenderer( new wxGridCellBoolRenderer() );
136 attr->SetReadOnly(); // not really; we delegate interactivity to GRID_TRICKS
137 attr->SetAlignment( wxALIGN_CENTER, wxALIGN_CENTER );
138 m_modelsGrid->SetColAttr( COL_SHOWN, attr );
139 m_modelsGrid->SetWindowStyleFlag( m_modelsGrid->GetWindowStyle() & ~wxHSCROLL );
140
142
144
145 m_previewPane->SetEmbeddedFilesDelegate( m_filesPanel->GetLocalFiles() );
146
147 m_LowerSizer3D->Add( m_previewPane, 1, wxEXPAND, 5 );
148
149 // Configure button logos
155
156 m_modelsGrid->Bind( wxEVT_GRID_CELL_CHANGING, &PANEL_FP_PROPERTIES_3D_MODEL::on3DModelCellChanging, this );
157 Bind( wxEVT_SHOW, &PANEL_FP_PROPERTIES_3D_MODEL::onShowEvent, this );
158 Bind( wxEVT_SIZE, &PANEL_FP_PROPERTIES_3D_MODEL::onSize, this );
159 aParent->Bind( wxEVT_NOTEBOOK_PAGE_CHANGED, &PANEL_FP_PROPERTIES_3D_MODEL::onNotebookPageChanged, this );
161
162 // Bind extrusion control events to update the 3D preview
169}
170
171
173{
174 // Delete the GRID_TRICKS.
175 m_modelsGrid->PopEventHandler( true );
176
177 m_modelsGrid->Unbind( wxEVT_GRID_CELL_CHANGING, &PANEL_FP_PROPERTIES_3D_MODEL::on3DModelCellChanging, this );
178 // Unbind OnShowEvent to prevent unnecessary event handling.
179 Unbind( wxEVT_SHOW, &PANEL_FP_PROPERTIES_3D_MODEL::onShowEvent, this );
180 Unbind( wxEVT_SIZE, &PANEL_FP_PROPERTIES_3D_MODEL::onSize, this );
181 GetParent()->Unbind( wxEVT_NOTEBOOK_PAGE_CHANGED, &PANEL_FP_PROPERTIES_3D_MODEL::onNotebookPageChanged, this );
182
187 this );
190
191 // free the memory used by all models, otherwise models which were
192 // browsed but not used would consume memory
194
195 delete m_previewPane;
196}
197
198
200{
202
203 bool hasExtrusion = m_footprint->HasExtrudedBody();
204 m_enableExtrusionCheckbox->SetValue( hasExtrusion );
205
206 const EXTRUDED_3D_BODY* body = m_footprint->GetExtrudedBody();
207 m_showExtrusionCheckbox->SetValue( body ? body->m_show : true );
208 m_componentHeightCtrl->SetValue(
209 wxString::Format( wxT( "%.4f" ), body ? pcbIUScale.IUTomm( body->m_height ) : 0.0 ) );
210 m_standoffHeightCtrl->SetValue(
211 wxString::Format( wxT( "%.4f" ), body ? pcbIUScale.IUTomm( body->m_standoff ) : 0.0 ) );
212
214
215 m_extrusionLayerChoice->Clear();
216 m_extrusionLayerChoice->Append( _( "Auto" ) );
217 m_extrusionLayerChoice->Append( _( "Courtyard layer" ) );
218 m_extrusionLayerChoice->Append( _( "Fabrication layer" ) );
219 m_extrusionLayerChoice->Append( _( "Silkscreen layer" ) );
220 m_extrusionLayerChoice->Append( _( "Pin bounding box" ) );
221
222 PCB_LAYER_ID layer = body ? body->m_layer : UNDEFINED_LAYER;
223 int selection = 0; // Auto
224
225 for( size_t i = 0; i < m_extrusionLayers.size(); i++ )
226 {
227 if( m_extrusionLayers[i] == layer )
228 {
229 selection = static_cast<int>( i );
230 break;
231 }
232 }
233
234 m_extrusionLayerChoice->SetSelection( selection );
235
238
239 if( color == KIGFX::COLOR4D::UNSPECIFIED )
241
242 m_extrusionColorSwatch->SetSwatchColor( color, false );
243
244 m_extrusionMaterialChoice->SetSelection(
245 static_cast<int>( body ? body->m_material : EXTRUSION_MATERIAL::PLASTIC ) );
246
249
250 Layout();
251
252 if( GetSizer() )
253 GetSizer()->Fit( this );
254
255 return true;
256}
257
258
260{
261 if( !m_modelsGrid->CommitPendingChanges() )
262 return false;
263
264 return m_filesPanel->CommitPendingChanges();
265}
266
267
269{
270 if( m_enableExtrusionCheckbox->GetValue() )
271 {
272 double compHeight;
273 double standoff;
274
275 if( !readExtrusionHeights( _( "Extruded 3D Body" ), compHeight, standoff ) )
276 return false;
277
278 if( compHeight <= 0.0 )
279 {
280 wxMessageBox( _( "Component height must be greater than zero." ), _( "Extruded 3D Body" ),
281 wxOK | wxICON_WARNING, this );
282 return false;
283 }
284
285 if( standoff < 0.0 )
286 standoff = 0.0;
287
288 if( standoff >= compHeight )
289 {
290 wxMessageBox( _( "Standoff height must be less than the overall height." ), _( "Extruded 3D Body" ),
291 wxOK | wxICON_WARNING, this );
292 return false;
293 }
294 }
295
296 return true;
297}
298
299
301{
302 if( m_enableExtrusionCheckbox->GetValue() )
303 {
304 double compHeight;
305 double standoff;
306
307 (void) readExtrusionHeights( _( "Extruded 3D Body" ), compHeight, standoff );
308
309 int sel = m_extrusionLayerChoice->GetSelection();
310
311 EXTRUDED_3D_BODY& body = m_footprint->EnsureExtrudedBody();
312 body.m_height = pcbIUScale.mmToIU( compHeight );
313 body.m_standoff = pcbIUScale.mmToIU( standoff );
314 body.m_layer = m_extrusionLayers[sel];
316 body.m_material = static_cast<EXTRUSION_MATERIAL>( m_extrusionMaterialChoice->GetSelection() );
317
318 FOOTPRINT* dummyFp = m_previewPane->GetDummyFootprint();
319
320 if( dummyFp && dummyFp->HasExtrudedBody() )
321 {
322 const EXTRUDED_3D_BODY* dummyBody = dummyFp->GetExtrudedBody();
323 body.m_scale = dummyBody->m_scale;
324 body.m_rotation = dummyBody->m_rotation;
325 body.m_offset = dummyBody->m_offset;
326 }
327
328 body.m_show = m_showExtrusionCheckbox->GetValue();
329 }
330 else
331 {
332 m_footprint->ClearExtrudedBody();
333 }
334
335 return true;
336}
337
338
340{
341 wxString default_path;
342 wxGetEnv( ENV_VAR::GetVersionedEnvVarName( wxS( "3DMODEL_DIR" ) ), &default_path );
343
344#ifdef __WINDOWS__
345 default_path.Replace( wxT( "/" ), wxT( "\\" ) );
346#endif
347
348 m_shapes3D_list.clear();
349 m_modelsGrid->ClearRows();
350
351 wxString origPath, alias, shortPath;
353
354 for( const FP_3DMODEL& model : m_footprint->Models() )
355 {
356 m_shapes3D_list.push_back( model );
357 origPath = model.m_Filename;
358
359 if( res && res->SplitAlias( origPath, alias, shortPath ) )
360 origPath = alias + wxT( ":" ) + shortPath;
361
362 m_modelsGrid->AppendRows( 1 );
363 int row = m_modelsGrid->GetNumberRows() - 1;
364 m_modelsGrid->SetCellValue( row, COL_FILENAME, origPath );
365 m_modelsGrid->SetCellValue( row, COL_SHOWN, model.m_Show ? wxT( "1" ) : wxT( "0" ) );
366
367 // Must be after the filename is set
369 }
370
371 select3DModel( 0 );
372
373 m_previewPane->UpdateDummyFootprint();
374 m_modelsGrid->SetGridWidthsDirty();
375
376 Layout();
377}
378
379
381{
382 m_inSelect = true;
383
384 aModelIdx = std::max( 0, aModelIdx );
385 aModelIdx = std::min( aModelIdx, m_modelsGrid->GetNumberRows() - 1 );
386
387 if( m_modelsGrid->GetNumberRows() )
388 {
389 m_modelsGrid->SelectRow( aModelIdx );
390 m_modelsGrid->SetGridCursor( aModelIdx, COL_FILENAME );
391 }
392
393 m_previewPane->SetSelectedModel( aModelIdx );
394
395 m_inSelect = false;
396}
397
398
400{
401 if( !m_inSelect )
402 select3DModel( aEvent.GetRow() );
403}
404
405
407{
408 if( !aFilename->empty() )
409 {
410 bool hasAlias = false;
412
413 aFilename->Replace( wxT( "\n" ), wxT( "" ) );
414 aFilename->Replace( wxT( "\r" ), wxT( "" ) );
415 aFilename->Replace( wxT( "\t" ), wxT( "" ) );
416
417 res->ValidateFileName( *aFilename, hasAlias );
418
419 // If the user has specified an alias in the name then prepend ':'
420 if( hasAlias )
421 aFilename->insert( 0, wxT( ":" ) );
422
423#ifdef __WINDOWS__
424 // In KiCad files, filenames and paths are stored using Unix notation
425 aFilename->Replace( wxT( "\\" ), wxT( "/" ) );
426#endif
427 }
428}
429
430
432{
433 if( aEvent.GetCol() == COL_FILENAME )
434 updateValidateStatus( aEvent.GetRow() );
435}
436
437
439{
440 if( aEvent.GetCol() == COL_FILENAME )
441 {
442 wxString filename = m_modelsGrid->GetCellValue( aEvent.GetRow(), COL_FILENAME );
443
444 if( !filename.empty() )
445 {
446 cleanupFilename( &filename );
447
448 // Update the grid with the modified filename
449 m_modelsGrid->SetCellValue( aEvent.GetRow(), COL_FILENAME, filename );
450 }
451
452 // Save the filename in the 3D shapes table
453 m_shapes3D_list[ aEvent.GetRow() ].m_Filename = filename;
454
455 // Update the validation status
456 updateValidateStatus( aEvent.GetRow() );
457 }
458 else if( aEvent.GetCol() == COL_SHOWN )
459 {
460 wxString showValue = m_modelsGrid->GetCellValue( aEvent.GetRow(), COL_SHOWN );
461
462 m_shapes3D_list[ aEvent.GetRow() ].m_Show = ( showValue == wxT( "1" ) );
463 }
464
465 m_previewPane->UpdateDummyFootprint();
466 onModify();
467}
468
469
471{
472 if( !m_modelsGrid->CommitPendingChanges() )
473 return;
474
475 if( !m_modelsGrid->GetNumberRows() || m_shapes3D_list.empty() )
476 return;
477
478 wxArrayInt selectedRows = m_modelsGrid->GetSelectedRows();
479 wxGridCellCoordsArray selectedCells = m_modelsGrid->GetSelectedCells();
480 wxGridCellCoordsArray blockTopLeft = m_modelsGrid->GetSelectionBlockTopLeft();
481 wxGridCellCoordsArray blockBottomRight = m_modelsGrid->GetSelectionBlockBottomRight();
482
483 for( unsigned ii = 0; ii < selectedCells.GetCount(); ++ii )
484 selectedRows.Add( selectedCells[ii].GetRow() );
485
486 if( !blockTopLeft.IsEmpty() && !blockBottomRight.IsEmpty() )
487 {
488 for( int row = blockTopLeft[0].GetRow(); row <= blockBottomRight[0].GetRow(); ++row )
489 selectedRows.Add( row );
490 }
491
492 if( selectedRows.empty() && m_modelsGrid->GetGridCursorRow() >= 0 )
493 selectedRows.Add( m_modelsGrid->GetGridCursorRow() );
494
495 if( selectedRows.empty() )
496 {
497 wxBell();
498 return;
499 }
500
501 std::sort( selectedRows.begin(), selectedRows.end() );
502
503 int nextSelection = selectedRows.front();
504 int lastRow = -1;
505
506 // Don't allow selection until we call select3DModel(), below. Otherwise wxWidgets
507 // has a tendency to get its knickers in a knot....
508 m_inSelect = true;
509
510 m_modelsGrid->ClearSelection();
511
512 for( int ii = selectedRows.size() - 1; ii >= 0; --ii )
513 {
514 int row = selectedRows[ii];
515
516 if( row == lastRow )
517 continue;
518
519 lastRow = row;
520
521 if( row < 0 || row >= (int) m_shapes3D_list.size() )
522 continue;
523
524 // Not all files are embedded but this will ignore the ones that are not
525 m_filesPanel->RemoveEmbeddedFile( m_shapes3D_list[row].m_Filename );
526 m_shapes3D_list.erase( m_shapes3D_list.begin() + row );
527 m_modelsGrid->DeleteRows( row );
528 }
529
530 if( m_modelsGrid->GetNumberRows() > 0 )
531 nextSelection = std::min( nextSelection, m_modelsGrid->GetNumberRows() - 1 );
532 else
533 nextSelection = 0;
534
535 select3DModel( nextSelection ); // will clamp index within bounds
536 m_previewPane->UpdateDummyFootprint();
537 m_inSelect = false;
538
539 onModify();
540}
541
542
543bool PANEL_FP_PROPERTIES_3D_MODEL::pick3DModel( const wxString& aCurrentValue, FP_3DMODEL& aModel )
544{
545 PROJECT& project = m_frame->Prj();
547 FILENAME_RESOLVER* resolver = cache->GetResolver();
548 wxString directory;
549 wxString filename;
550
551 const wxString embeddedPrefix = wxString::FromUTF8( FILEEXT::KiCadUriPrefix ) + wxS( "://" );
552
553 if( !aCurrentValue.IsEmpty() && !aCurrentValue.StartsWith( embeddedPrefix ) )
554 {
555 wxString libraryBasePath;
556
557 if( std::optional<LIBRARY_TABLE_ROW*> row =
558 PROJECT_PCB::FootprintLibAdapter( &project )->GetRow( m_footprint->GetFPID().GetLibNickname() ) )
559 {
560 libraryBasePath = LIBRARY_MANAGER::GetFullURI( *row, true );
561 }
562
563 wxString resolved = resolver->ResolvePath( aCurrentValue, libraryBasePath,
564 { m_filesPanel->GetLocalFiles(), m_frame->GetBoard()->GetEmbeddedFiles() } );
565 wxFileName currentFile( resolved );
566
567 if( currentFile.FileExists() )
568 {
569 directory = currentFile.GetPath();
570 filename = currentFile.GetFullName();
571 }
572 }
573
574 if( directory.IsEmpty() && m_configuredPathChoice->GetSelection() > 0 )
575 directory = m_configuredPathChoice->GetStringSelection();
576
577 if( directory.IsEmpty() )
579
580 PCBNEW_SETTINGS* settings = GetAppSettings<PCBNEW_SETTINGS>( "pcbnew" );
581
582 if( directory.IsEmpty() && settings )
584
585 if( directory.IsEmpty() )
586 wxGetEnv( ENV_VAR::GetVersionedEnvVarName( wxS( "3DMODEL_DIR" ) ), &directory );
587
588 if( directory.IsEmpty() )
589 directory = project.GetProjectPath();
590
591 wxString wildcard;
592 wxString extensions;
593 int filterCount = 0;
594
595 if( const std::list<wxString>* filters = cache->GetFileFilters() )
596 {
597 for( const wxString& filter : *filters )
598 {
599 ++filterCount;
600
601 if( !wildcard.IsEmpty() )
602 wildcard += '|';
603
604 wildcard += filter;
605 wxString extension = filter.AfterLast( '|' );
606
607 if( extension != wxS( "*.*" ) )
608 {
609 if( !extensions.IsEmpty() )
610 extensions += ';';
611
612 extensions += extension;
613 }
614 }
615 }
616
617 if( !extensions.IsEmpty() )
618 {
619 wxString fullFilter = wxString::Format( _( "All supported files (%s)" ), extensions );
620 fullFilter << '|' << extensions << '|' << wildcard;
621 wildcard = fullFilter;
622 ++filterCount;
623 }
624
625 if( wildcard.IsEmpty() )
626 wildcard = wxFileSelectorDefaultWildcardStr;
627
628 COMMON_SETTINGS* commonSettings = Pgm().GetCommonSettings();
630 options.parent = m_parentDialog;
631 options.title = _( "Select 3D Model" );
632 options.directory = directory;
633 options.filename = filename;
634 options.wildcard = wildcard;
635 options.style = wxFD_OPEN | wxFD_FILE_MUST_EXIST;
636 options.embedFile = commonSettings && commonSettings->m_EmbedFileDefaults.model_3d;
637
638 long filterIndex = 0;
639 project.GetRString( PROJECT::VIEWER_3D_FILTER_INDEX ).ToLong( &filterIndex );
640 options.filterIndex = filterCount > 0
641 ? static_cast<int>( std::clamp( filterIndex, 0L,
642 static_cast<long>( filterCount - 1 ) ) )
643 : 0;
644
646
647 struct MODAL_SCOPE
648 {
649 explicit MODAL_SCOPE( DIALOG_SHIM* aDialog ) : dialog( aDialog ) { dialog->PrepareForModalSubDialog(); }
650 ~MODAL_SCOPE() { dialog->CleanupAfterModalSubDialog(); }
651 DIALOG_SHIM* dialog;
652 };
653
654 {
655 MODAL_SCOPE modalScope( m_parentDialog );
656 selected = ShowNativeModelFilePicker( options );
657 }
658
659 if( !selected.accepted )
660 return false;
661
662 if( commonSettings )
663 commonSettings->m_EmbedFileDefaults.model_3d = selected.embedFile;
664
665 wxFileName selectedFile( selected.path );
666 selectedFile.Normalize( FN_NORMALIZE_FLAGS | wxPATH_NORM_ENV_VARS );
667 project.SetRString( PROJECT::VIEWER_3D_PATH, selectedFile.GetPath() );
668 project.SetRString( PROJECT::VIEWER_3D_FILTER_INDEX, wxString::Format( wxS( "%d" ), selected.filterIndex ) );
669
670 if( settings )
671 settings->m_LastFootprint3dDir = selectedFile.GetPath();
672
673 if( selected.embedFile )
674 {
675 if( EMBEDDED_FILES::EMBEDDED_FILE* embedded = m_filesPanel->AddEmbeddedFile( selected.path ) )
676 aModel.m_Filename = embedded->GetLink();
677 else
678 {
679 wxMessageBox( _( "Error adding 3D model" ), _( "Error" ), wxICON_ERROR | wxOK, this );
680 return false;
681 }
682 }
683 else
684 {
685 aModel.m_Filename = resolver->ShortenPath( selectedFile.GetFullPath() );
686 }
687
688 return true;
689}
690
691
693{
694 wxCHECK_RET( aRow >= 0 && aRow < static_cast<int>( m_shapes3D_list.size() ),
695 wxS( "invalid 3D model row" ) );
696
697 m_shapes3D_list[aRow] = aModel;
698 wxString displayName = aModel.m_Filename;
699 wxString alias;
700 wxString shortPath;
701
702 if( FILENAME_RESOLVER* resolver = PROJECT_PCB::Get3DCacheManager( &m_frame->Prj() )->GetResolver();
703 resolver && resolver->SplitAlias( displayName, alias, shortPath ) )
704 {
705 displayName = alias + wxS( ":" ) + shortPath;
706 }
707
708 m_modelsGrid->SetCellValue( aRow, COL_FILENAME, displayName );
709 m_modelsGrid->SetCellValue( aRow, COL_SHOWN, aModel.m_Show ? wxS( "1" ) : wxS( "0" ) );
710 select3DModel( aRow );
711 updateValidateStatus( aRow );
712 m_previewPane->UpdateDummyFootprint();
713 onModify();
714}
715
716
718{
719 const wxString previous = m_configuredPathChoice->GetStringSelection();
720 m_configuredPathChoice->Clear();
721 m_configuredPathChoice->Append( _( "Automatic" ) );
722
724
725 if( const std::list<SEARCH_PATH>* paths = resolver->GetPaths() )
726 {
727 for( const SEARCH_PATH& path : *paths )
728 {
729 if( !path.m_Pathexp.IsEmpty() && m_configuredPathChoice->FindString( path.m_Pathexp ) == wxNOT_FOUND )
730 m_configuredPathChoice->Append( path.m_Pathexp );
731 }
732 }
733
734 const int previousIndex = m_configuredPathChoice->FindString( previous );
735 m_configuredPathChoice->SetSelection( previousIndex == wxNOT_FOUND ? 0 : previousIndex );
736}
737
738
740{
741 if( !m_modelsGrid->CommitPendingChanges() )
742 return;
743
744 const int selected = m_modelsGrid->GetGridCursorRow();
746
747 if( !pick3DModel( wxEmptyString, model ) )
748 {
749 if( selected >= 0 )
750 {
751 select3DModel( selected );
752 updateValidateStatus( selected );
753 }
754
755 return;
756 }
757
758 const int row = m_modelsGrid->GetNumberRows();
759 model.m_Show = true;
760 m_shapes3D_list.push_back( model );
761 m_modelsGrid->AppendRows( 1 );
762 set3DModelRow( row, model );
763}
764
765
767{
768 m_modelsGrid->OnAddRow(
769 [&]() -> std::pair<int, int>
770 {
772
773 model.m_Show = true;
774 m_shapes3D_list.push_back( model );
775
776 int row = m_modelsGrid->GetNumberRows();
777 m_modelsGrid->AppendRows( 1 );
778 m_modelsGrid->SetCellValue( row, COL_SHOWN, wxT( "1" ) );
779 m_modelsGrid->SetCellValue( row, COL_PROBLEM, "" );
780
781 select3DModel( row );
783 onModify();
784
785 return { row, COL_FILENAME };
786 } );
787}
788
789
791{
792 int icon = 0;
793 wxString errStr;
794 wxString filename = m_modelsGrid->GetCellValue( aRow, COL_FILENAME );
795
796 if( wxGridCellEditor* cellEditor = m_modelsGrid->GetCellEditor( aRow, COL_FILENAME ) )
797 {
798 if( cellEditor->IsCreated() && cellEditor->GetWindow()->IsShown() )
799 filename = cellEditor->GetValue();
800
801 cellEditor->DecRef();
802 }
803
804 switch( validateModelExists( filename ) )
805 {
807 icon = 0;
808 errStr = "";
809 break;
810
812 icon = wxICON_WARNING;
813 errStr = _( "No filename entered" );
814 break;
815
817 icon = wxICON_ERROR;
818 errStr = _( "Illegal filename" );
819 break;
820
822 icon = wxICON_ERROR;
823 errStr = _( "File not found" );
824 break;
825
827 icon = wxICON_ERROR;
828 errStr = _( "Unable to open file" );
829 break;
830
831 default:
832 icon = wxICON_ERROR;
833 errStr = _( "Unknown error" );
834 break;
835 }
836
837 m_modelsGrid->SetCellValue( aRow, COL_PROBLEM, errStr );
838 m_modelsGrid->SetCellRenderer( aRow, COL_PROBLEM, new GRID_CELL_STATUS_ICON_RENDERER( icon ) );
839}
840
841
843{
844 if( aFilename.empty() )
846
847 bool hasAlias = false;
849
850 if( !resolv )
852
853 if( !resolv->ValidateFileName( aFilename, hasAlias ) )
855
856 wxString libraryName = m_footprint->GetFPID().GetLibNickname();
857 wxString footprintBasePath = wxEmptyString;
858
859 std::optional<LIBRARY_TABLE_ROW*> fpRow =
860 PROJECT_PCB::FootprintLibAdapter( &m_frame->Prj() )->GetRow( libraryName );
861 if( fpRow )
862 footprintBasePath = LIBRARY_MANAGER::GetFullURI( *fpRow, true );
863
864 std::vector<const EMBEDDED_FILES*> embeddedFilesStack;
865 embeddedFilesStack.push_back( m_filesPanel->GetLocalFiles() );
866 embeddedFilesStack.push_back( m_frame->GetBoard()->GetEmbeddedFiles() );
867
868 wxString fullPath = resolv->ResolvePath( aFilename, footprintBasePath, std::move( embeddedFilesStack ) );
869
870 if( fullPath.IsEmpty() )
872
873 if( !wxFileName::IsFileReadable( fullPath ) )
875
877}
878
879
880void PANEL_FP_PROPERTIES_3D_MODEL::Cfg3DPath( wxCommandEvent& event )
881{
882 DIALOG_CONFIGURE_PATHS dlg( this );
883
884 if( dlg.ShowQuasiModal() == wxID_OK )
885 {
887 m_previewPane->UpdateDummyFootprint();
888 }
889}
890
891
892void PANEL_FP_PROPERTIES_3D_MODEL::OnUpdateUI( wxUpdateUIEvent& event )
893{
894 m_button3DShapeRemove->Enable( m_modelsGrid->GetNumberRows() > 0 );
895}
896
897
899{
900 if( DIALOG_SHIM* dlg = dynamic_cast<DIALOG_SHIM*>( wxGetTopLevelParent( this ) ) )
901 dlg->OnModify();
902}
903
904
906{
907 postCustomPanelShownEventWithPredicate( static_cast<int>( aEvent.IsShown() ) );
908 aEvent.Skip();
909}
910
911
912void PANEL_FP_PROPERTIES_3D_MODEL::onSize( wxSizeEvent& aEvent )
913{
914 aEvent.Skip();
916}
917
918
920{
921 const int page = GetSize().y;
922
923 if( m_sashPositioned || page <= 0 || !m_splitter1->IsSplit() )
924 return;
925
926 const int rowHeight = m_modelsGrid->GetDefaultRowSize();
927 const int chrome = m_modelsGrid->GetColLabelSize() + 4;
928 const int minGrid = 3 * rowHeight + chrome;
929 const int maxGrid = std::max( minGrid, page / 3 );
930 const int grid = std::clamp( m_modelsGrid->GetNumberRows() * rowHeight + chrome, minGrid, maxGrid );
931
932 // Everything else in the upper pane is fixed height, so the sash is that plus the grid.
933 m_modelsGrid->SetMinSize( wxSize( -1, minGrid ) );
934 const int sash = m_upperPanel->GetSizer()->GetMinSize().y - minGrid + grid;
935
936 // An early layout pass reports a page far shorter than the final one. Wait for a size
937 // that leaves the preview a usable pane before locking the split in.
938 if( page - sash < m_splitter1->GetMinimumPaneSize() )
939 return;
940
941 m_splitter1->SetSashPosition( sash );
942 m_sashPositioned = true;
943}
944
945
947{
948 postCustomPanelShownEventWithPredicate( aEvent.GetActive() && m_previewPane->IsShownOnScreen() );
949 aEvent.Skip();
950}
951
952
954{
955 wxNotebook* notebook = dynamic_cast<wxNotebook*>( aEvent.GetEventObject() );
956
957 if( notebook && notebook == GetParent() )
958 {
959 // Native notebooks do not consistently emit show events when hiding their pages.
960 const int selected = aEvent.GetSelection();
962 && selected < (int) notebook->GetPageCount() && notebook->GetPage( selected ) == this );
963 }
964
965 aEvent.Skip();
966}
967
968
970{
971 wxCommandEvent event( wxCUSTOM_PANEL_SHOWN_EVENT );
972 event.SetEventObject( m_previewPane );
973 event.SetInt( static_cast<int>( predicate ) );
974 m_previewPane->ProcessWindowEvent( event );
975}
976
977
984
985
987{
989 onModify();
990 event.Skip();
991}
992
993
995{
998 onModify();
999 event.Skip();
1000}
1001
1002
1004{
1005 bool enabled = m_enableExtrusionCheckbox->GetValue();
1006 m_showExtrusionCheckbox->Enable( enabled );
1007 m_componentHeightCtrl->Enable( enabled );
1008 m_standoffHeightCtrl->Enable( enabled );
1009 m_extrusionLayerChoice->Enable( enabled );
1010 m_extrusionColorSwatch->Enable( enabled );
1011 m_extrusionMaterialChoice->Enable( enabled );
1012 m_buttonExportExtruded->Enable( enabled );
1013
1014 if( enabled )
1015 {
1016 FOOTPRINT* dummyFp = m_previewPane->GetDummyFootprint();
1017
1018 if( dummyFp )
1019 m_previewPane->SetExtrusionTransformMode( &dummyFp->EnsureExtrudedBody() );
1020 }
1021 else
1022 {
1023 m_previewPane->SetExtrusionTransformMode( nullptr );
1024 }
1025}
1026
1027
1028bool PANEL_FP_PROPERTIES_3D_MODEL::readExtrusionHeights( const wxString& aTitle, double& aHeight, double& aStandoff )
1029{
1030 wxString heightText = m_componentHeightCtrl->GetValue();
1031 wxString standoffText = m_standoffHeightCtrl->GetValue();
1032
1033 // An empty field reads as zero.
1034 aHeight = 0.0;
1035 aStandoff = 0.0;
1036
1037 if( !heightText.IsEmpty() && !heightText.ToDouble( &aHeight ) )
1038 {
1039 wxMessageBox( _( "Overall height is not a valid number." ), aTitle, wxOK | wxICON_WARNING, this );
1040 return false;
1041 }
1042
1043 if( !standoffText.IsEmpty() && !standoffText.ToDouble( &aStandoff ) )
1044 {
1045 wxMessageBox( _( "Standoff height is not a valid number." ), aTitle, wxOK | wxICON_WARNING, this );
1046 return false;
1047 }
1048
1049 return true;
1050}
1051
1052
1054{
1055 FOOTPRINT* dummyFp = m_previewPane->GetDummyFootprint();
1056
1057 if( !dummyFp || m_extrusionLayers.empty() )
1058 return;
1059
1060 double compHeight = 0.0;
1061 double standoff = 0.0;
1062 m_componentHeightCtrl->GetValue().ToDouble( &compHeight );
1063 m_standoffHeightCtrl->GetValue().ToDouble( &standoff );
1064
1065 int sel = m_extrusionLayerChoice->GetSelection();
1066
1067 EXTRUDED_3D_BODY& body = dummyFp->EnsureExtrudedBody();
1068 body.m_height = pcbIUScale.mmToIU( compHeight );
1069 body.m_standoff = pcbIUScale.mmToIU( standoff );
1070 body.m_layer = m_extrusionLayers[sel];
1072 body.m_material = static_cast<EXTRUSION_MATERIAL>( m_extrusionMaterialChoice->GetSelection() );
1073 body.m_show = m_enableExtrusionCheckbox->GetValue() && m_showExtrusionCheckbox->GetValue();
1074
1075 m_previewPane->UpdateDummyFootprint( true );
1076}
1077
1078
1080{
1082 {
1083 EXTRUSION_MATERIAL mat = static_cast<EXTRUSION_MATERIAL>( m_extrusionMaterialChoice->GetSelection() );
1084 m_extrusionColorSwatch->SetSwatchColor( EXTRUDED_3D_BODY::GetDefaultColor( mat ), false );
1085 }
1086
1088 onModify();
1089 event.Skip();
1090}
1091
1092
1094{
1095 double height;
1096 double standoff;
1097
1098 if( !readExtrusionHeights( _( "Export Extruded Body" ), height, standoff ) )
1099 return;
1100
1101 if( height <= 0.0 )
1102 {
1103 wxMessageBox( _( "Component height must be greater than zero." ), _( "Export Extruded Body" ),
1104 wxOK | wxICON_WARNING, this );
1105 return;
1106 }
1107
1108 int layerSel = m_extrusionLayerChoice->GetSelection();
1109
1110 SHAPE_POLY_SET outline;
1111 bool gotOutline =
1112 GetExtrusionOutline( m_footprint, outline, m_extrusionLayers[layerSel] ) && outline.OutlineCount() > 0;
1113
1114 if( !gotOutline )
1115 {
1116 wxMessageBox( _( "No extrusion outline could be generated for this footprint." ), _( "Export Extruded Body" ),
1117 wxOK | wxICON_WARNING, this );
1118 return;
1119 }
1120
1121 wxString defaultName = m_footprint->GetReference() + wxT( "_extruded" );
1122
1123 wxFileDialog dlg( this, _( "Export Extruded 3D Body" ), wxEmptyString, defaultName,
1124 _( "STEP files" ) + wxT( " (*.step)|*.step|" ) + _( "GLB files" ) + wxT( " (*.glb)|*.glb|" )
1125 + _( "STL files" ) + wxT( " (*.stl)|*.stl|" ) + _( "BREP files" )
1126 + wxT( " (*.brep)|*.brep" ),
1127 wxFD_SAVE | wxFD_OVERWRITE_PROMPT );
1128
1129 if( dlg.ShowModal() == wxID_CANCEL )
1130 return;
1131
1132 wxString path = dlg.GetPath();
1133 int filterIdx = dlg.GetFilterIndex();
1134
1135 bool bottom = m_footprint->IsFlipped();
1136 VECTOR2D origin( m_footprint->GetPosition().x, m_footprint->GetPosition().y );
1137
1138 FOOTPRINT* dummyFp = m_previewPane->GetDummyFootprint();
1139 const EXTRUDED_3D_BODY* body = dummyFp ? dummyFp->GetExtrudedBody() : nullptr;
1140
1141 double bodyStandoff = standoff;
1142
1143 if( body )
1144 {
1145 ApplyExtrusionTransform( outline, body, m_footprint->GetPosition() );
1146
1147 double bodyThickness = ( height - standoff ) * body->m_scale.z;
1148 bodyStandoff = standoff + body->m_offset.z;
1149 height = bodyStandoff + bodyThickness;
1150 }
1151
1152 EXTRUSION_MATERIAL material = static_cast<EXTRUSION_MATERIAL>( m_extrusionMaterialChoice->GetSelection() );
1153
1154 KIGFX::COLOR4D c = m_extrusionColorSwatch->GetSwatchColor();
1155
1157 c = EXTRUDED_3D_BODY::GetDefaultColor( material );
1158
1159 uint32_t colorKey = EXTRUDED_3D_BODY::PackColorKey( c );
1160
1162 STEP_PCB_MODEL model( wxT( "extruded_body" ), &reporter );
1163
1164 if( !model.AddExtrudedBody( outline, bottom, bodyStandoff, height, origin, colorKey, material,
1165 m_footprint->GetReference() ) )
1166 {
1167 wxMessageBox( _( "Failed to create extruded body geometry." ), _( "Export Extruded Body" ), wxOK | wxICON_ERROR,
1168 this );
1169 return;
1170 }
1171
1172 if( standoff > 0.0 )
1173 model.AddExtrudedPins( m_footprint, body, bottom, bodyStandoff, origin );
1174
1175 SHAPE_POLY_SET boardOutline( outline );
1176 model.CreatePCB( boardOutline, origin, false );
1177
1178 bool ok = false;
1179
1180 switch( filterIdx )
1181 {
1182 case 0: ok = model.WriteSTEP( path, true, false ); break;
1183 case 1: ok = model.WriteGLTF( path ); break;
1184 case 2: ok = model.WriteSTL( path ); break;
1185 case 3: ok = model.WriteBREP( path ); break;
1186 }
1187
1188 if( !ok )
1189 {
1190 wxMessageBox( _( "Failed to write file." ), _( "Export Extruded Body" ), wxOK | wxICON_ERROR, this );
1191 }
1192}
void ApplyExtrusionTransform(SHAPE_POLY_SET &aOutline, const EXTRUDED_3D_BODY *aBody, const VECTOR2I &aFpPos)
Apply 2D extrusion transforms (rotation, scale, offset) to an outline.
bool GetExtrusionOutline(const FOOTPRINT *aFootprint, SHAPE_POLY_SET &aOutline, PCB_LAYER_ID aLayerOverride)
Get the extrusion outline polygon for a footprint in board coordinates.
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:128
wxBitmapBundle KiBitmapBundle(BITMAPS aBitmap, int aMinHeight)
Definition bitmap.cpp:106
EMBED_FILE_DEFAULTS m_EmbedFileDefaults
Dialog helper object to sit in the inheritance tree between wxDialog and any class written by wxFormB...
Definition dialog_shim.h:80
KIGFX::COLOR4D m_color
Definition footprint.h:121
VECTOR3D m_offset
Definition footprint.h:127
PCB_LAYER_ID m_layer
Definition footprint.h:120
static KIGFX::COLOR4D GetDefaultColor(EXTRUSION_MATERIAL aMaterial)
Definition footprint.h:129
VECTOR3D m_rotation
Definition footprint.h:126
static uint32_t PackColorKey(const KIGFX::COLOR4D &aColor)
Definition footprint.h:148
VECTOR3D m_scale
Definition footprint.h:125
EXTRUSION_MATERIAL m_material
Definition footprint.h:122
Provide an extensible class to resolve 3D model paths.
bool ValidateFileName(const wxString &aFileName, bool &hasAlias) const
Return true if the given path is a valid aliased relative path.
wxString ResolvePath(const wxString &aFileName, const wxString &aWorkingPath, std::vector< const EMBEDDED_FILES * > aEmbeddedFilesStack)
Determine the full path of the given file name.
void SetProgramBase(PGM_BASE *aBase)
Set a pointer to the application's PGM_BASE instance used to extract the local env vars.
const std::list< SEARCH_PATH > * GetPaths() const
Return a pointer to the internal path list; the items in:load.
wxString ShortenPath(const wxString &aFullPathName)
Produce a relative path based on the existing search directories or returns the same path if the path...
const EXTRUDED_3D_BODY * GetExtrudedBody() const
Definition footprint.h:429
bool HasExtrudedBody() const
Definition footprint.h:428
EXTRUDED_3D_BODY & EnsureExtrudedBody()
wxString m_Filename
The 3D shape filename in 3D library.
Definition footprint.h:186
bool m_Show
Include model in rendering.
Definition footprint.h:187
A cell editor which runs a provided function when the grid cell button is clicked.
Add mouse and command handling (such as cut, copy, and paste) to a WX_GRID instance.
Definition grid_tricks.h:57
void SetTooltipEnable(int aCol, bool aEnable=true)
Enable the tooltip for a column.
Definition grid_tricks.h:71
A color representation with 4 components: red, green, blue, alpha.
Definition color4d.h:101
static const COLOR4D UNSPECIFIED
For legacy support; used as a value to indicate color hasn't been set yet.
Definition color4d.h:400
std::optional< LIBRARY_TABLE_ROW * > GetRow(const wxString &aNickname, LIBRARY_TABLE_SCOPE aScope=LIBRARY_TABLE_SCOPE::BOTH) const
Like LIBRARY_MANAGER::GetRow but filtered to the LIBRARY_TABLE_TYPE of this adapter.
std::optional< wxString > GetFullURI(LIBRARY_TABLE_TYPE aType, const wxString &aNickname, bool aSubstituted=false)
Return the full location specifying URI for the LIB, either in original UI form or in environment var...
A singleton reporter that reports to nowhere.
Definition reporter.h:267
PANEL_FP_PROPERTIES_3D_MODEL_BASE(wxWindow *parent, wxWindowID id=wxID_ANY, const wxPoint &pos=wxDefaultPosition, const wxSize &size=wxSize(778, 420), long style=wxTAB_TRAVERSAL, const wxString &name=wxEmptyString)
void on3DModelCellChanging(wxGridEvent &aEvent)
void OnUpdateUI(wxUpdateUIEvent &event) override
void Cfg3DPath(wxCommandEvent &event) override
void OnEnableExtrusion(wxCommandEvent &event) override
bool pick3DModel(const wxString &aCurrentValue, FP_3DMODEL &aModel)
void onNotebookPageChanged(wxBookCtrlEvent &aEvent)
void OnRemove3DModel(wxCommandEvent &event) override
virtual void onShowEvent(wxShowEvent &aEvent)
void On3DModelCellChanged(wxGridEvent &aEvent) override
virtual void onDialogActivateEvent(wxActivateEvent &aEvent)
std::vector< PCB_LAYER_ID > m_extrusionLayers
void setDefaultSashPosition()
Split the page so the model list shows its rows, never fewer than three and never more than a third o...
PANEL_FP_PROPERTIES_3D_MODEL(PCB_BASE_EDIT_FRAME *aFrame, FOOTPRINT *aFootprint, DIALOG_SHIM *aDialogParent, PANEL_EMBEDDED_FILES *aFilesPanel, wxWindow *aParent)
void postCustomPanelShownEventWithPredicate(bool predicate)
void OnExportExtrudedModel(wxCommandEvent &event) override
MODEL_VALIDATE_ERRORS validateModelExists(const wxString &aFilename)
void OnAdd3DModel(wxCommandEvent &event) override
void OnAdd3DRow(wxCommandEvent &event) override
void onExtrusionColorChanged(wxCommandEvent &event)
void onExtrusionControlChanged(wxCommandEvent &event)
bool readExtrusionHeights(const wxString &aTitle, double &aHeight, double &aStandoff)
void set3DModelRow(int aRow, const FP_3DMODEL &aModel)
void onExtrusionMaterialChanged(wxCommandEvent &event)
void On3DModelSelected(wxGridEvent &) override
wxString m_LastFootprint3dDir
Common, abstract interface for edit frames.
virtual COMMON_SETTINGS * GetCommonSettings() const
Definition pgm_base.cpp:546
static S3D_CACHE * Get3DCacheManager(PROJECT *aProject, bool updateProjDir=false)
Return a pointer to an instance of the 3D cache manager.
static FILENAME_RESOLVER * Get3DFilenameResolver(PROJECT *aProject)
Accessor for 3D path resolver.
static FOOTPRINT_LIBRARY_ADAPTER * FootprintLibAdapter(PROJECT *aProject)
Container for project specific data.
Definition project.h:63
@ VIEWER_3D_FILTER_INDEX
Definition project.h:221
@ VIEWER_3D_PATH
Definition project.h:220
Cache for storing the 3D shapes.
Definition 3d_cache.h:53
void FlushCache(bool closePlugins=true)
Free all data in the cache and by default closes all plugins.
Definition 3d_cache.cpp:560
std::list< wxString > const * GetFileFilters() const
Return the list of file filters retrieved from the plugins.
Definition 3d_cache.cpp:554
FILENAME_RESOLVER * GetResolver() noexcept
Definition 3d_cache.cpp:548
Represent a set of closed polygons.
int OutlineCount() const
Return the number of outlines in the set.
#define _(s)
Declaration of the eda_3d_viewer class.
Functions related to environment variables, including help functions.
EXTRUSION_MATERIAL
Definition footprint.h:106
static const std::string KiCadUriPrefix
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:56
@ F_CrtYd
Definition layer_ids.h:112
@ UNSELECTED_LAYER
Definition layer_ids.h:58
@ F_Fab
Definition layer_ids.h:115
@ F_SilkS
Definition layer_ids.h:96
@ UNDEFINED_LAYER
Definition layer_ids.h:57
KICOMMON_API wxString GetVersionedEnvVarName(const wxString &aBaseName)
Construct a versioned environment variable based on this KiCad major version.
Definition env_vars.cpp:78
void EllipsizeChoiceBox(wxChoice *aChoice)
Configure a wxChoice control to ellipsize the shown text in the button with the ellipses placed at th...
Definition wxgtk/ui.cpp:326
NATIVE_MODEL_FILE_PICKER_RESULT ShowNativeModelFilePicker(const NATIVE_MODEL_FILE_PICKER_OPTIONS &aOptions)
wxDEFINE_EVENT(wxCUSTOM_PANEL_SHOWN_EVENT, wxCommandEvent)
PGM_BASE & Pgm()
The global program "get" accessor.
see class PGM_BASE
static const std::vector< std::string > filters
Definition plugin.cpp:64
static const std::vector< std::string > extensions
Definition plugin.cpp:63
T * GetAppSettings(const char *aFilename)
std::string path
IbisParser parser & reporter
KIBIS_MODEL * model
VECTOR3I res
VECTOR2< double > VECTOR2D
Definition vector2d.h:707
#define FN_NORMALIZE_FLAGS
Default flags to pass to wxFileName::Normalize().
Definition wx_filename.h:35