2023-09-06 11:58:39 +00:00
|
|
|
/*
|
|
|
|
* This program source code file is part of KiCad, a free EDA CAD application.
|
|
|
|
*
|
|
|
|
* Copyright (C) 2023 Alex Shvartzkop <dudesuchamazing@gmail.com>
|
|
|
|
* Copyright (C) 2023 KiCad Developers, see AUTHORS.txt for contributors.
|
|
|
|
*
|
|
|
|
* This program is free software; you can redistribute it and/or
|
|
|
|
* modify it under the terms of the GNU General Public License
|
|
|
|
* as published by the Free Software Foundation; either version 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
|
|
|
|
*/
|
|
|
|
|
2023-12-19 17:39:26 +00:00
|
|
|
#include <io/easyedapro/easyedapro_import_utils.h>
|
|
|
|
#include <io/easyedapro/easyedapro_parser.h>
|
2023-12-24 01:21:58 +00:00
|
|
|
#include <pcb_io/easyedapro/pcb_io_easyedapro.h>
|
|
|
|
#include <pcb_io/easyedapro/pcb_io_easyedapro_parser.h>
|
2023-12-19 17:39:26 +00:00
|
|
|
#include <pcb_io/pcb_io.h>
|
2023-09-06 11:58:39 +00:00
|
|
|
|
|
|
|
#include <board.h>
|
|
|
|
#include <footprint.h>
|
|
|
|
#include <progress_reporter.h>
|
|
|
|
#include <common.h>
|
|
|
|
#include <macros.h>
|
|
|
|
|
|
|
|
#include <fstream>
|
|
|
|
#include <wx/txtstrm.h>
|
|
|
|
#include <wx/wfstream.h>
|
|
|
|
#include <wx/mstream.h>
|
|
|
|
#include <wx/zipstrm.h>
|
|
|
|
#include <wx/log.h>
|
|
|
|
|
2024-03-14 02:49:01 +00:00
|
|
|
#include <json_common.h>
|
2023-09-06 11:58:39 +00:00
|
|
|
#include <core/json_serializers.h>
|
|
|
|
#include <core/map_helpers.h>
|
2023-10-30 06:34:45 +00:00
|
|
|
#include <string_utf8_map.h>
|
2023-09-06 11:58:39 +00:00
|
|
|
|
|
|
|
|
2023-12-24 01:21:58 +00:00
|
|
|
struct PCB_IO_EASYEDAPRO::PRJ_DATA
|
2023-09-06 11:58:39 +00:00
|
|
|
{
|
|
|
|
std::map<wxString, std::unique_ptr<FOOTPRINT>> m_Footprints;
|
|
|
|
std::map<wxString, EASYEDAPRO::BLOB> m_Blobs;
|
|
|
|
std::map<wxString, std::multimap<wxString, EASYEDAPRO::POURED>> m_Poured;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2023-12-24 04:31:22 +00:00
|
|
|
PCB_IO_EASYEDAPRO::PCB_IO_EASYEDAPRO() : PCB_IO( wxS( "EasyEDA (JLCEDA) Professional" ) )
|
2023-09-06 11:58:39 +00:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-12-24 01:21:58 +00:00
|
|
|
PCB_IO_EASYEDAPRO::~PCB_IO_EASYEDAPRO()
|
2023-09-06 11:58:39 +00:00
|
|
|
{
|
|
|
|
if( m_projectData )
|
|
|
|
delete m_projectData;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-12-24 01:21:58 +00:00
|
|
|
bool PCB_IO_EASYEDAPRO::CanReadBoard( const wxString& aFileName ) const
|
2023-09-06 11:58:39 +00:00
|
|
|
{
|
|
|
|
if( aFileName.Lower().EndsWith( wxS( ".epro" ) ) )
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
else if( aFileName.Lower().EndsWith( wxS( ".zip" ) ) )
|
|
|
|
{
|
|
|
|
std::shared_ptr<wxZipEntry> entry;
|
|
|
|
wxFFileInputStream in( aFileName );
|
|
|
|
wxZipInputStream zip( in );
|
|
|
|
|
|
|
|
if( !zip.IsOk() )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
while( entry.reset( zip.GetNextEntry() ), entry.get() != NULL )
|
|
|
|
{
|
|
|
|
wxString name = entry->GetName();
|
|
|
|
|
|
|
|
if( name == wxS( "project.json" ) )
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-12-24 01:21:58 +00:00
|
|
|
BOARD* PCB_IO_EASYEDAPRO::LoadBoard( const wxString& aFileName, BOARD* aAppendToMe,
|
2023-12-27 17:06:23 +00:00
|
|
|
const STRING_UTF8_MAP* aProperties, PROJECT* aProject )
|
2023-09-06 11:58:39 +00:00
|
|
|
{
|
|
|
|
m_props = aProperties;
|
|
|
|
|
|
|
|
m_board = aAppendToMe ? aAppendToMe : new BOARD();
|
|
|
|
|
|
|
|
// Give the filename to the board if it's new
|
|
|
|
if( !aAppendToMe )
|
|
|
|
m_board->SetFileName( aFileName );
|
|
|
|
|
2023-12-27 17:06:23 +00:00
|
|
|
if( m_progressReporter )
|
2023-09-06 11:58:39 +00:00
|
|
|
{
|
2023-12-27 17:06:23 +00:00
|
|
|
m_progressReporter->Report( wxString::Format( _( "Loading %s..." ), aFileName ) );
|
2023-09-06 11:58:39 +00:00
|
|
|
|
2023-12-27 17:06:23 +00:00
|
|
|
if( !m_progressReporter->KeepRefreshing() )
|
2023-09-06 11:58:39 +00:00
|
|
|
THROW_IO_ERROR( _( "Open cancelled by user." ) );
|
|
|
|
}
|
|
|
|
|
2023-12-24 01:21:58 +00:00
|
|
|
PCB_IO_EASYEDAPRO_PARSER parser( nullptr, nullptr );
|
2023-09-06 11:58:39 +00:00
|
|
|
|
|
|
|
wxFileName fname( aFileName );
|
|
|
|
|
|
|
|
if( fname.GetExt() == wxS( "epro" ) || fname.GetExt() == wxS( "zip" ) )
|
|
|
|
{
|
2023-12-25 04:00:11 +00:00
|
|
|
nlohmann::json project = EASYEDAPRO::ReadProjectOrDeviceFile( aFileName );
|
2023-09-06 11:58:39 +00:00
|
|
|
|
|
|
|
wxString pcbToLoad;
|
|
|
|
|
2023-10-30 06:34:45 +00:00
|
|
|
if( m_props && m_props->Exists( "pcb_id" ) )
|
2023-09-06 11:58:39 +00:00
|
|
|
{
|
2023-11-02 00:49:08 +00:00
|
|
|
pcbToLoad = wxString::FromUTF8( aProperties->at( "pcb_id" ) );
|
2023-09-06 11:58:39 +00:00
|
|
|
}
|
2023-10-30 06:34:45 +00:00
|
|
|
else
|
2023-09-06 11:58:39 +00:00
|
|
|
{
|
2023-10-30 06:34:45 +00:00
|
|
|
std::map<wxString, wxString> prjPcbNames = project.at( "pcbs" );
|
|
|
|
|
|
|
|
if( prjPcbNames.size() == 1 )
|
|
|
|
{
|
|
|
|
pcbToLoad = prjPcbNames.begin()->first;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
std::vector<IMPORT_PROJECT_DESC> chosen = m_choose_project_handler(
|
|
|
|
EASYEDAPRO::ProjectToSelectorDialog( project, true, false ) );
|
|
|
|
|
|
|
|
if( chosen.size() > 0 )
|
|
|
|
pcbToLoad = chosen[0].PCBId;
|
|
|
|
}
|
2023-09-06 11:58:39 +00:00
|
|
|
}
|
|
|
|
|
2023-10-30 06:34:45 +00:00
|
|
|
if( pcbToLoad.empty() )
|
2023-09-06 11:58:39 +00:00
|
|
|
return nullptr;
|
|
|
|
|
|
|
|
LoadAllDataFromProject( aFileName, project );
|
|
|
|
|
|
|
|
if( !m_projectData )
|
|
|
|
return nullptr;
|
|
|
|
|
|
|
|
auto cb = [&]( const wxString& name, const wxString& pcbUuid, wxInputStream& zip ) -> bool
|
|
|
|
{
|
|
|
|
if( !name.EndsWith( wxS( ".epcb" ) ) )
|
|
|
|
EASY_IT_CONTINUE;
|
|
|
|
|
|
|
|
if( pcbUuid != pcbToLoad )
|
|
|
|
EASY_IT_CONTINUE;
|
|
|
|
|
|
|
|
std::vector<nlohmann::json> lines = EASYEDAPRO::ParseJsonLines( zip, name );
|
|
|
|
|
|
|
|
wxString boardKey = pcbUuid + wxS( "_0" );
|
|
|
|
wxScopedCharBuffer cb = boardKey.ToUTF8();
|
|
|
|
wxString boardPouredKey = wxBase64Encode( cb.data(), cb.length() );
|
|
|
|
|
|
|
|
const std::multimap<wxString, EASYEDAPRO::POURED>& boardPoured =
|
|
|
|
get_def( m_projectData->m_Poured, boardPouredKey );
|
|
|
|
|
|
|
|
parser.ParseBoard( m_board, project, m_projectData->m_Footprints,
|
|
|
|
m_projectData->m_Blobs, boardPoured, lines,
|
|
|
|
EASYEDAPRO::ShortenLibName( fname.GetName() ) );
|
|
|
|
|
|
|
|
EASY_IT_BREAK;
|
|
|
|
};
|
|
|
|
EASYEDAPRO::IterateZipFiles( aFileName, cb );
|
|
|
|
}
|
|
|
|
|
|
|
|
return m_board;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-12-24 01:21:58 +00:00
|
|
|
long long PCB_IO_EASYEDAPRO::GetLibraryTimestamp( const wxString& aLibraryPath ) const
|
2023-09-06 11:58:39 +00:00
|
|
|
{
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-12-24 01:21:58 +00:00
|
|
|
void PCB_IO_EASYEDAPRO::FootprintEnumerate( wxArrayString& aFootprintNames,
|
2023-09-06 11:58:39 +00:00
|
|
|
const wxString& aLibraryPath, bool aBestEfforts,
|
|
|
|
const STRING_UTF8_MAP* aProperties )
|
|
|
|
{
|
|
|
|
wxFileName fname( aLibraryPath );
|
|
|
|
|
|
|
|
if( fname.GetExt() == wxS( "efoo" ) )
|
|
|
|
{
|
|
|
|
wxFFileInputStream ffis( aLibraryPath );
|
|
|
|
wxTextInputStream txt( ffis, wxS( " " ), wxConvUTF8 );
|
|
|
|
|
|
|
|
while( ffis.CanRead() )
|
|
|
|
{
|
|
|
|
wxString line = txt.ReadLine();
|
|
|
|
|
|
|
|
if( !line.Contains( wxS( "ATTR" ) ) )
|
|
|
|
continue; // Don't bother parsing
|
|
|
|
|
|
|
|
nlohmann::json js = nlohmann::json::parse( line );
|
|
|
|
if( js.at( 0 ) == "ATTR" && js.at( 7 ) == "Footprint" )
|
|
|
|
{
|
|
|
|
aFootprintNames.Add( js.at( 8 ).get<wxString>() );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-12-25 04:00:11 +00:00
|
|
|
else if( fname.GetExt() == wxS( "elibz" ) || fname.GetExt() == wxS( "epro" )
|
|
|
|
|| fname.GetExt() == wxS( "zip" ) )
|
2023-09-06 11:58:39 +00:00
|
|
|
{
|
2023-12-25 04:00:11 +00:00
|
|
|
nlohmann::json project = EASYEDAPRO::ReadProjectOrDeviceFile( aLibraryPath );
|
2023-09-06 11:58:39 +00:00
|
|
|
std::map<wxString, nlohmann::json> footprintMap = project.at( "footprints" );
|
|
|
|
|
|
|
|
for( auto& [key, value] : footprintMap )
|
2023-12-25 04:00:11 +00:00
|
|
|
{
|
|
|
|
wxString title;
|
|
|
|
|
|
|
|
if( value.contains( "display_title" ) )
|
|
|
|
title = value.at( "display_title" ).get<wxString>();
|
|
|
|
else
|
|
|
|
title = value.at( "title" ).get<wxString>();
|
|
|
|
|
|
|
|
aFootprintNames.Add( title );
|
|
|
|
}
|
2023-09-06 11:58:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-12-24 01:21:58 +00:00
|
|
|
void PCB_IO_EASYEDAPRO::LoadAllDataFromProject( const wxString& aProjectPath,
|
2023-09-06 11:58:39 +00:00
|
|
|
const nlohmann::json& aProject )
|
|
|
|
{
|
|
|
|
if( m_projectData )
|
|
|
|
delete m_projectData;
|
|
|
|
|
|
|
|
m_projectData = new PRJ_DATA();
|
|
|
|
|
2023-12-24 01:21:58 +00:00
|
|
|
PCB_IO_EASYEDAPRO_PARSER parser( nullptr, nullptr );
|
2023-09-06 11:58:39 +00:00
|
|
|
wxFileName fname( aProjectPath );
|
|
|
|
wxString fpLibName = EASYEDAPRO::ShortenLibName( fname.GetName() );
|
|
|
|
|
|
|
|
std::map<wxString, std::unique_ptr<FOOTPRINT>> result;
|
|
|
|
|
|
|
|
auto cb = [&]( const wxString& name, const wxString& baseName, wxInputStream& zip ) -> bool
|
|
|
|
{
|
|
|
|
if( !name.EndsWith( wxS( ".efoo" ) ) && !name.EndsWith( wxS( ".eblob" ) )
|
|
|
|
&& !name.EndsWith( wxS( ".ecop" ) ) )
|
|
|
|
{
|
|
|
|
EASY_IT_CONTINUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::vector<nlohmann::json> lines = EASYEDAPRO::ParseJsonLines( zip, name );
|
|
|
|
|
|
|
|
if( name.EndsWith( wxS( ".efoo" ) ) )
|
|
|
|
{
|
|
|
|
nlohmann::json fpData = aProject.at( "footprints" ).at( baseName );
|
|
|
|
wxString fpTitle = fpData.at( "title" );
|
|
|
|
|
|
|
|
FOOTPRINT* footprint = parser.ParseFootprint( aProject, baseName, lines );
|
|
|
|
|
|
|
|
if( !footprint )
|
|
|
|
EASY_IT_CONTINUE;
|
|
|
|
|
|
|
|
LIB_ID fpID = EASYEDAPRO::ToKiCadLibID( fpLibName, fpTitle );
|
|
|
|
footprint->SetFPID( fpID );
|
|
|
|
|
|
|
|
m_projectData->m_Footprints.emplace( baseName, footprint );
|
|
|
|
}
|
|
|
|
else if( name.EndsWith( wxS( ".eblob" ) ) )
|
|
|
|
{
|
|
|
|
for( const nlohmann::json& line : lines )
|
|
|
|
{
|
|
|
|
if( line.at( 0 ) == "BLOB" )
|
|
|
|
{
|
|
|
|
EASYEDAPRO::BLOB blob = line;
|
|
|
|
m_projectData->m_Blobs[blob.objectId] = blob;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if( name.EndsWith( wxS( ".ecop" ) ) && EASYEDAPRO::IMPORT_POURED )
|
|
|
|
{
|
|
|
|
for( const nlohmann::json& line : lines )
|
|
|
|
{
|
|
|
|
if( line.at( 0 ) == "POURED" )
|
|
|
|
{
|
|
|
|
if( !line.at( 2 ).is_string() )
|
|
|
|
continue; // Unknown type of POURED
|
|
|
|
|
|
|
|
EASYEDAPRO::POURED poured = line;
|
|
|
|
m_projectData->m_Poured[baseName].emplace( poured.parentId, poured );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
EASY_IT_CONTINUE;
|
|
|
|
};
|
|
|
|
EASYEDAPRO::IterateZipFiles( aProjectPath, cb );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-12-24 01:21:58 +00:00
|
|
|
FOOTPRINT* PCB_IO_EASYEDAPRO::FootprintLoad( const wxString& aLibraryPath,
|
2023-09-06 11:58:39 +00:00
|
|
|
const wxString& aFootprintName, bool aKeepUUID,
|
|
|
|
const STRING_UTF8_MAP* aProperties )
|
|
|
|
{
|
2023-12-24 01:21:58 +00:00
|
|
|
PCB_IO_EASYEDAPRO_PARSER parser( nullptr, nullptr );
|
2023-09-06 11:58:39 +00:00
|
|
|
FOOTPRINT* footprint = nullptr;
|
|
|
|
|
|
|
|
wxFileName libFname( aLibraryPath );
|
|
|
|
|
|
|
|
if( libFname.GetExt() == wxS( "efoo" ) )
|
|
|
|
{
|
|
|
|
wxFFileInputStream ffis( aLibraryPath );
|
|
|
|
wxTextInputStream txt( ffis, wxS( " " ), wxConvUTF8 );
|
|
|
|
|
|
|
|
std::vector<nlohmann::json> lines = EASYEDAPRO::ParseJsonLines( ffis, aLibraryPath );
|
|
|
|
|
|
|
|
for( const nlohmann::json& js : lines )
|
|
|
|
{
|
|
|
|
if( js.at( 0 ) == "ATTR" )
|
|
|
|
{
|
|
|
|
EASYEDAPRO::PCB_ATTR attr = js;
|
|
|
|
|
|
|
|
if( attr.key == wxS( "Footprint" ) && attr.value != aFootprintName )
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
footprint = parser.ParseFootprint( nlohmann::json(), wxEmptyString, lines );
|
|
|
|
|
|
|
|
if( !footprint )
|
|
|
|
{
|
|
|
|
THROW_IO_ERROR( wxString::Format( _( "Cannot load footprint '%s' from '%s'" ),
|
|
|
|
aFootprintName, aLibraryPath ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
LIB_ID fpID = EASYEDAPRO::ToKiCadLibID( wxEmptyString, aFootprintName );
|
|
|
|
footprint->SetFPID( fpID );
|
|
|
|
|
|
|
|
footprint->Reference().SetVisible( true );
|
|
|
|
footprint->Value().SetText( aFootprintName );
|
|
|
|
footprint->Value().SetVisible( true );
|
|
|
|
footprint->AutoPositionFields();
|
|
|
|
}
|
2023-12-25 04:00:11 +00:00
|
|
|
else if( libFname.GetExt() == wxS( "elibz" ) || libFname.GetExt() == wxS( "epro" )
|
|
|
|
|| libFname.GetExt() == wxS( "zip" ) )
|
2023-09-06 11:58:39 +00:00
|
|
|
{
|
2023-12-25 04:00:11 +00:00
|
|
|
nlohmann::json project = EASYEDAPRO::ReadProjectOrDeviceFile( aLibraryPath );
|
2023-09-06 11:58:39 +00:00
|
|
|
|
|
|
|
wxString fpUuid;
|
|
|
|
|
|
|
|
std::map<wxString, nlohmann::json> footprintMap = project.at( "footprints" );
|
|
|
|
for( auto& [uuid, data] : footprintMap )
|
|
|
|
{
|
2023-12-25 04:00:11 +00:00
|
|
|
wxString title;
|
|
|
|
|
|
|
|
if( data.contains( "display_title" ) )
|
|
|
|
title = data.at( "display_title" ).get<wxString>();
|
|
|
|
else
|
|
|
|
title = data.at( "title" ).get<wxString>();
|
2023-09-06 11:58:39 +00:00
|
|
|
|
|
|
|
if( title == aFootprintName )
|
|
|
|
{
|
|
|
|
fpUuid = uuid;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if( !fpUuid )
|
|
|
|
{
|
|
|
|
THROW_IO_ERROR( wxString::Format( _( "Footprint '%s' not found in project '%s'" ),
|
|
|
|
aFootprintName, aLibraryPath ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
auto cb = [&]( const wxString& name, const wxString& baseName, wxInputStream& zip ) -> bool
|
|
|
|
{
|
|
|
|
if( !name.EndsWith( wxS( ".efoo" ) ) )
|
|
|
|
EASY_IT_CONTINUE;
|
|
|
|
|
|
|
|
if( baseName != fpUuid )
|
|
|
|
EASY_IT_CONTINUE;
|
|
|
|
|
|
|
|
std::vector<nlohmann::json> lines = EASYEDAPRO::ParseJsonLines( zip, name );
|
|
|
|
|
|
|
|
footprint = parser.ParseFootprint( project, fpUuid, lines );
|
|
|
|
|
|
|
|
if( !footprint )
|
|
|
|
{
|
|
|
|
THROW_IO_ERROR( wxString::Format( _( "Cannot load footprint '%s' from '%s'" ),
|
|
|
|
aFootprintName, aLibraryPath ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
LIB_ID fpID = EASYEDAPRO::ToKiCadLibID( wxEmptyString, aFootprintName );
|
|
|
|
footprint->SetFPID( fpID );
|
|
|
|
|
|
|
|
footprint->Reference().SetVisible( true );
|
|
|
|
footprint->Value().SetText( aFootprintName );
|
|
|
|
footprint->Value().SetVisible( true );
|
|
|
|
footprint->AutoPositionFields();
|
|
|
|
|
|
|
|
EASY_IT_BREAK;
|
|
|
|
};
|
|
|
|
EASYEDAPRO::IterateZipFiles( aLibraryPath, cb );
|
|
|
|
}
|
|
|
|
|
|
|
|
return footprint;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-12-24 01:21:58 +00:00
|
|
|
std::vector<FOOTPRINT*> PCB_IO_EASYEDAPRO::GetImportedCachedLibraryFootprints()
|
2023-09-06 11:58:39 +00:00
|
|
|
{
|
|
|
|
std::vector<FOOTPRINT*> result;
|
|
|
|
|
|
|
|
if( !m_projectData )
|
|
|
|
return result;
|
|
|
|
|
|
|
|
for( auto& [fpUuid, footprint] : m_projectData->m_Footprints )
|
|
|
|
{
|
|
|
|
result.push_back( static_cast<FOOTPRINT*>( footprint->Clone() ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|