Commit deb3d54c authored by Cirilo Bernado's avatar Cirilo Bernado Committed by jean-pierre charras

Adds basic IDF3 export (board and cutouts / holes only)

parents c2648237 0b853c5a
...@@ -977,6 +977,12 @@ public: ...@@ -977,6 +977,12 @@ public:
bool ExportVRML_File( const wxString & aFullFileName, double aMMtoWRMLunit, bool ExportVRML_File( const wxString & aFullFileName, double aMMtoWRMLunit,
bool aExport3DFiles, const wxString & a3D_Subdir ); bool aExport3DFiles, const wxString & a3D_Subdir );
/**
* Function ExportToIDF3
* will export the current BOARD to a IDFv3 board and lib files.
*/
void ExportToIDF3( wxCommandEvent& event );
/** /**
* Function ExporttoSPECCTRA * Function ExporttoSPECCTRA
* will export the current BOARD to a specctra dsn file. See * will export the current BOARD to a specctra dsn file. See
......
...@@ -55,6 +55,8 @@ set( PCBNEW_DIALOGS ...@@ -55,6 +55,8 @@ set( PCBNEW_DIALOGS
dialogs/dialog_edit_module_text.cpp dialogs/dialog_edit_module_text.cpp
dialogs/dialog_edit_module_text_base.cpp dialogs/dialog_edit_module_text_base.cpp
dialogs/dialog_exchange_modules_base.cpp dialogs/dialog_exchange_modules_base.cpp
dialogs/dialog_export_idf.cpp
dialogs/dialog_export_idf_base.cpp
dialogs/dialog_export_vrml_base.cpp dialogs/dialog_export_vrml_base.cpp
dialogs/dialog_export_vrml.cpp dialogs/dialog_export_vrml.cpp
dialogs/dialog_find_base.cpp dialogs/dialog_find_base.cpp
...@@ -173,6 +175,7 @@ set( PCBNEW_CLASS_SRCS ...@@ -173,6 +175,7 @@ set( PCBNEW_CLASS_SRCS
event_handlers_tracks_vias_sizes.cpp event_handlers_tracks_vias_sizes.cpp
export_d356.cpp export_d356.cpp
export_gencad.cpp export_gencad.cpp
export_idf.cpp
export_vrml.cpp export_vrml.cpp
files.cpp files.cpp
gen_drill_report_files.cpp gen_drill_report_files.cpp
...@@ -183,6 +186,7 @@ set( PCBNEW_CLASS_SRCS ...@@ -183,6 +186,7 @@ set( PCBNEW_CLASS_SRCS
hotkeys.cpp hotkeys.cpp
hotkeys_board_editor.cpp hotkeys_board_editor.cpp
hotkeys_module_editor.cpp hotkeys_module_editor.cpp
idf.cpp
initpcb.cpp initpcb.cpp
layer_widget.cpp layer_widget.cpp
librairi.cpp librairi.cpp
......
/**
* @file dialog_export_idf.cpp
*/
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2013 Cirilo Bernardo
*
* 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 2
* 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, you may find one here:
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include <wxPcbStruct.h>
#include <appl_wxstruct.h>
#include <pcbnew.h>
#include <class_board.h>
// IDF export header generated by wxFormBuilder
#include <dialog_export_idf_base.h>
#define OPTKEY_IDF_THOU wxT( "IDFExportThou" )
bool Export_IDF3( BOARD *aPcb, const wxString & aFullFileName, double aUseThou );
class DIALOG_EXPORT_IDF3: public DIALOG_EXPORT_IDF3_BASE
{
private:
PCB_EDIT_FRAME* m_parent;
wxConfig* m_config;
bool m_idfThouOpt; // remember last preference for units in THOU
void OnCancelClick( wxCommandEvent& event )
{
EndModal( wxID_CANCEL );
}
void OnOkClick( wxCommandEvent& event )
{
EndModal( wxID_OK );
}
public:
DIALOG_EXPORT_IDF3( PCB_EDIT_FRAME* parent ) :
DIALOG_EXPORT_IDF3_BASE( parent )
{
m_parent = parent;
m_config = wxGetApp().GetSettings();
SetFocus();
m_idfThouOpt = false;
m_config->Read( OPTKEY_IDF_THOU, &m_idfThouOpt );
m_chkThou->SetValue( m_idfThouOpt );
GetSizer()->SetSizeHints( this );
Centre();
}
~DIALOG_EXPORT_IDF3()
{
m_idfThouOpt = m_chkThou->GetValue();
m_config->Write( OPTKEY_IDF_THOU, m_idfThouOpt );
}
bool GetThouOption()
{
return m_chkThou->GetValue();
}
wxFilePickerCtrl* FilePicker()
{
return m_filePickerIDF;
}
};
/**
* Function OnExportIDF3
* will export the current BOARD to IDF board and lib files.
*/
void PCB_EDIT_FRAME::ExportToIDF3( wxCommandEvent& event )
{
wxFileName fn;
// Build default file name
fn = GetBoard()->GetFileName();
fn.SetExt( wxT( "emn" ) );
DIALOG_EXPORT_IDF3 dlg( this );
dlg.FilePicker()->SetPath( fn.GetFullPath() );
if ( dlg.ShowModal() != wxID_OK )
return;
bool thou = dlg.GetThouOption();
wxBusyCursor dummy;
wxString fullFilename = dlg.FilePicker()->GetPath();
if ( !Export_IDF3( GetBoard(), fullFilename, thou ) )
{
wxString msg = _("Unable to create ") + fullFilename;
wxMessageBox( msg );
return;
}
}
///////////////////////////////////////////////////////////////////////////
// C++ code generated with wxFormBuilder (version Oct 8 2012)
// http://www.wxformbuilder.org/
//
// PLEASE DO "NOT" EDIT THIS FILE!
///////////////////////////////////////////////////////////////////////////
#include "dialog_export_idf_base.h"
///////////////////////////////////////////////////////////////////////////
DIALOG_EXPORT_IDF3_BASE::DIALOG_EXPORT_IDF3_BASE( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : DIALOG_SHIM( parent, id, title, pos, size, style )
{
this->SetSizeHints( wxDefaultSize, wxDefaultSize );
wxBoxSizer* bSizerIDFFile;
bSizerIDFFile = new wxBoxSizer( wxVERTICAL );
m_txtBrdFile = new wxStaticText( this, wxID_ANY, wxT("IDF Board file"), wxDefaultPosition, wxDefaultSize, 0 );
m_txtBrdFile->Wrap( -1 );
bSizerIDFFile->Add( m_txtBrdFile, 0, wxALL, 5 );
m_filePickerIDF = new wxFilePickerCtrl( this, wxID_ANY, wxEmptyString, wxT("Select a board file"), wxT("*.emn"), wxDefaultPosition, wxDefaultSize, wxFLP_OVERWRITE_PROMPT|wxFLP_SAVE|wxFLP_USE_TEXTCTRL );
m_filePickerIDF->SetMinSize( wxSize( 420,30 ) );
bSizerIDFFile->Add( m_filePickerIDF, 0, wxALL, 5 );
m_chkThou = new wxCheckBox( this, wxID_ANY, wxT("unit: THOU"), wxDefaultPosition, wxDefaultSize, 0 );
bSizerIDFFile->Add( m_chkThou, 0, wxALL, 5 );
m_sdbSizer1 = new wxStdDialogButtonSizer();
m_sdbSizer1OK = new wxButton( this, wxID_OK );
m_sdbSizer1->AddButton( m_sdbSizer1OK );
m_sdbSizer1Cancel = new wxButton( this, wxID_CANCEL );
m_sdbSizer1->AddButton( m_sdbSizer1Cancel );
m_sdbSizer1->Realize();
bSizerIDFFile->Add( m_sdbSizer1, 1, wxEXPAND, 5 );
this->SetSizer( bSizerIDFFile );
this->Layout();
this->Centre( wxBOTH );
}
DIALOG_EXPORT_IDF3_BASE::~DIALOG_EXPORT_IDF3_BASE()
{
}
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<wxFormBuilder_Project>
<FileVersion major="1" minor="11" />
<object class="Project" expanded="1">
<property name="class_decoration"></property>
<property name="code_generation">C++</property>
<property name="disconnect_events">1</property>
<property name="disconnect_mode">source_name</property>
<property name="disconnect_php_events">0</property>
<property name="disconnect_python_events">0</property>
<property name="embedded_files_path">res</property>
<property name="encoding">UTF-8</property>
<property name="event_generation">connect</property>
<property name="file">dialog_export_idf_base</property>
<property name="first_id">1000</property>
<property name="help_provider">none</property>
<property name="internationalize">0</property>
<property name="name">dialog_export_idf3_base</property>
<property name="namespace"></property>
<property name="path">.</property>
<property name="precompiled_header"></property>
<property name="relative_path">1</property>
<property name="skip_php_events">1</property>
<property name="skip_python_events">1</property>
<property name="use_enum">1</property>
<property name="use_microsoft_bom">0</property>
<object class="Dialog" expanded="1">
<property name="aui_managed">0</property>
<property name="aui_manager_style">wxAUI_MGR_DEFAULT</property>
<property name="bg"></property>
<property name="center">wxBOTH</property>
<property name="context_help"></property>
<property name="context_menu">1</property>
<property name="enabled">1</property>
<property name="event_handler">impl_virtual</property>
<property name="extra_style"></property>
<property name="fg"></property>
<property name="font"></property>
<property name="hidden">0</property>
<property name="id">wxID_ANY</property>
<property name="maximum_size"></property>
<property name="minimum_size"></property>
<property name="name">DIALOG_EXPORT_IDF3_BASE</property>
<property name="pos"></property>
<property name="size">458,177</property>
<property name="style">wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER</property>
<property name="subclass">DIALOG_SHIM; dialog_shim.h</property>
<property name="title">Export IDFv3</property>
<property name="tooltip"></property>
<property name="window_extra_style"></property>
<property name="window_name"></property>
<property name="window_style"></property>
<event name="OnActivate"></event>
<event name="OnActivateApp"></event>
<event name="OnAuiFindManager"></event>
<event name="OnAuiPaneButton"></event>
<event name="OnAuiPaneClose"></event>
<event name="OnAuiPaneMaximize"></event>
<event name="OnAuiPaneRestore"></event>
<event name="OnAuiRender"></event>
<event name="OnChar"></event>
<event name="OnClose"></event>
<event name="OnEnterWindow"></event>
<event name="OnEraseBackground"></event>
<event name="OnHibernate"></event>
<event name="OnIconize"></event>
<event name="OnIdle"></event>
<event name="OnInitDialog"></event>
<event name="OnKeyDown"></event>
<event name="OnKeyUp"></event>
<event name="OnKillFocus"></event>
<event name="OnLeaveWindow"></event>
<event name="OnLeftDClick"></event>
<event name="OnLeftDown"></event>
<event name="OnLeftUp"></event>
<event name="OnMiddleDClick"></event>
<event name="OnMiddleDown"></event>
<event name="OnMiddleUp"></event>
<event name="OnMotion"></event>
<event name="OnMouseEvents"></event>
<event name="OnMouseWheel"></event>
<event name="OnPaint"></event>
<event name="OnRightDClick"></event>
<event name="OnRightDown"></event>
<event name="OnRightUp"></event>
<event name="OnSetFocus"></event>
<event name="OnSize"></event>
<event name="OnUpdateUI"></event>
<object class="wxBoxSizer" expanded="1">
<property name="minimum_size"></property>
<property name="name">bSizerIDFFile</property>
<property name="orient">wxVERTICAL</property>
<property name="permission">none</property>
<object class="sizeritem" expanded="1">
<property name="border">5</property>
<property name="flag">wxALL</property>
<property name="proportion">0</property>
<object class="wxStaticText" expanded="1">
<property name="BottomDockable">1</property>
<property name="LeftDockable">1</property>
<property name="RightDockable">1</property>
<property name="TopDockable">1</property>
<property name="aui_layer"></property>
<property name="aui_name"></property>
<property name="aui_position"></property>
<property name="aui_row"></property>
<property name="best_size"></property>
<property name="bg"></property>
<property name="caption"></property>
<property name="caption_visible">1</property>
<property name="center_pane">0</property>
<property name="close_button">1</property>
<property name="context_help"></property>
<property name="context_menu">1</property>
<property name="default_pane">0</property>
<property name="dock">Dock</property>
<property name="dock_fixed">0</property>
<property name="docking">Left</property>
<property name="enabled">1</property>
<property name="fg"></property>
<property name="floatable">1</property>
<property name="font"></property>
<property name="gripper">0</property>
<property name="hidden">0</property>
<property name="id">wxID_ANY</property>
<property name="label">IDF Board file</property>
<property name="max_size"></property>
<property name="maximize_button">0</property>
<property name="maximum_size"></property>
<property name="min_size"></property>
<property name="minimize_button">0</property>
<property name="minimum_size"></property>
<property name="moveable">1</property>
<property name="name">m_txtBrdFile</property>
<property name="pane_border">1</property>
<property name="pane_position"></property>
<property name="pane_size"></property>
<property name="permission">protected</property>
<property name="pin_button">1</property>
<property name="pos"></property>
<property name="resize">Resizable</property>
<property name="show">1</property>
<property name="size"></property>
<property name="style"></property>
<property name="subclass"></property>
<property name="toolbar_pane">0</property>
<property name="tooltip"></property>
<property name="window_extra_style"></property>
<property name="window_name"></property>
<property name="window_style"></property>
<property name="wrap">-1</property>
<event name="OnChar"></event>
<event name="OnEnterWindow"></event>
<event name="OnEraseBackground"></event>
<event name="OnKeyDown"></event>
<event name="OnKeyUp"></event>
<event name="OnKillFocus"></event>
<event name="OnLeaveWindow"></event>
<event name="OnLeftDClick"></event>
<event name="OnLeftDown"></event>
<event name="OnLeftUp"></event>
<event name="OnMiddleDClick"></event>
<event name="OnMiddleDown"></event>
<event name="OnMiddleUp"></event>
<event name="OnMotion"></event>
<event name="OnMouseEvents"></event>
<event name="OnMouseWheel"></event>
<event name="OnPaint"></event>
<event name="OnRightDClick"></event>
<event name="OnRightDown"></event>
<event name="OnRightUp"></event>
<event name="OnSetFocus"></event>
<event name="OnSize"></event>
<event name="OnUpdateUI"></event>
</object>
</object>
<object class="sizeritem" expanded="1">
<property name="border">5</property>
<property name="flag">wxALL</property>
<property name="proportion">0</property>
<object class="wxFilePickerCtrl" expanded="1">
<property name="BottomDockable">1</property>
<property name="LeftDockable">1</property>
<property name="RightDockable">1</property>
<property name="TopDockable">1</property>
<property name="aui_layer"></property>
<property name="aui_name"></property>
<property name="aui_position"></property>
<property name="aui_row"></property>
<property name="best_size"></property>
<property name="bg"></property>
<property name="caption"></property>
<property name="caption_visible">1</property>
<property name="center_pane">0</property>
<property name="close_button">1</property>
<property name="context_help"></property>
<property name="context_menu">1</property>
<property name="default_pane">0</property>
<property name="dock">Dock</property>
<property name="dock_fixed">0</property>
<property name="docking">Left</property>
<property name="enabled">1</property>
<property name="fg"></property>
<property name="floatable">1</property>
<property name="font"></property>
<property name="gripper">0</property>
<property name="hidden">0</property>
<property name="id">wxID_ANY</property>
<property name="max_size"></property>
<property name="maximize_button">0</property>
<property name="maximum_size"></property>
<property name="message">Select a board file</property>
<property name="min_size"></property>
<property name="minimize_button">0</property>
<property name="minimum_size">420,30</property>
<property name="moveable">1</property>
<property name="name">m_filePickerIDF</property>
<property name="pane_border">1</property>
<property name="pane_position"></property>
<property name="pane_size"></property>
<property name="permission">protected</property>
<property name="pin_button">1</property>
<property name="pos"></property>
<property name="resize">Resizable</property>
<property name="show">1</property>
<property name="size"></property>
<property name="style">wxFLP_OVERWRITE_PROMPT|wxFLP_SAVE|wxFLP_USE_TEXTCTRL</property>
<property name="subclass"></property>
<property name="toolbar_pane">0</property>
<property name="tooltip"></property>
<property name="validator_data_type"></property>
<property name="validator_style">wxFILTER_NONE</property>
<property name="validator_type">wxDefaultValidator</property>
<property name="validator_variable"></property>
<property name="value"></property>
<property name="wildcard">*.emn</property>
<property name="window_extra_style"></property>
<property name="window_name"></property>
<property name="window_style"></property>
<event name="OnChar"></event>
<event name="OnEnterWindow"></event>
<event name="OnEraseBackground"></event>
<event name="OnFileChanged"></event>
<event name="OnKeyDown"></event>
<event name="OnKeyUp"></event>
<event name="OnKillFocus"></event>
<event name="OnLeaveWindow"></event>
<event name="OnLeftDClick"></event>
<event name="OnLeftDown"></event>
<event name="OnLeftUp"></event>
<event name="OnMiddleDClick"></event>
<event name="OnMiddleDown"></event>
<event name="OnMiddleUp"></event>
<event name="OnMotion"></event>
<event name="OnMouseEvents"></event>
<event name="OnMouseWheel"></event>
<event name="OnPaint"></event>
<event name="OnRightDClick"></event>
<event name="OnRightDown"></event>
<event name="OnRightUp"></event>
<event name="OnSetFocus"></event>
<event name="OnSize"></event>
<event name="OnUpdateUI"></event>
</object>
</object>
<object class="sizeritem" expanded="1">
<property name="border">5</property>
<property name="flag">wxALL</property>
<property name="proportion">0</property>
<object class="wxCheckBox" expanded="1">
<property name="BottomDockable">1</property>
<property name="LeftDockable">1</property>
<property name="RightDockable">1</property>
<property name="TopDockable">1</property>
<property name="aui_layer"></property>
<property name="aui_name"></property>
<property name="aui_position"></property>
<property name="aui_row"></property>
<property name="best_size"></property>
<property name="bg"></property>
<property name="caption"></property>
<property name="caption_visible">1</property>
<property name="center_pane">0</property>
<property name="checked">0</property>
<property name="close_button">1</property>
<property name="context_help"></property>
<property name="context_menu">1</property>
<property name="default_pane">0</property>
<property name="dock">Dock</property>
<property name="dock_fixed">0</property>
<property name="docking">Left</property>
<property name="enabled">1</property>
<property name="fg"></property>
<property name="floatable">1</property>
<property name="font"></property>
<property name="gripper">0</property>
<property name="hidden">0</property>
<property name="id">wxID_ANY</property>
<property name="label">unit: THOU</property>
<property name="max_size"></property>
<property name="maximize_button">0</property>
<property name="maximum_size"></property>
<property name="min_size"></property>
<property name="minimize_button">0</property>
<property name="minimum_size"></property>
<property name="moveable">1</property>
<property name="name">m_chkThou</property>
<property name="pane_border">1</property>
<property name="pane_position"></property>
<property name="pane_size"></property>
<property name="permission">protected</property>
<property name="pin_button">1</property>
<property name="pos"></property>
<property name="resize">Resizable</property>
<property name="show">1</property>
<property name="size"></property>
<property name="style"></property>
<property name="subclass"></property>
<property name="toolbar_pane">0</property>
<property name="tooltip"></property>
<property name="validator_data_type"></property>
<property name="validator_style">wxFILTER_NONE</property>
<property name="validator_type">wxDefaultValidator</property>
<property name="validator_variable"></property>
<property name="window_extra_style"></property>
<property name="window_name"></property>
<property name="window_style"></property>
<event name="OnChar"></event>
<event name="OnCheckBox"></event>
<event name="OnEnterWindow"></event>
<event name="OnEraseBackground"></event>
<event name="OnKeyDown"></event>
<event name="OnKeyUp"></event>
<event name="OnKillFocus"></event>
<event name="OnLeaveWindow"></event>
<event name="OnLeftDClick"></event>
<event name="OnLeftDown"></event>
<event name="OnLeftUp"></event>
<event name="OnMiddleDClick"></event>
<event name="OnMiddleDown"></event>
<event name="OnMiddleUp"></event>
<event name="OnMotion"></event>
<event name="OnMouseEvents"></event>
<event name="OnMouseWheel"></event>
<event name="OnPaint"></event>
<event name="OnRightDClick"></event>
<event name="OnRightDown"></event>
<event name="OnRightUp"></event>
<event name="OnSetFocus"></event>
<event name="OnSize"></event>
<event name="OnUpdateUI"></event>
</object>
</object>
<object class="sizeritem" expanded="1">
<property name="border">5</property>
<property name="flag">wxEXPAND</property>
<property name="proportion">1</property>
<object class="wxStdDialogButtonSizer" expanded="1">
<property name="Apply">0</property>
<property name="Cancel">1</property>
<property name="ContextHelp">0</property>
<property name="Help">0</property>
<property name="No">0</property>
<property name="OK">1</property>
<property name="Save">0</property>
<property name="Yes">0</property>
<property name="minimum_size"></property>
<property name="name">m_sdbSizer1</property>
<property name="permission">protected</property>
<event name="OnApplyButtonClick"></event>
<event name="OnCancelButtonClick"></event>
<event name="OnContextHelpButtonClick"></event>
<event name="OnHelpButtonClick"></event>
<event name="OnNoButtonClick"></event>
<event name="OnOKButtonClick"></event>
<event name="OnSaveButtonClick"></event>
<event name="OnYesButtonClick"></event>
</object>
</object>
</object>
</object>
</object>
</wxFormBuilder_Project>
///////////////////////////////////////////////////////////////////////////
// C++ code generated with wxFormBuilder (version Oct 8 2012)
// http://www.wxformbuilder.org/
//
// PLEASE DO "NOT" EDIT THIS FILE!
///////////////////////////////////////////////////////////////////////////
#ifndef __DIALOG_EXPORT_IDF_BASE_H__
#define __DIALOG_EXPORT_IDF_BASE_H__
#include <wx/artprov.h>
#include <wx/xrc/xmlres.h>
class DIALOG_SHIM;
#include "dialog_shim.h"
#include <wx/string.h>
#include <wx/stattext.h>
#include <wx/gdicmn.h>
#include <wx/font.h>
#include <wx/colour.h>
#include <wx/settings.h>
#include <wx/filepicker.h>
#include <wx/checkbox.h>
#include <wx/sizer.h>
#include <wx/button.h>
#include <wx/dialog.h>
///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
/// Class DIALOG_EXPORT_IDF3_BASE
///////////////////////////////////////////////////////////////////////////////
class DIALOG_EXPORT_IDF3_BASE : public DIALOG_SHIM
{
private:
protected:
wxStaticText* m_txtBrdFile;
wxFilePickerCtrl* m_filePickerIDF;
wxCheckBox* m_chkThou;
wxStdDialogButtonSizer* m_sdbSizer1;
wxButton* m_sdbSizer1OK;
wxButton* m_sdbSizer1Cancel;
public:
DIALOG_EXPORT_IDF3_BASE( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = wxT("Export IDFv3"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( 458,177 ), long style = wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER );
~DIALOG_EXPORT_IDF3_BASE();
};
#endif //__DIALOG_EXPORT_IDF_BASE_H__
/**
* @file export_idf.cpp
*/
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2013 Cirilo Bernardo
*
* 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 2
* 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, you may find one here:
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include <list>
#include <wxPcbStruct.h>
#include <macros.h>
#include <pcbnew.h>
#include <class_board.h>
#include <class_module.h>
#include <class_edge_mod.h>
#include <idf.h>
// assumed default graphical line thickness: 10000 IU == 0.1mm
#define LINE_WIDTH (100000)
/**
* Function idf_export_outline
* retrieves line segment information from the edge layer and compiles
* the data into a form which can be output as an IDFv3 compliant
* BOARD_OUTLINE section.
*/
static void idf_export_outline( BOARD* aPcb, IDF_BOARD& aIDFBoard )
{
double scale = aIDFBoard.GetScale();
DRAWSEGMENT* graphic; // KiCad graphical item
IDF_POINT sp, ep; // start and end points from KiCad item
std::list< IDF_SEGMENT* > lines; // IDF intermediate form of KiCad graphical item
IDF_OUTLINE outline; // graphical items forming an outline or cutout
// NOTE: IMPLEMENTATION
// If/when component cutouts are allowed, we must implement them separately. Cutouts
// must be added to the board outline section and not to the Other Outline section.
// The module cutouts should be handled via the idf_export_module() routine.
double offX, offY;
aIDFBoard.GetOffset( offX, offY );
// Retrieve segments and arcs from the board
for( BOARD_ITEM* item = aPcb->m_Drawings; item; item = item->Next() )
{
if( item->Type() != PCB_LINE_T || item->GetLayer() != EDGE_N )
continue;
graphic = (DRAWSEGMENT*) item;
switch( graphic->GetShape() )
{
case S_SEGMENT:
{
sp.x = graphic->GetStart().x * scale + offX;
sp.y = -graphic->GetStart().y * scale + offY;
ep.x = graphic->GetEnd().x * scale + offX;
ep.y = -graphic->GetEnd().y * scale + offY;
IDF_SEGMENT* seg = new IDF_SEGMENT( sp, ep );
if( seg )
lines.push_back( seg );
}
break;
case S_ARC:
{
sp.x = graphic->GetCenter().x * scale + offX;
sp.y = -graphic->GetCenter().y * scale + offY;
ep.x = graphic->GetArcStart().x * scale + offX;
ep.y = -graphic->GetArcStart().y * scale + offY;
IDF_SEGMENT* seg = new IDF_SEGMENT( sp, ep, -graphic->GetAngle() / 10.0, true );
if( seg )
lines.push_back( seg );
}
break;
case S_CIRCLE:
{
sp.x = graphic->GetCenter().x * scale + offX;
sp.y = -graphic->GetCenter().y * scale + offY;
ep.x = sp.x - graphic->GetRadius() * scale;
ep.y = sp.y;
// Circles must always have an angle of +360 deg. to appease
// quirky MCAD implementations of IDF.
IDF_SEGMENT* seg = new IDF_SEGMENT( sp, ep, 360.0, true );
if( seg )
lines.push_back( seg );
}
break;
default:
break;
}
}
// if there is no outline then use the bounding box
if( lines.empty() )
{
goto UseBoundingBox;
}
// get the board outline and write it out
// note: we do not use a try/catch block here since we intend
// to simply ignore unclosed loops and continue processing
// until we're out of segments to process
IDF3::GetOutline( lines, outline );
if( outline.empty() )
goto UseBoundingBox;
aIDFBoard.AddOutline( outline );
// get all cutouts and write them out
while( !lines.empty() )
{
IDF3::GetOutline( lines, outline );
if( outline.empty() )
continue;
aIDFBoard.AddOutline( outline );
}
return;
UseBoundingBox:
// clean up if necessary
while( !lines.empty() )
{
delete lines.front();
lines.pop_front();
}
outline.Clear();
// fetch a rectangular bounding box for the board;
// there is always some uncertainty in the board dimensions
// computed via ComputeBoundingBox() since this depends on the
// individual module entities.
EDA_RECT bbbox = aPcb->ComputeBoundingBox( true );
// convert to mm and compensate for an assumed LINE_WIDTH line thickness
double x = ( bbbox.GetOrigin().x + LINE_WIDTH / 2 ) * scale + offX;
double y = ( bbbox.GetOrigin().y + LINE_WIDTH / 2 ) * scale + offY;
double dx = ( bbbox.GetSize().x - LINE_WIDTH ) * scale;
double dy = ( bbbox.GetSize().y - LINE_WIDTH ) * scale;
double px[4], py[4];
px[0] = x;
py[0] = y;
px[1] = x;
py[1] = y + dy;
px[2] = x + dx;
py[2] = y + dy;
px[3] = x + dx;
py[3] = y;
IDF_POINT p1, p2;
p1.x = px[3];
p1.y = py[3];
p2.x = px[0];
p2.y = py[0];
outline.push( new IDF_SEGMENT( p1, p2 ) );
for( int i = 1; i < 4; ++i )
{
p1.x = px[i - 1];
p1.y = py[i - 1];
p2.x = px[i];
p2.y = py[i];
outline.push( new IDF_SEGMENT( p1, p2 ) );
}
aIDFBoard.AddOutline( outline );
}
/**
* Function idf_export_module
* retrieves information from all board modules, adds drill holes to
* the DRILLED_HOLES or BOARD_OUTLINE section as appropriate,
* compiles data for the PLACEMENT section and compiles data for
* the library ELECTRICAL section.
*/
static void idf_export_module( BOARD* aPcb, MODULE* aModule,
IDF_BOARD& aIDFBoard )
{
// Reference Designator
std::string crefdes = TO_UTF8( aModule->GetReference() );
if( crefdes.empty() || !crefdes.compare( "~" ) )
{
std::string cvalue = TO_UTF8( aModule->GetValue() );
// if both the RefDes and Value are empty or set to '~' the board owns the part,
// otherwise associated parts of the module must be marked NOREFDES.
if( cvalue.empty() || !cvalue.compare( "~" ) )
crefdes = "BOARD";
else
crefdes = "NOREFDES";
}
// TODO: If module cutouts are supported we must add code here
// for( EDA_ITEM* item = aModule->GraphicalItems(); item != NULL; item = item->Next() )
// {
// if( ( item->Type() != PCB_MODULE_EDGE_T )
// || (item->GetLayer() != EDGE_N ) ) continue;
// code to export cutouts
// }
// Export pads
double drill, x, y;
double scale = aIDFBoard.GetScale();
IDF3::KEY_PLATING kplate;
std::string pintype;
std::string tstr;
double dx, dy;
aIDFBoard.GetOffset( dx, dy );
for( D_PAD* pad = aModule->Pads(); pad; pad = pad->Next() )
{
drill = (double) pad->GetDrillSize().x * scale;
x = pad->GetPosition().x * scale + dx;
y = -pad->GetPosition().y * scale + dy;
// Export the hole on the edge layer
if( drill > 0.0 )
{
// plating
if( pad->GetAttribute() == PAD_HOLE_NOT_PLATED )
kplate = IDF3::NPTH;
else
kplate = IDF3::PTH;
// hole type
tstr = TO_UTF8( pad->GetPadName() );
if( tstr.empty() || !tstr.compare( "0" ) || !tstr.compare( "~" )
|| ( kplate == IDF3::NPTH ) || ( pad->GetDrillShape() == PAD_OVAL ) )
pintype = "MTG";
else
pintype = "PIN";
// fields:
// 1. hole dia. : float
// 2. X coord : float
// 3. Y coord : float
// 4. plating : PTH | NPTH
// 5. Assoc. part : BOARD | NOREFDES | PANEL | {"refdes"}
// 6. type : PIN | VIA | MTG | TOOL | { "other" }
// 7. owner : MCAD | ECAD | UNOWNED
if( ( pad->GetDrillShape() == PAD_OVAL )
&& ( pad->GetDrillSize().x != pad->GetDrillSize().y ) )
{
// NOTE: IDF does not have direct support for slots;
// slots are implemented as a board cutout and we
// cannot represent plating or reference designators
double dlength = pad->GetDrillSize().y * scale;
// NOTE: The orientation of modules and pads have
// the opposite sense due to KiCad drawing on a
// screen with a LH coordinate system
double angle = pad->GetOrientation() / 10.0;
if( dlength < drill )
{
std::swap( drill, dlength );
angle += M_PI2;
}
// NOTE: KiCad measures a slot's length from end to end
// rather than between the centers of the arcs
dlength -= drill;
aIDFBoard.AddSlot( drill, dlength, angle, x, y );
}
else
{
aIDFBoard.AddDrill( drill, x, y, kplate, crefdes, pintype, IDF3::ECAD );
}
}
}
// TODO
// add to the library item list
}
/**
* Function Export_IDF3
* generates IDFv3 compliant board (*.emn) and library (*.emp)
* files representing the user's PCB design.
*/
bool Export_IDF3( BOARD* aPcb, const wxString& aFullFileName, double aUseThou )
{
IDF_BOARD idfBoard;
SetLocaleTo_C_standard();
// NOTE:
// XXX We may enclose all this in a TRY .. CATCH block
idfBoard.Setup( aPcb->GetFileName(), aFullFileName, aUseThou,
aPcb->GetDesignSettings().GetBoardThickness() );
// set up the global offsets
EDA_RECT bbox = aPcb->ComputeBoundingBox( true );
idfBoard.SetOffset( bbox.Centre().x * idfBoard.GetScale(),
bbox.Centre().y * idfBoard.GetScale() );
// Export the board outline
idf_export_outline( aPcb, idfBoard );
// Output the drill holes and module (library) data.
for( MODULE* module = aPcb->m_Modules; module != 0; module = module->Next() )
idf_export_module( aPcb, module, idfBoard );
idfBoard.Finish();
SetLocaleTo_Default();
return true;
}
/**
* file: idf.cpp
*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2013 Cirilo Bernardo
*
* 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 2
* 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, you may find one here:
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
// TODO: Consider using different precision formats for THOU vs MM output
// Keep in mind that THOU cannot represent MM very well although MM can
// represent 1 THOU with 4 decimal places. For modern manufacturing we
// are interested in a resolution of about 0.1 THOU.
#include <list>
#include <string>
#include <algorithm>
#include <cstdio>
#include <cmath>
#include <ctime>
#include <wx/filename.h>
#include <macros.h>
#include <idf.h>
#include <build_version.h>
// differences in angle smaller than MIN_ANG are considered equal
#define MIN_ANG (0.01)
// minimum drill diameter (nanometers) - 10000 is a 0.01mm drill
#define IDF_MIN_DIA ( 10000.0 )
// minimum board thickness; this is about 0.012mm (0.5 mils)
// which is about the thickness of a single kapton layer typically
// used in a flexible design.
#define IDF_MIN_BRD_THICKNESS (12000)
bool IDF_POINT::Matches( const IDF_POINT& aPoint, double aRadius )
{
double dx = x - aPoint.x;
double dy = y - aPoint.y;
double d2 = dx * dx + dy * dy;
if( d2 <= aRadius * aRadius )
return true;
return false;
}
double IDF_POINT::CalcDistance( const IDF_POINT& aPoint ) const
{
double dx = aPoint.x - x;
double dy = aPoint.y - y;
double dist = sqrt( dx * dx + dy * dy );
return dist;
}
double IDF3::CalcAngleRad( const IDF_POINT& aStartPoint, const IDF_POINT& aEndPoint )
{
return atan2( aEndPoint.y - aStartPoint.y, aEndPoint.x - aStartPoint.x );
}
double IDF3::CalcAngleDeg( const IDF_POINT& aStartPoint, const IDF_POINT& aEndPoint )
{
double ang = CalcAngleRad( aStartPoint, aEndPoint );
// round to thousandths of a degree
int iang = int (ang / M_PI * 1800000.0);
ang = iang / 10000.0;
return ang;
}
IDF_SEGMENT::IDF_SEGMENT()
{
angle = 0.0;
offsetAngle = 0.0;
radius = 0.0;
}
IDF_SEGMENT::IDF_SEGMENT( const IDF_POINT& aStartPoint, const IDF_POINT& aEndPoint )
{
angle = 0.0;
offsetAngle = 0.0;
radius = 0.0;
startPoint = aStartPoint;
endPoint = aEndPoint;
}
IDF_SEGMENT::IDF_SEGMENT( const IDF_POINT& aStartPoint,
const IDF_POINT& aEndPoint,
double aAngle,
bool aFromKicad )
{
double diff = abs( aAngle ) - 360.0;
if( ( diff < MIN_ANG
&& diff > -MIN_ANG ) || ( aAngle < MIN_ANG && aAngle > -MIN_ANG ) || (!aFromKicad) )
{
angle = 0.0;
startPoint = aStartPoint;
endPoint = aEndPoint;
if( diff < MIN_ANG && diff > -MIN_ANG )
{
angle = 360.0;
center = aStartPoint;
offsetAngle = 0.0;
radius = aStartPoint.CalcDistance( aEndPoint );
}
else if( aAngle < MIN_ANG && aAngle > -MIN_ANG )
{
CalcCenterAndRadius();
}
return;
}
// we need to convert from the KiCad arc convention
angle = aAngle;
center = aStartPoint;
offsetAngle = IDF3::CalcAngleDeg( aStartPoint, aEndPoint );
radius = aStartPoint.CalcDistance( aEndPoint );
startPoint = aEndPoint;
double ang = offsetAngle + aAngle;
ang = (ang / 180.0) * M_PI;
endPoint.x = ( radius * cos( ang ) ) + center.x;
endPoint.y = ( radius * sin( ang ) ) + center.y;
}
bool IDF_SEGMENT::MatchesStart( const IDF_POINT& aPoint, double aRadius )
{
return startPoint.Matches( aPoint, aRadius );
}
bool IDF_SEGMENT::MatchesEnd( const IDF_POINT& aPoint, double aRadius )
{
return endPoint.Matches( aPoint, aRadius );
}
void IDF_SEGMENT::CalcCenterAndRadius( void )
{
// NOTE: this routine does not check if the points are the same
// or too close to be sensible in a production setting.
double offAng = IDF3::CalcAngleRad( startPoint, endPoint );
double d = startPoint.CalcDistance( endPoint ) / 2.0;
double xm = ( startPoint.x + endPoint.x ) * 0.5;
double ym = ( startPoint.y + endPoint.y ) * 0.5;
radius = d / sin( angle * M_PI / 180.0 );
if( radius < 0.0 )
{
radius = -radius;
}
// calculate the height of the triangle with base d and hypotenuse r
double dh2 = radius * radius - d * d;
if( dh2 < 0 )
{
// this should only ever happen due to rounding errors when r == d
dh2 = 0;
}
double h = sqrt( dh2 );
if( angle > 0.0 )
offAng += M_PI2;
else
offAng -= M_PI2;
if( ( angle > M_PI ) || ( angle < -M_PI ) )
offAng += M_PI;
center.x = h * cos( offAng ) + xm;
center.y = h * sin( offAng ) + ym;
offsetAngle = IDF3::CalcAngleDeg( center, startPoint );
}
bool IDF_SEGMENT::IsCircle( void )
{
double diff = abs( angle ) - 360.0;
if( ( diff < MIN_ANG ) && ( diff > -MIN_ANG ) )
return true;
return false;
}
double IDF_SEGMENT::GetMinX( void )
{
if( angle == 0.0 )
return std::min( startPoint.x, endPoint.x );
// Calculate the leftmost point of the circle or arc
if( IsCircle() )
{
// if only everything were this easy
return center.x - radius;
}
// cases:
// 1. CCW arc: if offset + included angle >= 180 deg then
// MinX = center.x - radius, otherwise MinX is the
// same as for the case of a line.
// 2. CW arc: if offset + included angle <= -180 deg then
// MinX = center.x - radius, otherwise MinX is the
// same as for the case of a line.
if( angle > 0 )
{
// CCW case
if( ( offsetAngle + angle ) >= 180.0 )
{
return center.x - radius;
}
else
{
return std::min( startPoint.x, endPoint.x );
}
}
// CW case
if( ( offsetAngle + angle ) <= -180.0 )
{
return center.x - radius;
}
return std::min( startPoint.x, endPoint.x );
}
void IDF_SEGMENT::SwapEnds( void )
{
if( IsCircle() )
{
// reverse the direction
angle = -angle;
return;
}
IDF_POINT tmp = startPoint;
startPoint = endPoint;
endPoint = tmp;
if( ( angle < MIN_ANG ) && ( angle > -MIN_ANG ) )
return; // nothing more to do
// change the direction of the arc
angle = -angle;
// calculate the new offset angle
offsetAngle = IDF3::CalcAngleDeg( center, startPoint );
}
IDF_DRILL_DATA::IDF_DRILL_DATA( double aDrillDia, double aPosX, double aPosY,
IDF3::KEY_PLATING aPlating,
const std::string aRefDes,
const std::string aHoleType,
IDF3::KEY_OWNER aOwner )
{
if( aDrillDia < 0.3 )
dia = 0.3;
else
dia = aDrillDia;
x = aPosX;
y = aPosY;
plating = aPlating;
if( !aRefDes.compare( "BOARD" ) )
{
kref = IDF3::BOARD;
}
else if( aRefDes.empty() || !aRefDes.compare( "NOREFDES" ) )
{
kref = IDF3::NOREFDES;
}
else if( !aRefDes.compare( "PANEL" ) )
{
kref = IDF3::PANEL;
}
else
{
kref = IDF3::REFDES;
refdes = aRefDes;
}
if( !aHoleType.compare( "PIN" ) )
{
khole = IDF3::PIN;
}
else if( !aHoleType.compare( "VIA" ) )
{
khole = IDF3::VIA;
}
else if( aHoleType.empty() || !aHoleType.compare( "MTG" ) )
{
khole = IDF3::MTG;
}
else if( !aHoleType.compare( "TOOL" ) )
{
khole = IDF3::TOOL;
}
else
{
khole = IDF3::OTHER;
holetype = aHoleType;
}
owner = aOwner;
} // IDF_DRILL_DATA::IDF_DRILL_DATA( ... )
bool IDF_DRILL_DATA::Write( FILE* aLayoutFile )
{
// TODO: check stream integrity and return 'false' as appropriate
if( !aLayoutFile )
return false;
std::string holestr;
std::string refstr;
std::string ownstr;
std::string pltstr;
switch( khole )
{
case IDF3::PIN:
holestr = "PIN";
break;
case IDF3::VIA:
holestr = "VIA";
break;
case IDF3::TOOL:
holestr = "TOOL";
break;
case IDF3::OTHER:
holestr = "\"" + holetype + "\"";
break;
default:
holestr = "MTG";
break;
}
switch( kref )
{
case IDF3::BOARD:
refstr = "BOARD";
break;
case IDF3::PANEL:
refstr = "PANEL";
break;
case IDF3::REFDES:
refstr = "\"" + refdes + "\"";
break;
default:
refstr = "NOREFDES";
break;
}
if( plating == IDF3::PTH )
pltstr = "PTH";
else
pltstr = "NPTH";
switch( owner )
{
case IDF3::MCAD:
ownstr = "MCAD";
break;
case IDF3::ECAD:
ownstr = "ECAD";
break;
default:
ownstr = "UNOWNED";
}
fprintf( aLayoutFile, "%.3f %.5f %.5f %s %s %s %s\n",
dia, x, y, pltstr.c_str(), refstr.c_str(), holestr.c_str(), ownstr.c_str() );
return true;
} // IDF_DRILL_DATA::Write( aLayoutFile )
IDF_BOARD::IDF_BOARD()
{
outlineIndex = 0;
scale = 1e-6;
boardThickness = 1.6; // default to 1.6mm thick boards
useThou = false; // by default we want mm output
hasBrdOutlineHdr = false;
layoutFile = NULL;
libFile = NULL;
}
IDF_BOARD::~IDF_BOARD()
{
Finish();
}
bool IDF_BOARD::Setup( wxString aBoardName,
wxString aFullFileName,
bool aUseThou,
int aBoardThickness )
{
if( aBoardThickness < IDF_MIN_BRD_THICKNESS )
return false;
if( aUseThou )
{
useThou = true;
scale = 1e-3 / 25.4;
}
else
{
useThou = false;
scale = 1e-6;
}
boardThickness = aBoardThickness * scale;
wxFileName brdname( aBoardName );
wxFileName idfname( aFullFileName );
// open the layout file
idfname.SetExt( wxT( "emn" ) );
layoutFile = wxFopen( aFullFileName, wxT( "wt" ) );
if( layoutFile == NULL )
return false;
// open the library file
idfname.SetExt( wxT( "emp" ) );
libFile = wxFopen( idfname.GetFullPath(), wxT( "wt" ) );
if( libFile == NULL )
{
fclose( layoutFile );
layoutFile = NULL;
return false;
}
time_t date;
time( &date );
struct tm tdate;
time( &date );
localtime_r( &date, &tdate );
fprintf( layoutFile, ".HEADER\n"
"BOARD_FILE 3.0 \"Created by KiCad %s\""
" %.4d/%.2d/%.2d.%.2d:%.2d:%.2d 1\n"
"\"%s\" %s\n"
".END_HEADER\n\n",
TO_UTF8( GetBuildVersion() ),
tdate.tm_year + 1900, tdate.tm_mon + 1, tdate.tm_mday,
tdate.tm_hour, tdate.tm_min, tdate.tm_sec,
TO_UTF8( brdname.GetFullName() ), useThou ? "THOU" : "MM" );
fprintf( libFile, ".HEADER\n"
"BOARD_FILE 3.0 \"Created by KiCad %s\" %.4d/%.2d/%.2d.%.2d:%.2d:%.2d 1\n"
".END_HEADER\n\n",
TO_UTF8( GetBuildVersion() ),
tdate.tm_year + 1900, tdate.tm_mon + 1, tdate.tm_mday,
tdate.tm_hour, tdate.tm_min, tdate.tm_sec );
return true;
}
bool IDF_BOARD::Finish( void )
{
// Steps to finalize the board and library files:
// 1. (emp) finalize the library file
// 2. (emn) close the BOARD_OUTLINE section
// 3. (emn) write out the DRILLED_HOLES section
// 4. (emn) write out the COMPONENT_PLACEMENT section
// TODO:
// idfLib.Finish();
if( libFile != NULL )
{
fclose( libFile );
libFile = NULL;
}
if( layoutFile == NULL )
return false;
// Finalize the board outline section
fprintf( layoutFile, ".END_BOARD_OUTLINE\n\n" );
// Write out the drill section
if( WriteDrills() )
{
fclose( layoutFile );
layoutFile = NULL;
return false;
}
// TODO: Write out the component placement section
// IDF3::export_placement();
fclose( layoutFile );
layoutFile = NULL;
return true;
}
bool IDF_BOARD::AddOutline( IDF_OUTLINE& aOutline )
{
if( !layoutFile )
return false;
// TODO: check the stream integrity
std::list<IDF_SEGMENT*>::iterator bo;
std::list<IDF_SEGMENT*>::iterator eo;
if( !hasBrdOutlineHdr )
{
fprintf( layoutFile, ".BOARD_OUTLINE ECAD\n%.5f\n", boardThickness );
hasBrdOutlineHdr = true;
}
if( aOutline.size() == 1 )
{
if( !aOutline.front()->IsCircle() )
return false; // this is a bad outline
// NOTE: a circle always has an angle of 360, never -360,
// otherwise SolidWorks chokes on the file.
fprintf( layoutFile, "%d %.5f %.5f 0\n", outlineIndex,
aOutline.front()->startPoint.x, aOutline.front()->startPoint.y );
fprintf( layoutFile, "%d %.5f %.5f 360\n", outlineIndex,
aOutline.front()->endPoint.x, aOutline.front()->endPoint.y );
++outlineIndex;
return true;
}
// ensure that the very last point is the same as the very first point
aOutline.back()-> endPoint = aOutline.front()->startPoint;
// check if we must reverse things
if( ( aOutline.IsCCW() && ( outlineIndex > 0 ) )
|| ( ( !aOutline.IsCCW() ) && ( outlineIndex == 0 ) ) )
{
eo = aOutline.begin();
bo = aOutline.end();
--bo;
// for the first item we write out both points
if( aOutline.front()->angle < MIN_ANG && aOutline.front()->angle > -MIN_ANG )
{
fprintf( layoutFile, "%d %.5f %.5f 0\n", outlineIndex,
aOutline.front()->endPoint.x, aOutline.front()->endPoint.y );
fprintf( layoutFile, "%d %.5f %.5f 0\n", outlineIndex,
aOutline.front()->startPoint.x, aOutline.front()->startPoint.y );
}
else
{
fprintf( layoutFile, "%d %.5f %.5f 0\n", outlineIndex,
aOutline.front()->endPoint.x, aOutline.front()->endPoint.y );
fprintf( layoutFile, "%d %.5f %.5f %.5f\n", outlineIndex,
aOutline.front()->startPoint.x, aOutline.front()->startPoint.y,
-aOutline.front()->angle );
}
// for all other segments we only write out the start point
while( bo != eo )
{
if( (*bo)->angle < MIN_ANG && (*bo)->angle > -MIN_ANG )
{
fprintf( layoutFile, "%d %.5f %.5f 0\n", outlineIndex,
(*bo)->startPoint.x, (*bo)->startPoint.y );
}
else
{
fprintf( layoutFile, "%d %.5f %.5f %.5f\n", outlineIndex,
(*bo)->startPoint.x, (*bo)->startPoint.y, -(*bo)->angle );
}
--bo;
}
}
else
{
bo = aOutline.begin();
eo = aOutline.end();
// for the first item we write out both points
if( (*bo)->angle < MIN_ANG && (*bo)->angle > -MIN_ANG )
{
fprintf( layoutFile, "%d %.5f %.5f 0\n", outlineIndex,
(*bo)->startPoint.x, (*bo)->startPoint.y );
fprintf( layoutFile, "%d %.5f %.5f 0\n", outlineIndex,
(*bo)->endPoint.x, (*bo)->endPoint.y );
}
else
{
fprintf( layoutFile, "%d %.5f %.5f 0\n", outlineIndex,
(*bo)->startPoint.x, (*bo)->startPoint.y );
fprintf( layoutFile, "%d %.5f %.5f %.5f\n", outlineIndex,
(*bo)->endPoint.x, (*bo)->endPoint.y, (*bo)->angle );
}
++bo;
// for all other segments we only write out the last point
while( bo != eo )
{
if( (*bo)->angle < MIN_ANG && (*bo)->angle > -MIN_ANG )
{
fprintf( layoutFile, "%d %.5f %.5f 0\n", outlineIndex,
(*bo)->endPoint.x, (*bo)->endPoint.y );
}
else
{
fprintf( layoutFile, "%d %.5f %.5f %.5f\n", outlineIndex,
(*bo)->endPoint.x, (*bo)->endPoint.y, (*bo)->angle );
}
++bo;
}
}
++outlineIndex;
return true;
}
bool IDF_BOARD::AddDrill( double dia, double x, double y,
IDF3::KEY_PLATING plating,
const std::string refdes,
const std::string holeType,
IDF3::KEY_OWNER owner )
{
if( dia < IDF_MIN_DIA * scale )
return false;
IDF_DRILL_DATA* dp = new IDF_DRILL_DATA( dia, x, y, plating, refdes, holeType, owner );
drills.push_back( dp );
return true;
}
bool IDF_BOARD::AddSlot( double aWidth, double aLength, double aOrientation,
double aX, double aY )
{
if( aWidth < IDF_MIN_DIA * scale )
return false;
if( aLength < IDF_MIN_DIA * scale )
return false;
IDF_POINT c[2]; // centers
IDF_POINT pt[4];
double a1 = aOrientation / 180.0 * M_PI;
double a2 = a1 + M_PI2;
double d1 = aLength / 2.0;
double d2 = aWidth / 2.0;
double sa1 = sin( a1 );
double ca1 = cos( a1 );
double dsa2 = d2 * sin( a2 );
double dca2 = d2 * cos( a2 );
c[0].x = aX + d1 * ca1;
c[0].y = aY + d1 * sa1;
c[1].x = aX - d1 * ca1;
c[1].y = aY - d1 * sa1;
pt[0].x = c[0].x - dca2;
pt[0].y = c[0].y - dsa2;
pt[1].x = c[1].x - dca2;
pt[1].y = c[1].y - dsa2;
pt[2].x = c[1].x + dca2;
pt[2].y = c[1].y + dsa2;
pt[3].x = c[0].x + dca2;
pt[3].y = c[0].y + dsa2;
IDF_OUTLINE outline;
// first straight run
IDF_SEGMENT* seg = new IDF_SEGMENT( pt[0], pt[1] );
outline.push( seg );
// first 180 degree cap
seg = new IDF_SEGMENT( c[1], pt[1], -180.0, true );
outline.push( seg );
// final straight run
seg = new IDF_SEGMENT( pt[2], pt[3] );
outline.push( seg );
// final 180 degree cap
seg = new IDF_SEGMENT( c[0], pt[3], -180.0, true );
outline.push( seg );
return AddOutline( outline );
}
bool IDF_BOARD::WriteDrills( void )
{
if( !layoutFile )
return false;
// TODO: check the stream integrity and return false as appropriate
if( drills.empty() )
return true;
fprintf( layoutFile, ".DRILLED_HOLES\n" );
std::list<struct IDF_DRILL_DATA*>::iterator ds = drills.begin();
std::list<struct IDF_DRILL_DATA*>::iterator de = drills.end();
while( ds != de )
{
if( !(*ds)->Write( layoutFile ) )
return false;
++ds;
}
fprintf( layoutFile, ".END_DRILLED_HOLES\n" );
return true;
}
double IDF_BOARD::GetScale( void )
{
return scale;
}
void IDF_BOARD::SetOffset( double x, double y )
{
offsetX = x;
offsetY = y;
}
void IDF_BOARD::GetOffset( double& x, double& y )
{
x = offsetX;
y = offsetY;
}
void IDF3::GetOutline( std::list<IDF_SEGMENT*>& aLines,
IDF_OUTLINE& aOutline )
{
aOutline.Clear();
// NOTE: To tell if the point order is CCW or CW,
// sum all: (endPoint.X[n] - startPoint.X[n])*(endPoint[n] + startPoint.Y[n])
// If the result is >0, the direction is CW, otherwise
// it is CCW. Note that the result cannot be 0 unless
// we have a bounded area of 0.
// First we find the segment with the leftmost point
std::list<IDF_SEGMENT*>::iterator bl = aLines.begin();
std::list<IDF_SEGMENT*>::iterator el = aLines.end();
std::list<IDF_SEGMENT*>::iterator idx = bl++; // iterator for the object with minX
double minx = (*idx)->GetMinX();
double curx;
while( bl != el )
{
curx = (*bl)->GetMinX();
if( curx < minx )
{
minx = curx;
idx = bl;
}
++bl;
}
aOutline.push( *idx );
aLines.erase( idx );
// If the item is a circle then we're done
if( aOutline.front()->IsCircle() )
return;
// Assemble the loop
bool complete = false; // set if loop is complete
bool matched; // set if a segment's end point was matched
while( !complete )
{
matched = false;
bl = aLines.begin();
el = aLines.end();
while( bl != el && !matched )
{
if( (*bl)->MatchesStart( aOutline.back()->endPoint ) )
{
if( (*bl)->IsCircle() )
{
// a circle on the perimeter is pathological but we just ignore it
++bl;
}
else
{
matched = true;
aOutline.push( *bl );
aLines.erase( bl );
}
continue;
}
++bl;
}
if( !matched )
{
// attempt to match the end points
bl = aLines.begin();
el = aLines.end();
while( bl != el && !matched )
{
if( (*bl)->MatchesEnd( aOutline.back()->endPoint ) )
{
if( (*bl)->IsCircle() )
{
// a circle on the perimeter is pathological but we just ignore it
++bl;
}
else
{
matched = true;
(*bl)->SwapEnds();
aOutline.push( *bl );
aLines.erase( bl );
}
continue;
}
++bl;
}
}
if( !matched )
{
// still no match - attempt to close the loop
if( (aOutline.size() > 1) || ( aOutline.front()->angle < -MIN_ANG )
|| ( aOutline.front()->angle > MIN_ANG ) )
{
// close the loop
IDF_SEGMENT* seg = new IDF_SEGMENT( aOutline.back()->endPoint,
aOutline.front()->startPoint );
if( seg )
{
complete = true;
aOutline.push( seg );
break;
}
}
// the outline is bad; drop the segments
aOutline.Clear();
return;
}
// check if the loop is complete
if( aOutline.front()->MatchesStart( aOutline.back()->endPoint ) )
{
complete = true;
break;
}
}
}
bool IDF_LIB::WriteLib( FILE* aLibFile )
{
if( !aLibFile )
return false;
// TODO: check stream integrity and return false as appropriate
// TODO: export models
return true;
}
bool IDF_LIB::WriteBrd( FILE* aLayoutFile )
{
if( !aLayoutFile )
return false;
// TODO: check stream integrity and return false as appropriate
// TODO: write out the board placement information
return true;
}
/**
* @file idf.h
*/
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2013 Cirilo Bernardo
*
* 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 2
* 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, you may find one here:
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#ifndef IDF_H
#define IDF_H
#include <wx/string.h>
#ifndef M_PI
#define M_PI 3.1415926535897932384626433832795028841
#endif
#ifndef M_PI2
#define M_PI2 ( M_PI / 2.0 )
#endif
#ifndef M_PI4
#define M_PI4 ( M_PI / 4.0 )
#endif
class IDF_POINT;
class IDF_SEGMENT;
class IDF_DRILL_DATA;
class IDF_OUTLINE;
namespace IDF3 {
enum KEY_OWNER
{
UNOWNED = 0, // < either MCAD or ECAD may modify a feature
MCAD, // < only MCAD may modify a feature
ECAD // < only ECAD may modify a feature
};
enum KEY_HOLETYPE
{
PIN = 0, // < drill hole is for a pin
VIA, // < drill hole is for a via
MTG, // < drill hole is for mounting
TOOL, // < drill hole is for tooling
OTHER // < user has specified a custom type
};
enum KEY_PLATING
{
PTH = 0, // < Plate-Through Hole
NPTH // < Non-Plate-Through Hole
};
enum KEY_REFDES
{
BOARD = 0, // < feature is associated with the board
NOREFDES, // < feature is associated with a component with no RefDes
PANEL, // < feature is associated with an IDF panel
REFDES // < reference designator as assigned by the CAD software
};
// calculate the angle between the horizon and the segment aStartPoint to aEndPoint
double CalcAngleRad( const IDF_POINT& aStartPoint, const IDF_POINT& aEndPoint );
double CalcAngleDeg( const IDF_POINT& aStartPoint, const IDF_POINT& aEndPoint );
// take contiguous elements from 'lines' and stuff them into 'outline'
void GetOutline( std::list<IDF_SEGMENT*>& aLines,
IDF_OUTLINE& aOutline );
}
/**
* @Class IDF_POINT
* represents a point
*/
class IDF_POINT
{
public:
double x; // < X coordinate
double y; // < Y coordinate
IDF_POINT()
{
x = 0.0;
y = 0.0;
}
/**
* Function Matches()
* returns true if the given coordinate point is within the given radius
* of the point.
* @param aPoint : coordinates of the point being compared
* @param aRadius : radius within which the points are considered the same
*/
bool Matches( const IDF_POINT& aPoint, double aRadius = 1e-5 );
double CalcDistance( const IDF_POINT& aPoint ) const;
};
/**
* @Class IDF_SEGMENT
* represents a geometry segment as used in IDFv3 outlines
*/
class IDF_SEGMENT
{
private:
/**
* Function CalcCenterAndRadius()
* Calculates the center, radius, and angle between center and start point given the
* IDF compliant points and included angle.
* @var startPoint, @var endPoint, and @var angle must be set prior as per IDFv3
*/
void CalcCenterAndRadius( void );
public:
IDF_POINT startPoint; // starting point in IDF coordinates
IDF_POINT endPoint; // end point in IDF coordinates
IDF_POINT center; // center of an arc or circle; used primarily for calculating min X
double angle; // included angle (degrees) according to IDFv3 specification
double offsetAngle; // angle between center and start of arc; used to speed up some calcs.
double radius; // radius of the arc or circle; used to speed up some calcs.
/**
* Function IDF_SEGMENT()
* initializes the internal variables
*/
IDF_SEGMENT();
/**
* Function IDF_SEGMENT( start, end )
* creates a straight segment
*/
IDF_SEGMENT( const IDF_POINT& aStartPoint, const IDF_POINT& aEndPoint );
/**
* Function IDF_SEGMENT( start, end )
* creates a straight segment, arc, or circle depending on the angle
* @param aStartPoint : start point (center if using KiCad convention, otherwise IDF convention)
* @param aEndPoint : end point (start of arc if using KiCad convention, otherwise IDF convention)
* @param aAngle : included angle; the KiCad convention is equivalent to the IDF convention
* @param fromKicad : set true if we need to convert from KiCad to IDF convention
*/
IDF_SEGMENT( const IDF_POINT& aStartPoint,
const IDF_POINT& aEndPoint,
double aAngle,
bool aFromKicad );
/**
* Function MatchesStart()
* returns true if the given coordinate is within a radius 'rad'
* of the start point.
* @param aPoint : coordinates of the point being compared
* @param aRadius : radius within which the points are considered the same
*/
bool MatchesStart( const IDF_POINT& aPoint, double aRadius = 1e-3 );
/**
* Function MatchesEnd()
* returns true if the given coordinate is within a radius 'rad'
* of the end point.
* @param aPoint : coordinates of the point being compared
* @param aRadius : radius within which the points are considered the same
*/
bool MatchesEnd( const IDF_POINT& aPoint, double aRadius = 1e-3 );
/**
* Function IsCircle()
* returns true if this segment is a circle
*/
bool IsCircle( void );
/**
* Function GetMinX()
* returns the minimum X coordinate of this segment
*/
double GetMinX( void );
/**
* Function SwapEnds()
* Swaps the start and end points and alters internal
* variables as necessary for arcs
*/
void SwapEnds( void );
};
/**
* @Class IDF_OUTLINE
* contains segment and winding information for an IDF outline
*/
class IDF_OUTLINE
{
private:
double dir;
std::list<IDF_SEGMENT*> outline;
public:
IDF_OUTLINE() { dir = 0.0; }
~IDF_OUTLINE() { Clear(); }
// returns true if the current list of points represents a counterclockwise winding
bool IsCCW( void )
{
if( dir > 0.0 )
return false;
return true;
}
// clears the internal list of outline segments
void Clear( void )
{
dir = 0.0;
while( !outline.empty() )
{
delete outline.front();
outline.pop_front();
}
}
// returns the size of the internal segment list
size_t size( void )
{
return outline.size();
}
// returns true if the internal segment list is empty
bool empty( void )
{
return outline.empty();
}
// return the front() of the internal segment list
IDF_SEGMENT*& front( void )
{
return outline.front();
}
// return the back() of the internal segment list
IDF_SEGMENT*& back( void )
{
return outline.back();
}
// return the begin() iterator of the internal segment list
std::list<IDF_SEGMENT*>::iterator begin( void )
{
return outline.begin();
}
// return the end() iterator of the internal segment list
std::list<IDF_SEGMENT*>::iterator end( void )
{
return outline.end();
}
// push a segment onto the internal list
void push( IDF_SEGMENT* item )
{
// XXX - check that startPoint[N] == endPoint[N -1], otherwise THROW
// XXX - a Circle must stand alone; if we add to a circle or add a
// circle to an existing list, we should throw an exception.
outline.push_back( item );
dir += ( outline.back()->endPoint.x - outline.back()->startPoint.x )
* ( outline.back()->endPoint.y + outline.back()->startPoint.y );
}
};
/**
* @Class IDF_BOARD
* contains objects necessary for the maintenance of the IDF board and library files.
*/
class IDF_BOARD
{
private:
std::list<IDF_DRILL_DATA*> drills; ///< IDF drill data
int outlineIndex; ///< next outline index to use
bool useThou; ///< true if output is THOU
double scale; ///< scale from KiCad IU to IDF output units
double boardThickness; ///< total thickness of the PCB
bool hasBrdOutlineHdr; ///< true when a board outline header has been written
double offsetX; ///< offset to roughly center the board on the world origin
double offsetY;
FILE* layoutFile; ///< IDF board file (*.emn)
FILE* libFile; ///< IDF library file (*.emp)
/**
* Function Write
* outputs a .DRILLED_HOLES section compliant with the
* IDFv3 specification.
* @param aLayoutFile : open file (*.emn) for output
*/
bool WriteDrills( void );
public:
IDF_BOARD();
~IDF_BOARD();
// Set up the output files and scale factor;
// return TRUE if everything is OK
bool Setup( wxString aBoardName, wxString aFullFileName, bool aUseThou, int aBoardThickness );
// Finish a board
// Write out all current data and close files.
// Return true for success
bool Finish( void );
/**
* Function GetScale
* returns the output scaling factor
*/
double GetScale( void );
/**
* Function SetOffset
* sets the global coordinate offsets
*/
void SetOffset( double x, double y );
/**
* Function GetOffset
* returns the global coordinate offsets
*/
void GetOffset( double& x, double& y );
// Add an outline; the very first outline is the board perimeter;
// all additional outlines are cutouts.
bool AddOutline( IDF_OUTLINE& aOutline );
/**
* Function AddDrill
* creates a drill entry and adds it to the list of PCB holes
* @param dia : drill diameter
* @param x : X coordinate of the drill center
* @param y : Y coordinate of the drill center
* @param plating : flag, PTH or NPTH
* @param refdes : component Reference Designator
* @param holetype : purpose of hole
* @param owner : one of MCAD, ECAD, UNOWNED
*/
bool AddDrill( double dia, double x, double y,
IDF3::KEY_PLATING plating,
const std::string refdes,
const std::string holeType,
IDF3::KEY_OWNER owner );
/**
* Function AddSlot
* creates a slot cutout within the IDF BOARD section; this is a deficient representation
* of a KiCad 'oval' drill; IDF is unable to represent a plated slot and unable to
* represent the Reference Designator association with a slot.
*/
bool AddSlot( double aWidth, double aLength, double aOrientation, double aX, double aY );
};
/**
* @Class IDF_DRILL_DATA
* contains information describing a drilled hole and is responsible for
* writing this information to a file in compliance with the IDFv3 specification.
*/
class IDF_DRILL_DATA
{
private:
double dia;
double x;
double y;
IDF3::KEY_PLATING plating;
IDF3::KEY_REFDES kref;
IDF3::KEY_HOLETYPE khole;
std::string refdes;
std::string holetype;
IDF3::KEY_OWNER owner;
public:
/**
* Constructor IDF_DRILL_DATA
* creates a drill entry with information compliant with the
* IDFv3 specifications.
* @param aDrillDia : drill diameter
* @param aPosX : X coordinate of the drill center
* @param aPosY : Y coordinate of the drill center
* @param aPlating : flag, PTH or NPTH
* @param aRefDes : component Reference Designator
* @param aHoleType : purpose of hole
* @param aOwner : one of MCAD, ECAD, UNOWNED
*/
IDF_DRILL_DATA( double aDrillDia, double aPosX, double aPosY,
IDF3::KEY_PLATING aPlating,
const std::string aRefDes,
const std::string aHoleType,
IDF3::KEY_OWNER aOwner );
/**
* Function Write
* writes a single line representing the hole within a .DRILLED_HOLES section
*/
bool Write( FILE* aLayoutFile );
};
/**
* @Class IDF_LIB
* stores information on IDF models ( also has an inbuilt NOMODEL model )
* and is responsible for writing the ELECTRICAL sections of the library file
* (*.emp) and the PLACEMENT section of the board file.
*/
class IDF_LIB
{
// TODO: IMPLEMENT
public:
/**
* Function WriteLib
* writes all current library information to the output file
*/
bool WriteLib( FILE* aLibFile );
// write placement information to the board file
bool WriteBrd( FILE* aLayoutFile );
// bool Finish( void )
// {
// TODO: Write out the library (*.emp) file
// idf_lib.Write( lib_file );
// TODO: fclose( lib_file );
// }
};
#endif // IDF_H
...@@ -204,6 +204,11 @@ void PCB_EDIT_FRAME::ReCreateMenuBar() ...@@ -204,6 +204,11 @@ void PCB_EDIT_FRAME::ReCreateMenuBar()
_( "Export a VRML board representation" ), _( "Export a VRML board representation" ),
KiBitmap( three_d_xpm ) ); KiBitmap( three_d_xpm ) );
// IDF3
AddMenuItem( submenuexport, ID_GEN_EXPORT_FILE_IDF3,
_( "I&DFv3 Board Shape Export" ), _( "Basci export of board shape only IDFv3 format" ),
KiBitmap( export_xpm ) );
AddMenuItem( filesMenu, submenuexport, AddMenuItem( filesMenu, submenuexport,
ID_GEN_EXPORT_FILE, _( "E&xport" ), ID_GEN_EXPORT_FILE, _( "E&xport" ),
_( "Export board" ), KiBitmap( export_xpm ) ); _( "Export board" ), KiBitmap( export_xpm ) );
......
...@@ -115,6 +115,7 @@ BEGIN_EVENT_TABLE( PCB_EDIT_FRAME, PCB_BASE_FRAME ) ...@@ -115,6 +115,7 @@ BEGIN_EVENT_TABLE( PCB_EDIT_FRAME, PCB_BASE_FRAME )
EVT_MENU( ID_GEN_EXPORT_FILE_GENCADFORMAT, PCB_EDIT_FRAME::ExportToGenCAD ) EVT_MENU( ID_GEN_EXPORT_FILE_GENCADFORMAT, PCB_EDIT_FRAME::ExportToGenCAD )
EVT_MENU( ID_GEN_EXPORT_FILE_MODULE_REPORT, PCB_EDIT_FRAME::GenFootprintsReport ) EVT_MENU( ID_GEN_EXPORT_FILE_MODULE_REPORT, PCB_EDIT_FRAME::GenFootprintsReport )
EVT_MENU( ID_GEN_EXPORT_FILE_VRML, PCB_EDIT_FRAME::OnExportVRML ) EVT_MENU( ID_GEN_EXPORT_FILE_VRML, PCB_EDIT_FRAME::OnExportVRML )
EVT_MENU( ID_GEN_EXPORT_FILE_IDF3, PCB_EDIT_FRAME::ExportToIDF3 )
EVT_MENU( ID_GEN_IMPORT_SPECCTRA_SESSION,PCB_EDIT_FRAME::ImportSpecctraSession ) EVT_MENU( ID_GEN_IMPORT_SPECCTRA_SESSION,PCB_EDIT_FRAME::ImportSpecctraSession )
EVT_MENU( ID_GEN_IMPORT_SPECCTRA_DESIGN, PCB_EDIT_FRAME::ImportSpecctraDesign ) EVT_MENU( ID_GEN_IMPORT_SPECCTRA_DESIGN, PCB_EDIT_FRAME::ImportSpecctraDesign )
......
...@@ -246,6 +246,7 @@ enum pcbnew_ids ...@@ -246,6 +246,7 @@ enum pcbnew_ids
ID_MENU_PCB_SWAP_LAYERS, ID_MENU_PCB_SWAP_LAYERS,
ID_MENU_PCB_RESET_TEXTMODULE_FIELDS_SIZES, ID_MENU_PCB_RESET_TEXTMODULE_FIELDS_SIZES,
ID_GEN_EXPORT_FILE_IDF3,
ID_GEN_EXPORT_FILE_VRML, ID_GEN_EXPORT_FILE_VRML,
ID_GEN_EXPORT_SPECCTRA, ID_GEN_EXPORT_SPECCTRA,
ID_GEN_EXPORT_FILE_GENCADFORMAT, ID_GEN_EXPORT_FILE_GENCADFORMAT,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment