From d84af5ed4dd0b25dcf2dd889365cf3e00c21dc5d Mon Sep 17 00:00:00 2001 From: Lukas K Date: Wed, 16 Mar 2022 23:32:36 +0100 Subject: [PATCH] add ODB++ output --- Makefile | 13 + scripts/app_versions.yml | 1 + src/board/board.cpp | 32 +- src/board/board.hpp | 6 + src/board/odb_output_settings.cpp | 33 + src/board/odb_output_settings.hpp | 28 + src/core/core_board.cpp | 9 +- src/core/core_board.hpp | 5 + src/document/idocument_board.hpp | 1 + src/export_odb/attribute_util.cpp | 82 ++ src/export_odb/attribute_util.hpp | 64 ++ src/export_odb/attributes.hpp | 103 ++ src/export_odb/canvas_odb.cpp | 249 +++++ src/export_odb/canvas_odb.hpp | 58 ++ src/export_odb/components.cpp | 35 + src/export_odb/components.hpp | 47 + src/export_odb/db.cpp | 222 +++++ src/export_odb/db.hpp | 118 +++ src/export_odb/eda_data.cpp | 374 +++++++ src/export_odb/eda_data.hpp | 261 +++++ src/export_odb/features.cpp | 178 ++++ src/export_odb/features.hpp | 164 +++ src/export_odb/odb_export.cpp | 238 +++++ src/export_odb/odb_export.hpp | 9 + src/export_odb/odb_util.cpp | 132 +++ src/export_odb/odb_util.hpp | 62 ++ src/export_odb/structured_text_writer.cpp | 52 + src/export_odb/structured_text_writer.hpp | 45 + src/export_odb/surface_data.cpp | 59 ++ src/export_odb/surface_data.hpp | 50 + src/export_odb/symbol.cpp | 32 + src/export_odb/symbol.hpp | 21 + src/export_odb/track_graph.cpp | 118 +++ src/export_odb/track_graph.hpp | 38 + src/imp/fab_output.ui | 1101 ++++++++++++++------- src/imp/fab_output_window.cpp | 215 +++- src/imp/fab_output_window.hpp | 38 + src/python_module/board.cpp | 36 + 38 files changed, 3933 insertions(+), 396 deletions(-) create mode 100644 src/board/odb_output_settings.cpp create mode 100644 src/board/odb_output_settings.hpp create mode 100644 src/export_odb/attribute_util.cpp create mode 100644 src/export_odb/attribute_util.hpp create mode 100644 src/export_odb/attributes.hpp create mode 100644 src/export_odb/canvas_odb.cpp create mode 100644 src/export_odb/canvas_odb.hpp create mode 100644 src/export_odb/components.cpp create mode 100644 src/export_odb/components.hpp create mode 100644 src/export_odb/db.cpp create mode 100644 src/export_odb/db.hpp create mode 100644 src/export_odb/eda_data.cpp create mode 100644 src/export_odb/eda_data.hpp create mode 100644 src/export_odb/features.cpp create mode 100644 src/export_odb/features.hpp create mode 100644 src/export_odb/odb_export.cpp create mode 100644 src/export_odb/odb_export.hpp create mode 100644 src/export_odb/odb_util.cpp create mode 100644 src/export_odb/odb_util.hpp create mode 100644 src/export_odb/structured_text_writer.cpp create mode 100644 src/export_odb/structured_text_writer.hpp create mode 100644 src/export_odb/surface_data.cpp create mode 100644 src/export_odb/surface_data.hpp create mode 100644 src/export_odb/symbol.cpp create mode 100644 src/export_odb/symbol.hpp create mode 100644 src/export_odb/track_graph.cpp create mode 100644 src/export_odb/track_graph.hpp diff --git a/Makefile b/Makefile index ad29b9b39..99aff8583 100644 --- a/Makefile +++ b/Makefile @@ -82,6 +82,7 @@ SRC_COMMON = \ 3rd_party/delaunator/delaunator.cpp\ src/board/airwires.cpp\ src/board/gerber_output_settings.cpp\ + src/board/odb_output_settings.cpp\ src/board/board_hole.cpp\ src/board/connection_line.cpp\ src/board/step_export_settings.cpp\ @@ -203,6 +204,18 @@ SRC_EXPORT = \ src/export_pdf/export_pdf_util.cpp\ src/export_pnp/export_pnp.cpp \ src/export_bom/export_bom.cpp\ + src/export_odb/odb_export.cpp\ + src/export_odb/canvas_odb.cpp\ + src/export_odb/db.cpp\ + src/export_odb/eda_data.cpp\ + src/export_odb/attribute_util.cpp\ + src/export_odb/surface_data.cpp\ + src/export_odb/features.cpp\ + src/export_odb/components.cpp\ + src/export_odb/symbol.cpp\ + src/export_odb/odb_util.cpp\ + src/export_odb/track_graph.cpp\ + src/export_odb/structured_text_writer.cpp\ SRC_CANVAS_GL = \ $(SRC_CANVAS) \ diff --git a/scripts/app_versions.yml b/scripts/app_versions.yml index 0f3dcd435..a4f572a7c 100644 --- a/scripts/app_versions.yml +++ b/scripts/app_versions.yml @@ -12,6 +12,7 @@ versions: 10: add thermal rules 11: add thermal spoke customisation 12: add net ties + 13: add ODB++ export schematic: 1: add custom values on symbols 2: add hierarchy diff --git a/src/board/board.cpp b/src/board/board.cpp index 15b243af8..9d6b67e87 100644 --- a/src/board/board.cpp +++ b/src/board/board.cpp @@ -34,7 +34,12 @@ BoardColors::BoardColors() : solder_mask({0, .5, 0}), silkscreen({1, 1, 1}), sub { } -static const unsigned int app_version = 12; +const LutEnumStr Board::output_format_lut = { + {"gerber", Board::OutputFormat::GERBER}, + {"odb", Board::OutputFormat::ODB}, +}; + +static const unsigned int app_version = 13; unsigned int Board::get_app_version() { @@ -247,6 +252,17 @@ Board::Board(const UUID &uu, const json &j, Block &iblock, IPool &pool) Logger::log_warning("couldn't load fab output settings", Logger::Domain::BOARD, e.what()); } } + if (j.count("odb_output_settings")) { + try { + odb_output_settings = ODBOutputSettings(j.at("odb_output_settings")); + } + catch (const std::exception &e) { + Logger::log_warning("couldn't load ODB++ output settings", Logger::Domain::BOARD, e.what()); + } + } + if (j.count("output_format")) { + output_format = output_format_lut.lookup(j.at("output_format")); + } if (j.count("colors")) { try { const auto &o = j.at("colors"); @@ -293,6 +309,7 @@ Board::Board(const UUID &uu, const json &j, Block &iblock, IPool &pool) } } gerber_output_settings.update_for_board(*this); + odb_output_settings.update_for_board(*this); update_pdf_export_settings(pdf_export_settings); update_refs(); // fill in smashed texts } @@ -342,11 +359,11 @@ Board::Board(const Board &brd, CopyMode copy_mode) junctions(brd.junctions), tracks(brd.tracks), texts(brd.texts), lines(brd.lines), arcs(brd.arcs), planes(brd.planes), keepouts(brd.keepouts), dimensions(brd.dimensions), connection_lines(brd.connection_lines), included_boards(brd.included_boards), board_panels(brd.board_panels), pictures(brd.pictures), decals(brd.decals), - net_ties(brd.net_ties), warnings(brd.warnings), rules(brd.rules), - gerber_output_settings(brd.gerber_output_settings), grid_settings(brd.grid_settings), airwires(brd.airwires), - stackup(brd.stackup), colors(brd.colors), pdf_export_settings(brd.pdf_export_settings), - step_export_settings(brd.step_export_settings), pnp_export_settings(brd.pnp_export_settings), - version(brd.version), n_inner_layers(brd.n_inner_layers) + net_ties(brd.net_ties), warnings(brd.warnings), output_format(brd.output_format), rules(brd.rules), + gerber_output_settings(brd.gerber_output_settings), odb_output_settings(brd.odb_output_settings), + grid_settings(brd.grid_settings), airwires(brd.airwires), stackup(brd.stackup), colors(brd.colors), + pdf_export_settings(brd.pdf_export_settings), step_export_settings(brd.step_export_settings), + pnp_export_settings(brd.pnp_export_settings), version(brd.version), n_inner_layers(brd.n_inner_layers) { if (copy_mode == CopyMode::DEEP) { packages = brd.packages; @@ -518,6 +535,7 @@ void Board::set_n_inner_layers(unsigned int n) } map_erase_if(stackup, [this](const auto &x) { return layers.count(x.first) == 0; }); gerber_output_settings.update_for_board(*this); + odb_output_settings.update_for_board(*this); update_pdf_export_settings(pdf_export_settings); } @@ -1124,6 +1142,8 @@ json Board::serialize() const j["n_inner_layers"] = n_inner_layers; j["rules"] = rules.serialize(); j["fab_output_settings"] = gerber_output_settings.serialize(); + j["odb_output_settings"] = odb_output_settings.serialize(); + j["output_format"] = output_format_lut.lookup_reverse(output_format); { json o; o["solder_mask"] = color_to_json(colors.solder_mask); diff --git a/src/board/board.hpp b/src/board/board.hpp index 67095ccb4..7b8a4d54c 100644 --- a/src/board/board.hpp +++ b/src/board/board.hpp @@ -12,6 +12,7 @@ #include "common/keepout.hpp" #include "common/pdf_export_settings.hpp" #include "gerber_output_settings.hpp" +#include "odb_output_settings.hpp" #include "nlohmann/json_fwd.hpp" #include "plane.hpp" #include "track.hpp" @@ -117,8 +118,13 @@ class Board : public ObjectProvider, public LayerProvider { std::vector warnings; + enum class OutputFormat { GERBER, ODB }; + OutputFormat output_format = OutputFormat::GERBER; + static const LutEnumStr output_format_lut; + BoardRules rules; GerberOutputSettings gerber_output_settings; + ODBOutputSettings odb_output_settings; GridSettings grid_settings; std::map> airwires; diff --git a/src/board/odb_output_settings.cpp b/src/board/odb_output_settings.cpp new file mode 100644 index 000000000..38cc905e6 --- /dev/null +++ b/src/board/odb_output_settings.cpp @@ -0,0 +1,33 @@ +#include "odb_output_settings.hpp" +#include "common/lut.hpp" +#include "nlohmann/json.hpp" + +namespace horizon { + +const LutEnumStr format_lut = { + {"directory", ODBOutputSettings::Format::DIRECTORY}, + {"zip", ODBOutputSettings::Format::ZIP}, + {"tgz", ODBOutputSettings::Format::TGZ}, +}; + +ODBOutputSettings::ODBOutputSettings(const json &j) + : format(format_lut.lookup(j.at("format"))), job_name(j.at("job_name").get()), + output_filename(j.at("output_filename").get()), + output_directory(j.at("output_directory").get()) +{ +} + +void ODBOutputSettings::update_for_board(const Board &brd) +{ +} + +json ODBOutputSettings::serialize() const +{ + json j; + j["format"] = format_lut.lookup_reverse(format); + j["job_name"] = job_name; + j["output_filename"] = output_filename; + j["output_directory"] = output_directory; + return j; +} +} // namespace horizon diff --git a/src/board/odb_output_settings.hpp b/src/board/odb_output_settings.hpp new file mode 100644 index 000000000..7b325ee7f --- /dev/null +++ b/src/board/odb_output_settings.hpp @@ -0,0 +1,28 @@ +#pragma once +#include "common/common.hpp" +#include "common/lut.hpp" +#include "nlohmann/json_fwd.hpp" +#include "util/uuid.hpp" + +namespace horizon { +using json = nlohmann::json; + +class ODBOutputSettings { +public: + ODBOutputSettings(const json &); + ODBOutputSettings() + { + } + json serialize() const; + void update_for_board(const class Board &brd); + + enum class Format { DIRECTORY, TGZ, ZIP }; + + Format format = Format::TGZ; + + std::string job_name; + + std::string output_filename; + std::string output_directory; +}; +} // namespace horizon diff --git a/src/core/core_board.cpp b/src/core/core_board.cpp index 1710023cc..e10b39138 100644 --- a/src/core/core_board.cpp +++ b/src/core/core_board.cpp @@ -48,10 +48,10 @@ CoreBoard::CoreBoard(const std::string &board_filename, const std::string &block const std::string &pictures_dir, IPool &pool, IPool &pool_caching) : Core(pool, &pool_caching), block(get_flattend_block(blocks_filename, pool_caching)), brd(Board::new_from_file(board_filename, *block, pool_caching)), rules(brd->rules), - gerber_output_settings(brd->gerber_output_settings), pdf_export_settings(brd->pdf_export_settings), - step_export_settings(brd->step_export_settings), pnp_export_settings(brd->pnp_export_settings), - grid_settings(brd->grid_settings), colors(brd->colors), m_board_filename(board_filename), - m_blocks_filename(blocks_filename), m_pictures_dir(pictures_dir) + gerber_output_settings(brd->gerber_output_settings), odb_output_settings(brd->odb_output_settings), + pdf_export_settings(brd->pdf_export_settings), step_export_settings(brd->step_export_settings), + pnp_export_settings(brd->pnp_export_settings), grid_settings(brd->grid_settings), colors(brd->colors), + m_board_filename(board_filename), m_blocks_filename(blocks_filename), m_pictures_dir(pictures_dir) { brd->load_pictures(pictures_dir); rebuild("init"); @@ -811,6 +811,7 @@ void CoreBoard::save(const std::string &suffix) { brd->rules = rules; brd->gerber_output_settings = gerber_output_settings; + brd->odb_output_settings = odb_output_settings; brd->pdf_export_settings = pdf_export_settings; brd->step_export_settings = step_export_settings; brd->pnp_export_settings = pnp_export_settings; diff --git a/src/core/core_board.hpp b/src/core/core_board.hpp index 3ea13c4e1..d4f39a998 100644 --- a/src/core/core_board.hpp +++ b/src/core/core_board.hpp @@ -33,6 +33,10 @@ class CoreBoard : public Core, public DocumentBoard { { return gerber_output_settings; } + ODBOutputSettings &get_odb_output_settings() override + { + return odb_output_settings; + } PDFExportSettings &get_pdf_export_settings() override { return pdf_export_settings; @@ -80,6 +84,7 @@ class CoreBoard : public Core, public DocumentBoard { BoardRules rules; GerberOutputSettings gerber_output_settings; + ODBOutputSettings odb_output_settings; PDFExportSettings pdf_export_settings; STEPExportSettings step_export_settings; PnPExportSettings pnp_export_settings; diff --git a/src/document/idocument_board.hpp b/src/document/idocument_board.hpp index c03577e7c..5e5981e73 100644 --- a/src/document/idocument_board.hpp +++ b/src/document/idocument_board.hpp @@ -6,6 +6,7 @@ class IDocumentBoard : public virtual IDocument { public: virtual class Board *get_board() = 0; virtual class GerberOutputSettings &get_gerber_output_settings() = 0; + virtual class ODBOutputSettings &get_odb_output_settings() = 0; virtual class PDFExportSettings &get_pdf_export_settings() = 0; virtual class STEPExportSettings &get_step_export_settings() = 0; virtual class PnPExportSettings &get_pnp_export_settings() = 0; diff --git a/src/export_odb/attribute_util.cpp b/src/export_odb/attribute_util.cpp new file mode 100644 index 000000000..debc8fef9 --- /dev/null +++ b/src/export_odb/attribute_util.cpp @@ -0,0 +1,82 @@ +#include "attribute_util.hpp" +#include +#include +#include "util/once.hpp" +#include "odb_util.hpp" + +namespace horizon::ODB { + +namespace attribute::detail { +std::string make_legal_string_attribute(const std::string &n) +{ + std::string out; + out.reserve(n.size()); + for (auto c : utf8_to_ascii(n)) { + if (isgraph(c) || c == ' ') + ; + else if (isspace(c)) + c = ' '; + else + c = '_'; + out.append(1, c); + } + + return out; +} +} // namespace attribute::detail + +std::string AttributeProvider::double_to_string(double v, unsigned int n) +{ + std::ostringstream oss; + oss << std::fixed << std::setprecision(n) << v; + return oss.str(); +} + + +static unsigned int get_or_create_text(std::map &m, const std::string &t) +{ + if (m.count(t)) { + return m.at(t); + } + else { + auto n = m.size(); + m.emplace(t, n); + return n; + } +} + +unsigned int AttributeProvider::get_or_create_attribute_name(const std::string &name) +{ + return get_or_create_text(attribute_names, name); +} + +unsigned int AttributeProvider::get_or_create_attribute_text(const std::string &name) +{ + return get_or_create_text(attribute_texts, name); +} + +void RecordWithAttributes::write_attributes(std::ostream &ost) const +{ + Once once; + for (const auto &attr : attributes) { + if (once()) + ost << " ;"; + else + ost << ","; + ost << attr.first; + if (attr.second.size()) + ost << "=" << attr.second; + } +} + +void AttributeProvider::write_attributes(std::ostream &ost, const std::string &prefix) const +{ + for (const auto &[name, n] : attribute_names) { + ost << prefix << "@" << n << " " << name << endl; + } + for (const auto &[name, n] : attribute_texts) { + ost << prefix << "&" << n << " " << name << endl; + } +} + +} // namespace horizon::ODB diff --git a/src/export_odb/attribute_util.hpp b/src/export_odb/attribute_util.hpp new file mode 100644 index 000000000..21549f834 --- /dev/null +++ b/src/export_odb/attribute_util.hpp @@ -0,0 +1,64 @@ +#pragma once +#include "attributes.hpp" +#include +#include +#include +#include + +namespace horizon::ODB { + +class AttributeProvider { + +public: + template void add_attribute(Tr &r, Ta v) + { + using Tc = typename Tr::template check_type; + static_assert(Tc::value); + + const auto id = get_or_create_attribute_name(attribute::attribute_name::name); + if constexpr (std::is_enum_v) + r.attributes.emplace_back(id, std::to_string(static_cast(v))); + else + r.attributes.emplace_back(id, attr_to_string(v)); + } + +protected: + unsigned int get_or_create_attribute_name(const std::string &name); + + void write_attributes(std::ostream &ost, const std::string &prefix = "") const; + + +private: + unsigned int get_or_create_attribute_text(const std::string &name); + + static std::string double_to_string(double v, unsigned int n); + + template std::string attr_to_string(attribute::float_attribute a) + { + return double_to_string(a.value, a.ndigits); + } + + template std::string attr_to_string(attribute::boolean_attribute a) + { + return ""; + } + + template std::string attr_to_string(attribute::text_attribute a) + { + return std::to_string(get_or_create_attribute_text(a.value)); + } + + + std::map attribute_names; + std::map attribute_texts; +}; + +class RecordWithAttributes { + +protected: + void write_attributes(std::ostream &ost) const; + +public: + std::vector> attributes; +}; +} // namespace horizon::ODB diff --git a/src/export_odb/attributes.hpp b/src/export_odb/attributes.hpp new file mode 100644 index 000000000..423f7e498 --- /dev/null +++ b/src/export_odb/attributes.hpp @@ -0,0 +1,103 @@ +#pragma once +#include + +namespace horizon::ODB::attribute { + +namespace detail { +std::string make_legal_string_attribute(const std::string &n); +} + +template struct attribute_name { +}; + +enum class Type { FLOAT, BOOLEAN, TEXT }; + +template struct float_attribute { + double value; + static constexpr unsigned int ndigits = n; + static constexpr Type type = Type::FLOAT; +}; + +template struct boolean_attribute { + bool value = true; + static constexpr Type type = Type::BOOLEAN; +}; + +template struct text_attribute { + text_attribute(const std::string &t) : value(detail::make_legal_string_attribute(t)) + { + } + std::string value; + static constexpr Type type = Type::TEXT; +}; + +#define ATTR_NAME(n) \ + template <> struct attribute_name { \ + static constexpr const char *name = "." #n; \ + }; + +#define MAKE_FLOAT_ATTR(n, nd) \ + using n = float_attribute; \ + ATTR_NAME(n) + +#define MAKE_TEXT_ATTR(n) \ + using n = text_attribute; \ + ATTR_NAME(n) + +#define MAKE_BOOLEAN_ATTR(n) \ + using n = boolean_attribute; \ + ATTR_NAME(n) + +template struct is_feature : std::false_type { +}; +template struct is_net : std::false_type { +}; +template struct is_pkg : std::false_type { +}; + +template inline constexpr bool is_feature_v = is_feature::value; +template inline constexpr bool is_net_v = is_net::value; +template inline constexpr bool is_pkg_v = is_pkg::value; + +#define ATTR_IS_FEATURE(a) \ + template <> struct is_feature : std::true_type { \ + }; + +#define ATTR_IS_NET(a) \ + template <> struct is_net : std::true_type { \ + }; + +#define ATTR_IS_PKG(a) \ + template <> struct is_pkg : std::true_type { \ + }; + +enum class drill { PLATED, NON_PLATED, VIA }; +ATTR_NAME(drill) +ATTR_IS_FEATURE(drill) + +enum class primary_side { TOP, BOTTOM }; +ATTR_NAME(primary_side) + +enum class pad_usage { TOEPRINT, VIA, G_FIDUCIAL, L_FIDUCIAL, TOOLING_HOLE }; +ATTR_NAME(pad_usage) +ATTR_IS_FEATURE(pad_usage) + +MAKE_FLOAT_ATTR(drc_max_height, 3) +ATTR_IS_FEATURE(drc_max_height) + +MAKE_BOOLEAN_ATTR(smd) +ATTR_IS_FEATURE(smd) + +MAKE_TEXT_ATTR(electrical_class) +ATTR_IS_NET(electrical_class) + +MAKE_TEXT_ATTR(net_type) +ATTR_IS_NET(net_type) + +MAKE_TEXT_ATTR(diff_pair) +ATTR_IS_NET(diff_pair) + +MAKE_TEXT_ATTR(string) +ATTR_IS_FEATURE(string) + +} // namespace horizon::ODB::attribute diff --git a/src/export_odb/canvas_odb.cpp b/src/export_odb/canvas_odb.cpp new file mode 100644 index 000000000..7b3908d06 --- /dev/null +++ b/src/export_odb/canvas_odb.cpp @@ -0,0 +1,249 @@ +#include "canvas_odb.hpp" +#include "odb_export.hpp" +#include "common/keepout.hpp" +#include "board/plane.hpp" +#include "board/board_layers.hpp" +#include "util/clipper_util.hpp" +#include "util/geom_util.hpp" +#include "pool/padstack.hpp" +#include "db.hpp" +#include "util/once.hpp" +#include "common/arc.hpp" +#include "board/board.hpp" +#include "odb_util.hpp" + +namespace horizon { +CanvasODB::CanvasODB(ODB::Job &j, const Board &b) : Canvas::Canvas(), job(j), brd(b) +{ + img_mode = true; +} +void CanvasODB::request_push() +{ +} + +void CanvasODB::img_net(const Net *n) +{ +} + +void CanvasODB::img_arc(const Arc &arc) +{ + if (auto feats = get_layer_features(arc.layer)) { + feats->draw_arc(arc.from->position, arc.to->position, arc.center->position, arc.width); + } +} + +void CanvasODB::img_polygon(const Polygon &ipoly, bool tr) +{ + if (!tr) + throw std::runtime_error("can't not transform polygon"); + if (padstack_mode) + return; + + if (ipoly.layer == BoardLayers::TOP_ASSEMBLY || ipoly.layer == BoardLayers::BOTTOM_ASSEMBLY) { + if (auto feats = get_layer_features(ipoly.layer)) // on assembly layer, draw polygons as lines + feats->draw_polygon_outline(ipoly, transform); + + return; + } + + if (auto plane = dynamic_cast(ipoly.usage.ptr)) { + if (auto feats = get_layer_features(ipoly.layer)) { + ODB::EDAData::Subnet *subnet = nullptr; + if (plane->fragments.size()) { + using SP = ODB::EDAData::SubnetPlane; + subnet = &eda_data->get_net(plane->net->uuid) + .add_subnet(SP::FillType::SOLID, SP::CutoutType::CIRCLE, 0); + } + for (const auto &frag : plane->fragments) { + auto &surf = feats->add_surface(); + eda_data->add_feature_id(*subnet, ODB::EDAData::FeatureID::Type::COPPER, + ODB::get_layer_name(plane->polygon->layer), surf.index); + + Once is_outline; + for (const auto &path : frag.paths) { + // check orientations + assert(ClipperLib::Orientation(path) == is_outline()); + surf.data.lines.emplace_back(); + auto &p = surf.data.lines.back(); + p.reserve(path.size()); + assert(transform.mirror == false); + for (auto it = path.crbegin(); it != path.crend(); it++) { + p.emplace_back(transform.transform(Coordi(it->X, it->Y))); + } + } + } + } + } + else if (dynamic_cast(ipoly.usage.ptr)) { + // nop + } + else { + if (auto feats = get_layer_features(ipoly.layer)) { + auto poly = ipoly; + if (poly.is_ccw() != transform.mirror) + poly.reverse(); + + auto &surf = feats->add_surface(); + surf.data.append_polygon(poly, transform); + } + } +} + +void CanvasODB::img_line(const Coordi &p0, const Coordi &p1, const uint64_t width, int layer, bool tr) +{ + if (auto feats = get_layer_features(layer)) { + ODB::Features::Line *feat = nullptr; + if (tr) + feat = &feats->draw_line(transform.transform(p0), transform.transform(p1), width); + else + feat = &feats->draw_line(p0, p1, width); + + if (object_refs_current.size()) { + const auto &ref = object_refs_current.back(); + if (ref.type == ObjectType::TRACK) { + if (track_subnets.count(ref.uuid)) + eda_data->add_feature_id(*track_subnets.at(ref.uuid), ODB::EDAData::FeatureID::Type::COPPER, + ODB::get_layer_name(layer), feat->index); + } + } + if (text_current) { + auto &text = *text_current; + feats->add_attribute(*feat, ODB::attribute::string{text.overridden ? text.text_override : text.text}); + } + } +} + +ODB::EDAData::SubnetToeprint *CanvasODB::get_subnet_toeprint() +{ + if (object_refs_current.size()) { + const auto &ref = object_refs_current.back(); + if (ref.type == ObjectType::PAD) { + const auto &pkg_uuid = ref.uuid2; + const auto &pad_uuid = ref.uuid; + const auto key = std::make_pair(pkg_uuid, pad_uuid); + if (pad_subnets.count(key)) + return pad_subnets.at(key); + } + } + return nullptr; +} + +void CanvasODB::img_padstack(const Padstack &padstack) +{ + std::set layers; + for (const auto &it : padstack.polygons) { + layers.insert(it.second.layer); + } + for (const auto &it : padstack.shapes) { + layers.insert(it.second.layer); + } + ODB::EDAData::SubnetVia *subnet_via = nullptr; + ODB::EDAData::SubnetToeprint *subnet_toep = get_subnet_toeprint(); + if (object_refs_current.size()) { + const auto &ref = object_refs_current.back(); + if (ref.type == ObjectType::VIA) { + auto &via = brd.vias.at(ref.uuid); + if (via.junction->net) { + auto &n = eda_data->get_net(via.junction->net->uuid); + auto &subnet = n.add_subnet(); + via_subnets.emplace(via.uuid, &subnet); + subnet_via = &subnet; + } + } + } + for (const auto layer : layers) { + if (auto feats = get_layer_features(layer)) { + auto sym = job.get_or_create_symbol(padstack, layer); + auto &pad = feats->draw_pad(sym, transform); + switch (patch_type) { + case PatchType::VIA: { + feats->add_attribute(pad, ODB::attribute::pad_usage::VIA); + if (subnet_via) { + eda_data->add_feature_id(*subnet_via, ODB::EDAData::FeatureID::Type::COPPER, + ODB::get_layer_name(layer), pad.index); + } + } break; + + case PatchType::PAD: + feats->add_attribute(pad, ODB::attribute::pad_usage::TOEPRINT); + if (layer == BoardLayers::TOP_COPPER || layer == BoardLayers::BOTTOM_COPPER) + feats->add_attribute(pad, ODB::attribute::smd{}); + if (subnet_toep) + eda_data->add_feature_id(*subnet_toep, ODB::EDAData::FeatureID::Type::COPPER, + ODB::get_layer_name(layer), pad.index); + break; + + case PatchType::PAD_TH: + feats->add_attribute(pad, ODB::attribute::pad_usage::TOEPRINT); + if (subnet_toep) + eda_data->add_feature_id(*subnet_toep, ODB::EDAData::FeatureID::Type::COPPER, + ODB::get_layer_name(layer), pad.index); + break; + + default:; + } + } + } +} + +void CanvasODB::img_set_padstack(bool v) +{ + padstack_mode = v; +} + +void CanvasODB::img_patch_type(PatchType pt) +{ + patch_type = pt; +} + +void CanvasODB::img_text(const Text *text) +{ + text_current = text; +} + +void CanvasODB::img_hole(const Hole &hole) +{ + if (hole.shape == Hole::Shape::ROUND) { + auto &pad = drill_features->draw_circle(transform.transform(hole.placement.shift), hole.diameter); + if (patch_type == PatchType::VIA) { + if (object_refs_current.size()) { + const auto &ref = object_refs_current.back(); + if (ref.type == ObjectType::VIA) { + auto &subnet = *via_subnets.at(ref.uuid); + eda_data->add_feature_id(subnet, ODB::EDAData::FeatureID::Type::HOLE, ODB::drills_layer, pad.index); + } + } + drill_features->add_attribute(pad, ODB::attribute::drill::VIA); + } + else if (hole.plated) { + drill_features->add_attribute(pad, ODB::attribute::drill::PLATED); + if (auto subnet_toep = get_subnet_toeprint()) + eda_data->add_feature_id(*subnet_toep, ODB::EDAData::FeatureID::Type::COPPER, ODB::drills_layer, + pad.index); + } + else { + drill_features->add_attribute(pad, ODB::attribute::drill::NON_PLATED); + } + } + else if (hole.shape == Hole::Shape::SLOT) { + auto tr = transform; + tr.accumulate(hole.placement); + if (tr.mirror) + tr.invert_angle(); + + double d = std::max(((int64_t)hole.length - (int64_t)hole.diameter) / 2, (int64_t)0); + const auto dv = Coordd::euler(d, angle_to_rad(tr.get_angle())).to_coordi(); + + auto &line = drill_features->draw_line(tr.shift - dv, tr.shift + dv, hole.diameter); + if (hole.plated) { + drill_features->add_attribute(line, ODB::attribute::drill::PLATED); + if (auto subnet_toep = get_subnet_toeprint()) + eda_data->add_feature_id(*subnet_toep, ODB::EDAData::FeatureID::Type::HOLE, ODB::drills_layer, + line.index); + } + else { + drill_features->add_attribute(line, ODB::attribute::drill::NON_PLATED); + } + } +} +} // namespace horizon diff --git a/src/export_odb/canvas_odb.hpp b/src/export_odb/canvas_odb.hpp new file mode 100644 index 000000000..2e4abcd7f --- /dev/null +++ b/src/export_odb/canvas_odb.hpp @@ -0,0 +1,58 @@ +#pragma once +#include "canvas/canvas.hpp" +#include "db.hpp" + +namespace horizon { +class CanvasODB : public Canvas { +public: + CanvasODB(ODB::Job &job, const class Board &brd); + void push() override + { + } + void request_push() override; + uint64_t outline_width = 0; + + std::map layer_features; + ODB::Features *drill_features = nullptr; + ODB::EDAData *eda_data = nullptr; + + std::map, ODB::EDAData::SubnetToeprint *> pad_subnets; + std::map track_subnets; + +private: + void img_net(const Net *net) override; + void img_polygon(const Polygon &poly, bool tr) override; + void img_arc(const Arc &arc) override; + bool img_supports_arc() const override + { + return true; + } + void img_line(const Coordi &p0, const Coordi &p1, const uint64_t width, int layer, bool tr = true) override; + void img_padstack(const Padstack &ps) override; + void img_hole(const Hole &hole) override; + void img_set_padstack(bool v) override; + void img_patch_type(PatchType pt) override; + void img_text(const Text *text) override; + + PatchType patch_type = PatchType::OTHER; + const Text *text_current = nullptr; + + bool padstack_mode = false; + + ODB::Features *get_layer_features(int layer) + { + auto x = layer_features.find(layer); + if (x == layer_features.end()) + return nullptr; + else + return x->second; + } + + ODB::Job &job; + const Board &brd; + + std::map via_subnets; + + ODB::EDAData::SubnetToeprint *get_subnet_toeprint(); +}; +} // namespace horizon diff --git a/src/export_odb/components.cpp b/src/export_odb/components.cpp new file mode 100644 index 000000000..7b4c66010 --- /dev/null +++ b/src/export_odb/components.cpp @@ -0,0 +1,35 @@ +#include "components.hpp" +#include "odb_util.hpp" +#include "board/board_package.hpp" + +namespace horizon::ODB { + +void Components::write(std::ostream &ost) const +{ + ost << "UNITS=MM" << endl; + write_attributes(ost); + for (const auto &comp : components) { + comp.write(ost); + } +} + +void Components::Component::write(std::ostream &ost) const +{ + ost << "CMP " << pkg_ref << " " << placement.shift << " " << Angle{placement} << " " + << "N" + << " " << comp_name << " " << part_name; + write_attributes(ost); + ost << endl; + for (const auto &toep : toeprints) { + toep.write(ost); + } +} + +void Components::Toeprint::write(std::ostream &ost) const +{ + ost << "TOP " << pin_num << " " << placement.shift << " " << Angle{placement} << " " + << "N" + << " " << net_num << " " << subnet_num << " " << toeprint_name << endl; +} + +} // namespace horizon::ODB diff --git a/src/export_odb/components.hpp b/src/export_odb/components.hpp new file mode 100644 index 000000000..1c3f63e83 --- /dev/null +++ b/src/export_odb/components.hpp @@ -0,0 +1,47 @@ +#pragma once +#include "attribute_util.hpp" +#include "eda_data.hpp" + +namespace horizon::ODB { + +class Components : public AttributeProvider { +public: + class Toeprint { + public: + Toeprint(const EDAData::Pin &pin) : pin_num(pin.index), toeprint_name(pin.name) + { + } + + unsigned int pin_num; + + Placement placement; + unsigned int net_num = 0; + unsigned int subnet_num = 0; + std::string toeprint_name = 0; + + void write(std::ostream &ost) const; + }; + + class Component : public RecordWithAttributes { + public: + Component(unsigned int i, unsigned int r) : index(i), pkg_ref(r) + { + } + const unsigned int index; + unsigned int pkg_ref; + Placement placement; + + std::string comp_name; + std::string part_name; + + std::list toeprints; + + void write(std::ostream &ost) const; + }; + + std::list components; + + void write(std::ostream &ost) const; +}; + +} // namespace horizon::ODB diff --git a/src/export_odb/db.cpp b/src/export_odb/db.cpp new file mode 100644 index 000000000..5195f1992 --- /dev/null +++ b/src/export_odb/db.cpp @@ -0,0 +1,222 @@ +#include "db.hpp" +#include "board/board_layers.hpp" +#include "export_util/tree_writer.hpp" +#include "structured_text_writer.hpp" +#include "pool/padstack.hpp" +#include "export_util/padstack_hash.hpp" +#include "board/plane.hpp" +#include "util/once.hpp" +#include "util/geom_util.hpp" +#include "util/version.hpp" +#include "block/net_class.hpp" +#include "pool/package.hpp" +#include "board/board_package.hpp" +#include "pool/part.hpp" +#include "util/util.hpp" +#include "odb_util.hpp" + +namespace horizon::ODB { + +#define MAKE_ENUM_TO_STRING(n) \ + std::string enum_to_string(n value) \ + { \ + using N = n; \ + const std::map items = {ITEMS}; \ + return items.at(value); \ + } + +#define X(a) \ + { \ + N::a, #a \ + } + +#define ITEMS X(POSITIVE), X(NEGATIVE) +MAKE_ENUM_TO_STRING(Polarity) +#undef ITEMS + +#define ITEMS X(SIGNAL), X(SOLDER_MASK), X(SILK_SCREEN), X(SOLDER_PASTE), X(DRILL), X(DOCUMENT), X(ROUT), X(COMPONENT) +MAKE_ENUM_TO_STRING(Matrix::Layer::Type) +#undef ITEMS + + +#define ITEMS X(MISC), X(BOARD) +MAKE_ENUM_TO_STRING(Matrix::Layer::Context) +#undef ITEMS + + +Components::Component &Step::add_component(const BoardPackage &bpkg) +{ + auto &comps = bpkg.flip ? comp_bot.value() : comp_top.value(); + auto &pkg = eda_data.get_package(bpkg.package.uuid); + + auto &comp = comps.components.emplace_back(comps.components.size(), pkg.index); + comp.placement = bpkg.placement; + if (bpkg.flip) + comp.placement.invert_angle(); + comp.comp_name = make_legal_name(bpkg.component->refdes); + comp.part_name = + make_legal_name(bpkg.component->part ? bpkg.component->part->get_MPN() : bpkg.component->entity->name); + + return comp; +} + +Matrix::Layer &Matrix::add_layer(const std::string &name) +{ + return layers.emplace_back(row++, name); +} + +void Matrix::add_step(const std::string &name) +{ + steps.emplace(name, col++); +} + + +Matrix::Layer &Job::add_matrix_layer(const std::string &name) +{ + for (auto &[step_name, col] : matrix.steps) { + steps.at(step_name).layer_features[name]; + } + + return matrix.add_layer(name); +} + +Step &Job::add_step(const std::string &name) +{ + matrix.add_step(name); + return steps[name]; +} + +void Step::write(TreeWriter &writer) const +{ + { + auto file = writer.create_file("stephdr"); + StructuredTextWriter txt_writer(file.stream); + txt_writer.write_line("X_DATUM", 0); + txt_writer.write_line("Y_DATUM", 0); + txt_writer.write_line("X_ORIGIN", 0); + txt_writer.write_line("Y_ORIGIN", 0); + } + for (const auto &[layer_name, feats] : layer_features) { + auto file = writer.create_file(fs::path("layers") / layer_name / "features"); + feats.write(file.stream); + } + if (comp_top) { + auto file = writer.create_file("layers/comp_+_top/components"); + comp_top->write(file.stream); + } + if (comp_bot) { + auto file = writer.create_file("layers/comp_+_bot/components"); + comp_bot->write(file.stream); + } + if (profile) { + auto file = writer.create_file("profile"); + profile->write(file.stream); + } + { + auto file = writer.create_file("eda/data"); + eda_data.write(file.stream); + } +} + +std::string Job::get_or_create_symbol(const Padstack &ps, int layer) +{ + // try to use built-in symbol first + { + std::vector layer_polys; + std::vector layer_shapes; + for (const auto &[uu, it] : ps.polygons) { + if (it.layer == layer) + layer_polys.push_back(&it); + } + for (const auto &[uu, it] : ps.shapes) { + if (it.layer == layer) + layer_shapes.push_back(&it); + } + if (layer_shapes.size() == 1 && layer_polys.size() == 0) { + auto &sh = *layer_shapes.front(); + if (sh.form == Shape::Form::CIRCLE && sh.placement.shift == Coordi()) + return make_symbol_circle(sh.params.at(0)); + else if (sh.form == Shape::Form::RECTANGLE && sh.placement.shift == Coordi() + && sh.placement.get_angle() == 0) + return make_symbol_rect(sh.params.at(0), sh.params.at(1)); + else if (sh.form == Shape::Form::OBROUND && sh.placement.shift == Coordi() && sh.placement.get_angle() == 0) + return make_symbol_oval(sh.params.at(0), sh.params.at(1)); + } + } + + const auto hash = PadstackHash::hash(ps); + const auto key = std::make_tuple(ps.uuid, layer, hash); + if (symbols.count(key)) { + return symbols.at(key).name; + } + else { + auto &sym = + symbols.emplace(std::piecewise_construct, std::forward_as_tuple(key), std::forward_as_tuple(ps, layer)) + .first->second; + if (symbol_names.count(sym.name)) { + const std::string name_orig = sym.name; + unsigned int i = 1; + do { + sym.name = name_orig + "_" + std::to_string(i++); + } while (symbol_names.count(sym.name)); + symbol_names.insert(sym.name); + } + else { + symbol_names.insert(sym.name); + } + return sym.name; + } +} + +void Job::write(TreeWriter &top_writer) const +{ + TreeWriterPrefixed writer{top_writer, job_name}; + { + auto file = writer.create_file("matrix/matrix"); + matrix.write(file.stream); + } + for (const auto &[key, sym] : symbols) { + TreeWriterPrefixed wr_prefixed(writer, fs::path("symbols") / sym.name); + sym.write(wr_prefixed); + } + for (const auto &[step_name, step] : steps) { + TreeWriterPrefixed wr_prefixed(writer, fs::path("steps") / step_name); + step.write(wr_prefixed); + } + { + auto file = writer.create_file("misc/info"); + StructuredTextWriter twriter(file.stream); + twriter.write_line("UNITS", "MM"); + twriter.write_line("ODB_VERSION_MAJOR", 8); + twriter.write_line("ODB_VERSION_MINOR", 1); + twriter.write_line("CREATION_DATE", "20220309.133742"); + twriter.write_line("SAVE_DATE", "20220309.133742"); + twriter.write_line("ODB_SOURCE", "Horizon EDA"); + twriter.write_line("JOB_NAME", job_name); + twriter.write_line("SAVE_APP", "Horizon EDA Version " + Version::get_string()); + } +} + +void Matrix::write(std::ostream &ost) const +{ + StructuredTextWriter writer(ost); + for (const auto &[step_name, column] : steps) { + const auto a = writer.make_array_proxy("STEP"); + writer.write_line("COL", column); + writer.write_line("NAME", step_name); + } + for (const auto &layer : layers) { + const auto a = writer.make_array_proxy("LAYER"); + writer.write_line("ROW", layer.row); + writer.write_line_enum("CONTEXT", layer.context); + writer.write_line_enum("TYPE", layer.type); + writer.write_line("NAME", layer.name); + writer.write_line_enum("POLARITY", layer.polarity); + if (layer.span) { + writer.write_line("START_NAME", layer.span->start); + writer.write_line("END_NAME", layer.span->end); + } + } +} + +} // namespace horizon::ODB diff --git a/src/export_odb/db.hpp b/src/export_odb/db.hpp new file mode 100644 index 000000000..af3bd6fae --- /dev/null +++ b/src/export_odb/db.hpp @@ -0,0 +1,118 @@ +#pragma once +#include "common/common.hpp" +#include "util/placement.hpp" +#include "util/uuid.hpp" +#include +#include +#include +#include "attributes.hpp" +#include "clipper/clipper.hpp" +#include "board/plane.hpp" +#include "eda_data.hpp" +#include "attribute_util.hpp" +#include "features.hpp" +#include "components.hpp" +#include "symbol.hpp" + +namespace horizon { +class TreeWriter; +class Padstack; +class Shape; +class Polygon; +class Package; +class Pad; +class BoardPackage; +} // namespace horizon + +namespace horizon::ODB { + +class Symbol; + +enum class Polarity { POSITIVE, NEGATIVE }; + +class Step { +public: + std::map layer_features; + std::optional profile; + + std::optional comp_top; + std::optional comp_bot; + + EDAData eda_data; + + Components::Component &add_component(const BoardPackage &pkg); + + void write(TreeWriter &writer) const; +}; + +class Matrix { +public: + std::map steps; // step name -> column + + class Layer { + public: + Layer(unsigned int r, const std::string &n) : row(r), name(n) + { + } + + const unsigned int row; + const std::string name; + + enum class Context { BOARD, MISC }; + Context context; + + enum class Type { + SIGNAL, + SOLDER_MASK, + SILK_SCREEN, + SOLDER_PASTE, + DRILL, + ROUT, + DOCUMENT, + COMPONENT, + }; + Type type; + struct Span { + std::string start; + std::string end; + }; + std::optional span; + + Polarity polarity = Polarity::POSITIVE; + }; + std::vector layers; + + Layer &add_layer(const std::string &name); + void add_step(const std::string &name); + + void write(std::ostream &ost) const; + +private: + unsigned int row = 1; + unsigned int col = 1; +}; + +class Job { +public: + Matrix matrix; + Matrix::Layer &add_matrix_layer(const std::string &name); + Step &add_step(const std::string &name); + + std::string job_name = "board"; + std::map steps; + + using SymbolKey = std::tuple; // padstack UUID, layer, content hash + std::map symbols; + std::set symbol_names; + + std::string get_or_create_symbol(const Padstack &ps, int layer); + + void write(TreeWriter &writer) const; +}; + +std::string enum_to_string(Polarity); +std::string enum_to_string(Matrix::Layer::Type); +std::string enum_to_string(Matrix::Layer::Context); + + +} // namespace horizon::ODB diff --git a/src/export_odb/eda_data.cpp b/src/export_odb/eda_data.cpp new file mode 100644 index 000000000..1a5fb5515 --- /dev/null +++ b/src/export_odb/eda_data.cpp @@ -0,0 +1,374 @@ +#include "eda_data.hpp" +#include "block/net.hpp" +#include "block/net_class.hpp" +#include "common/polygon.hpp" +#include "pool/package.hpp" +#include "board/board_layers.hpp" +#include "util/util.hpp" +#include "odb_util.hpp" + +namespace horizon::ODB { + +EDAData::EDAData() +{ + auto &x = nets_map.emplace(std::piecewise_construct, std::forward_as_tuple(UUID{}), + std::forward_as_tuple(nets.size(), "$NONE$")) + .first->second; + nets.push_back(&x); +} + +EDAData::Net::Net(unsigned int i, const std::string &n) : index(i), name(make_legal_name(n)) +{ +} + +void EDAData::Net::write(std::ostream &ost) const +{ + ost << "NET " << name; + write_attributes(ost); + ost << endl; + + for (const auto &subnet : subnets) { + subnet->write(ost); + } +} + +static std::string get_net_name(const horizon::Net &net) +{ + std::string net_name; + if (net.is_named()) + return net.name; + else + return "$" + static_cast(net.uuid); +} + +EDAData::Net &EDAData::add_net(const horizon::Net &net) +{ + const auto net_name = get_net_name(net); + auto &x = nets_map.emplace(std::piecewise_construct, std::forward_as_tuple(net.uuid), + std::forward_as_tuple(nets.size(), net_name)) + .first->second; + nets.push_back(&x); + add_attribute(x, attribute::net_type{net.net_class->name}); + if (net.diffpair) { + // ensure attribute value is shorter than 64 chars + auto n1 = make_legal_name(net_name).substr(0, 31); + auto n2 = make_legal_name(get_net_name(*net.diffpair)).substr(0, 31); + if (n1 > n2) + std::swap(n1, n2); + add_attribute(x, attribute::diff_pair{n1 + "," + n2}); + } + + return x; +} + +unsigned int EDAData::get_or_create_layer(const std::string &l) +{ + if (layers_map.count(l)) { + return layers_map.at(l); + } + else { + auto n = layers_map.size(); + layers_map.emplace(l, n); + layers.push_back(l); + assert(layers.size() == layers_map.size()); + return n; + } +} + +void EDAData::Subnet::write(std::ostream &ost) const +{ + ost << "SNT "; + write_subnet(ost); + ost << endl; + for (const auto &fid : feature_ids) { + fid.write(ost); + } +} + +void EDAData::FeatureID::write(std::ostream &ost) const +{ + static const std::map type_map = { + {Type::COPPER, "C"}, + {Type::HOLE, "H"}, + }; + ost << "FID " << type_map.at(type) << " " << layer << " " << feature_id << endl; +} + +void EDAData::SubnetVia::write_subnet(std::ostream &ost) const +{ + ost << "VIA"; +} + +void EDAData::SubnetTrace::write_subnet(std::ostream &ost) const +{ + ost << "TRC"; +} + +void EDAData::SubnetPlane::write_subnet(std::ostream &ost) const +{ + static const std::map fill_type_map = { + {FillType::SOLID, "S"}, + }; + static const std::map cutout_type_map = { + {CutoutType::CIRCLE, "C"}, + }; + ost << "PLN " << fill_type_map.at(fill_type) << " " << cutout_type_map.at(cutout_type) << " " << Dim{fill_size}; +} + +void EDAData::SubnetToeprint::write_subnet(std::ostream &ost) const +{ + static const std::map side_map = { + {Side::BOTTOM, "B"}, + {Side::TOP, "T"}, + }; + ost << "TOP " << side_map.at(side) << " " << comp_num << " " << toep_num; +} + +void EDAData::add_feature_id(Subnet &subnet, FeatureID::Type type, const std::string &layer, unsigned int feature_id) +{ + subnet.feature_ids.emplace_back(type, get_or_create_layer(layer), feature_id); +} + + +static std::unique_ptr poly_as_rectangle_or_square(const Polygon &poly) +{ + if (!poly.is_rect()) + return nullptr; + const auto &p0 = poly.vertices.at(0).position; + const auto &p1 = poly.vertices.at(1).position; + const auto &p2 = poly.vertices.at(2).position; + const auto &p3 = poly.vertices.at(3).position; + const auto v0 = p1 - p0; + const auto v1 = p2 - p1; + const Coordi bottom_left = Coordi::min(p0, Coordi::min(p1, Coordi::min(p2, p3))); + uint64_t w; + uint64_t h; + if (v0.y == 0) { + assert(v1.x == 0); + w = std::abs(v0.x); + h = std::abs(v1.y); + } + else if (v0.x == 0) { + assert(v1.y == 0); + w = std::abs(v1.x); + h = std::abs(v0.y); + } + else { + assert(false); + } + if (w == h) { + const auto hs = w / 2; + return std::make_unique(bottom_left + Coordi(hs, hs), hs); + } + else { + return std::make_unique(bottom_left, w, h); + } +} + +static std::unique_ptr outline_contour_from_polygon(const Polygon &poly) +{ + auto r = std::make_unique(); + r->data.append_polygon_auto_orientation(poly); + return r; +} + +static std::unique_ptr outline_from_polygon(const Polygon &poly) +{ + if (auto x = poly_as_rectangle_or_square(poly)) + return x; + return outline_contour_from_polygon(poly); +} + +void EDAData::OutlineSquare::write(std::ostream &ost) const +{ + ost << "SQ " << center << " " << Dim{half_side} << endl; +} + +void EDAData::OutlineCircle::write(std::ostream &ost) const +{ + ost << "CR " << center << " " << Dim{radius} << endl; +} + +void EDAData::OutlineRectangle::write(std::ostream &ost) const +{ + ost << "RC " << lower << " " << Dim{width} << " " << Dim{height} << endl; +} + +void EDAData::OutlineContour::write(std::ostream &ost) const +{ + ost << "CT" << endl; + data.write(ost); + ost << "CE" << endl; +} + +EDAData::Package::Package(const unsigned int i, const std::string &n) : index(i), name(make_legal_name(n)) +{ +} + +EDAData::Package &EDAData::add_package(const horizon::Package &pkg) +{ + auto &x = packages_map + .emplace(std::piecewise_construct, std::forward_as_tuple(pkg.uuid), + std::forward_as_tuple(packages.size(), pkg.name)) + .first->second; + packages.push_back(&x); + const auto bb = pkg.get_bbox(); + x.xmin = bb.first.x; + x.ymin = bb.first.y; + x.xmax = bb.second.x; + x.ymax = bb.second.y; + x.pitch = UINT64_MAX; + if (pkg.pads.size() < 2) + x.pitch = 1_mm; // placeholder value to not break anything + + for (auto it = pkg.pads.begin(); it != pkg.pads.end(); it++) { + auto it2 = it; + it2++; + for (; it2 != pkg.pads.end(); it2++) { + const uint64_t pin_dist = Coordd(it->second.placement.shift - it2->second.placement.shift).mag(); + x.pitch = std::min(x.pitch, pin_dist); + } + } + + std::set outline_polys; + for (const auto &[uu, poly] : pkg.polygons) { + if (poly.layer == BoardLayers::TOP_PACKAGE) { + outline_polys.insert(&poly); + } + } + + if (outline_polys.size() == 1) { + x.outline.push_back(outline_from_polygon(**outline_polys.begin())); + } + else if (outline_polys.size() > 1) { + for (auto poly : outline_polys) + x.outline.push_back(outline_contour_from_polygon(*poly)); + } + else { + x.outline.push_back(std::make_unique(bb)); + } + + { + auto pads_sorted = pkg.get_pads_sorted(); + for (const auto pad : pads_sorted) { + x.add_pin(*pad); + } + } + + return x; +} + +EDAData::Pin::Pin(unsigned int i, const std::string &n) : name(make_legal_name(n)), index(i) +{ +} + +EDAData::Pin &EDAData::Package::add_pin(const horizon::Pad &pad) +{ + auto &pin = pins_map.emplace(std::piecewise_construct, std::forward_as_tuple(pad.uuid), + std::forward_as_tuple(pins.size(), pad.name)) + .first->second; + pins.push_back(&pin); + pin.center = pad.placement.shift; + switch (pad.padstack.type) { + case Padstack::Type::THROUGH: + pin.type = Pin::Type::THROUGH_HOLE; + pin.mtype = Pin::MountType::THROUGH_HOLE; + pin.etype = Pin::ElectricalType::ELECTRICAL; + break; + + case Padstack::Type::TOP: + case Padstack::Type::BOTTOM: + pin.type = Pin::Type::SURFACE; + pin.mtype = Pin::MountType::SMT; + pin.etype = Pin::ElectricalType::ELECTRICAL; + break; + + case Padstack::Type::MECHANICAL: + pin.type = Pin::Type::THROUGH_HOLE; + pin.mtype = Pin::MountType::THROUGH_HOLE; + pin.etype = Pin::ElectricalType::MECHANICAL; + break; + + default:; + } + + const auto &ps = pad.padstack; + std::set shapes_top; + for (const auto &[uu, sh] : ps.shapes) { + if (sh.layer == BoardLayers::TOP_COPPER) + shapes_top.insert(&sh); + } + const auto n_polys_top = std::count_if(ps.polygons.begin(), ps.polygons.end(), + [](auto &it) { return it.second.layer == BoardLayers::TOP_COPPER; }); + if (shapes_top.size() == 1 && n_polys_top == 0 && (*shapes_top.begin())->form == Shape::Form::CIRCLE) { + const auto &sh = **shapes_top.begin(); + pin.outline.push_back( + std::make_unique(pad.placement.transform(sh.placement.shift), sh.params.at(0) / 2)); + } + else { + const auto bb = ps.get_bbox(true); + pin.outline.push_back(std::make_unique(pad.placement.transform_bb(bb))); + } + return pin; +} + +void EDAData::Pin::write(std::ostream &ost) const +{ + static const std::map type_map = { + {Type::SURFACE, "S"}, + {Type::THROUGH_HOLE, "T"}, + }; + static const std::map etype_map = { + {ElectricalType::ELECTRICAL, "E"}, + {ElectricalType::MECHANICAL, "M"}, + {ElectricalType::UNDEFINED, "U"}, + }; + static const std::map mtype_map = { + {MountType::THROUGH_HOLE, "T"}, + {MountType::SMT, "S"}, + {MountType::UNDEFINED, "U"}, + }; + ost << "PIN " << name << " " << type_map.at(type) << " " << center << " 0 " << etype_map.at(etype) << " " + << mtype_map.at(mtype) << endl; + for (const auto &ol : outline) { + ol->write(ost); + } +} + + +void EDAData::Package::write(std::ostream &ost) const +{ + ost << "PKG " << name << " " << Dim{pitch} << " " << Dim{xmin} << " " << Dim{ymin} << " " << Dim{xmax} << " " + << Dim{ymax} << endl; + + for (const auto &ol : outline) { + ol->write(ost); + } + for (const auto &pin : pins) { + pin->write(ost); + } +} + +void EDAData::write(std::ostream &ost) const +{ + ost << "HDR Horizon EDA" << endl; + ost << "UNITS=MM" << endl; + ost << "LYR"; + + for (const auto &layer : layers) { + + ost << " " << layer; + } + + ost << endl; + + write_attributes(ost, "#"); + + for (const auto &net : nets) { + net->write(ost); + } + for (const auto pkg : packages) { + pkg->write(ost); + } +} +} // namespace horizon::ODB diff --git a/src/export_odb/eda_data.hpp b/src/export_odb/eda_data.hpp new file mode 100644 index 000000000..073258da7 --- /dev/null +++ b/src/export_odb/eda_data.hpp @@ -0,0 +1,261 @@ +#pragma once +#include +#include +#include "common/common.hpp" +#include "util/uuid.hpp" +#include "attribute_util.hpp" +#include "surface_data.hpp" + +namespace horizon { +class Net; +class Pad; +class Package; +} // namespace horizon + +namespace horizon::ODB { + +class EDAData : public AttributeProvider { +public: + EDAData(); + + void write(std::ostream &ost) const; + + class FeatureID { + friend EDAData; + + public: + enum class Type { COPPER, LAMINATE, HOLE }; + + FeatureID(Type t, unsigned int l, unsigned int fid) : type(t), layer(l), feature_id(fid) + { + } + + Type type; + unsigned int layer; + unsigned int feature_id; + + void write(std::ostream &ost) const; + }; + + class Subnet { + public: + Subnet(unsigned int i) : index(i) + { + } + const unsigned int index; + void write(std::ostream &ost) const; + + std::list feature_ids; + + virtual ~Subnet() = default; + + protected: + virtual void write_subnet(std::ostream &ost) const = 0; + }; + + class SubnetVia : public Subnet { + public: + using Subnet::Subnet; + void write_subnet(std::ostream &ost) const override; + }; + + class SubnetTrace : public Subnet { + public: + using Subnet::Subnet; + void write_subnet(std::ostream &ost) const override; + }; + + class SubnetPlane : public Subnet { + public: + enum class FillType { SOLID, OUTLINE }; + enum class CutoutType { CIRCLE, RECT, OCTAGON, EXACT }; + + SubnetPlane(unsigned int i, FillType ft, CutoutType ct, double fs) + : Subnet(i), fill_type(ft), cutout_type(ct), fill_size(fs) + { + } + + FillType fill_type; + CutoutType cutout_type; + double fill_size; + + void write_subnet(std::ostream &ost) const override; + }; + + class SubnetToeprint : public Subnet { + public: + enum class Side { TOP, BOTTOM }; + + SubnetToeprint(unsigned int i, Side s, unsigned int c, unsigned int t) + : Subnet(i), side(s), comp_num(c), toep_num(t) + { + } + + Side side; + + unsigned int comp_num; + unsigned int toep_num; + + void write_subnet(std::ostream &ost) const override; + }; + + void add_feature_id(Subnet &subnet, FeatureID::Type type, const std::string &layer, unsigned int feature_id); + + class Net : public RecordWithAttributes { + public: + template using check_type = attribute::is_net; + + Net(unsigned int index, const std::string &name); + const unsigned int index; + + std::string name; + + std::list> subnets; + + template T &add_subnet(Args &&... args) + { + auto f = std::make_unique(subnets.size(), std::forward(args)...); + auto &r = *f; + subnets.push_back(std::move(f)); + return r; + } + + void write(std::ostream &ost) const; + }; + + Net &add_net(const horizon::Net &net); + Net &get_net(const UUID &uu) + { + return nets_map.at(uu); + } + + class Outline { + public: + virtual void write(std::ostream &ost) const = 0; + + virtual ~Outline() = default; + }; + + class OutlineRectangle : public Outline { + public: + OutlineRectangle(const Coordi &l, uint64_t w, uint64_t h) : lower(l), width(w), height(h) + { + } + OutlineRectangle(const std::pair &bb) + : OutlineRectangle(bb.first, bb.second.x - bb.first.x, bb.second.y - bb.first.y) + { + } + + Coordi lower; + uint64_t width; + uint64_t height; + + void write(std::ostream &ost) const override; + }; + + class OutlineContour : public Outline { + public: + SurfaceData data; + + void write(std::ostream &ost) const override; + }; + + class OutlineSquare : public Outline { + public: + OutlineSquare(const Coordi &c, uint64_t s) : center(c), half_side(s) + { + } + Coordi center; + uint64_t half_side; + + void write(std::ostream &ost) const override; + }; + + class OutlineCircle : public Outline { + public: + OutlineCircle(const Coordi &c, uint64_t r) : center(c), radius(r) + { + } + Coordi center; + uint64_t radius; + + void write(std::ostream &ost) const override; + }; + + class Pin { + public: + Pin(unsigned int i, const std::string &n); + std::string name; + const unsigned int index; + + Coordi center; + + enum class Type { THROUGH_HOLE, BLIND, SURFACE }; + Type type = Type::SURFACE; + + enum class ElectricalType { ELECTRICAL, MECHANICAL, UNDEFINED }; + ElectricalType etype = ElectricalType::UNDEFINED; + + enum class MountType { + SMT, + SMT_RECOMMENDED, + THROUGH_HOLE, + THROUGH_RECOMMENDED, + PRESSFIT, + NON_BOARD, + HOLE, + UNDEFINED + }; + MountType mtype = MountType::UNDEFINED; + + std::list> outline; + + void write(std::ostream &ost) const; + }; + + class Package : public RecordWithAttributes { + public: + template using check_type = attribute::is_pkg; + + Package(const unsigned int i, const std::string &n); + const unsigned int index; + std::string name; + + uint64_t pitch; + int64_t xmin, ymin, xmax, ymax; + + std::list> outline; + + Pin &add_pin(const horizon::Pad &pad); + const Pin &get_pin(const UUID &uu) const + { + return pins_map.at(uu); + } + + void write(std::ostream &ost) const; + + + private: + std::map pins_map; + std::list pins; + }; + + Package &add_package(const horizon::Package &pkg); + const Package &get_package(const UUID &uu) const + { + return packages_map.at(uu); + } + +private: + std::map nets_map; + std::list nets; + + std::map packages_map; + std::list packages; + + unsigned int get_or_create_layer(const std::string &l); + + std::map layers_map; + std::vector layers; +}; +} // namespace horizon::ODB diff --git a/src/export_odb/features.cpp b/src/export_odb/features.cpp new file mode 100644 index 000000000..af75f3195 --- /dev/null +++ b/src/export_odb/features.cpp @@ -0,0 +1,178 @@ +#include "features.hpp" +#include "odb_util.hpp" +#include "symbol.hpp" +#include "util/geom_util.hpp" +#include "common/polygon.hpp" +#include "common/shape.hpp" +#include + +namespace horizon::ODB { + +unsigned int Features::get_or_create_symbol_circle(uint64_t diameter) +{ + return get_or_create_symbol(symbols_circle, diameter); +} + +unsigned int Features::get_or_create_symbol_pad(const std::string &sym) +{ + return get_or_create_symbol(symbols_pad, sym); +} + +unsigned int Features::get_or_create_symbol_rect(uint64_t width, uint64_t height) +{ + return get_or_create_symbol(symbols_rect, std::make_pair(width, height)); +} + +unsigned int Features::get_or_create_symbol_oval(uint64_t width, uint64_t height) +{ + return get_or_create_symbol(symbols_oval, std::make_pair(width, height)); +} + +Features::Line &Features::draw_line(const Coordi &from, const Coordi &to, uint64_t width) +{ + const auto sym = get_or_create_symbol_circle(width); + return add_feature(from, to, sym); +} + +Features::Arc &Features::draw_arc(const Coordi &from, const Coordi &to, const Coordi ¢er, uint64_t width) +{ + const auto sym = get_or_create_symbol_circle(width); + const auto real_center = project_onto_perp_bisector(from, to, center).to_coordi(); + return add_feature(from, to, real_center, sym, Arc::Direction::CCW); +} + +std::vector Features::draw_polygon_outline(const Polygon &ipoly, const Placement &transform) +{ + std::vector r; + r.reserve(ipoly.vertices.size()); + for (size_t i = 0; i < ipoly.vertices.size(); i++) { + const auto &v_last = ipoly.get_vertex(i); + const auto &v = ipoly.get_vertex(i + 1); + if (v_last.type == Polygon::Vertex::Type::LINE) { + r.push_back(&draw_line(transform.transform(v_last.position), transform.transform(v.position), 0)); + } + else if (v_last.type == Polygon::Vertex::Type::ARC) { + if (!v_last.arc_reverse != transform.mirror) + r.push_back(&draw_arc(transform.transform(v_last.position), transform.transform(v.position), + transform.transform(v_last.arc_center), 0)); + else + r.push_back(&draw_arc(transform.transform(v.position), transform.transform(v_last.position), + transform.transform(v_last.arc_center), 0)); + } + } + return r; +} + +Features::Pad &Features::draw_pad(const std::string &sym, const Placement &transform) +{ + return add_feature(transform, get_or_create_symbol_pad(sym)); +} + +Features::Pad &Features::draw_circle(const Coordi &pos, uint64_t diameter) +{ + return add_feature(Placement(pos), get_or_create_symbol_circle(diameter)); +} + +Features::Pad &Features::draw_shape(const Shape &shape) +{ + switch (shape.form) { + case Shape::Form::CIRCLE: + return add_feature(shape.placement, get_or_create_symbol_circle(shape.params.at(0))); + + case Shape::Form::RECTANGLE: + return add_feature(shape.placement, get_or_create_symbol_rect(shape.params.at(0), shape.params.at(1))); + + case Shape::Form::OBROUND: + return add_feature(shape.placement, get_or_create_symbol_oval(shape.params.at(0), shape.params.at(1))); + } + throw std::runtime_error("unsupported shape form"); +} + +Features::Surface &Features::add_surface() +{ + return add_feature(); +} + +void Features::Feature::write(std::ostream &ost) const +{ + switch (get_type()) { + case Type::LINE: + ost << "L"; + break; + + case Type::ARC: + ost << "A"; + break; + + case Type::PAD: + ost << "P"; + break; + + case Type::SURFACE: + ost << "S"; + break; + } + ost << " "; + write_feature(ost); + + write_attributes(ost); + ost << endl; +} + +void Features::Line::write_feature(std::ostream &ost) const +{ + ost << from << " " << to << " " << symbol << " P 0"; +} + +void Features::Arc::write_feature(std::ostream &ost) const +{ + ost << from << " " << to << " " + << " " << center << " " << symbol << " P 0 " << (direction == Direction::CW ? "Y" : "N"); +} + +void Features::Pad::write_feature(std::ostream &ost) const +{ + ost << placement.shift << " " << symbol << " P 0 "; + if (placement.mirror) + ost << "9"; + else + ost << "8"; + ost << " " << Angle{placement}; +} + +void Features::Surface::write_feature(std::ostream &ost) const +{ + ost << "P 0"; +} + +void Features::Surface::write(std::ostream &ost) const +{ + Features::Feature::write(ost); + data.write(ost); + ost << "SE" << endl; +} + +void Features::write(std::ostream &ost) const +{ + if (features.size() == 0) + return; + ost << "UNITS=MM" << endl; + ost << "#Symbols" << endl; + for (const auto &[diameter, n] : symbols_circle) { + ost << "$" << n << " " << make_symbol_circle(diameter) << endl; + } + for (const auto &[dim, n] : symbols_rect) { + ost << "$" << n << " " << make_symbol_rect(dim.first, dim.second) << endl; + } + for (const auto &[dim, n] : symbols_oval) { + ost << "$" << n << " " << make_symbol_oval(dim.first, dim.second) << endl; + } + for (const auto &[name, n] : symbols_pad) { + ost << "$" << n << " " << name << endl; + } + write_attributes(ost); + for (const auto &feat : features) { + feat->write(ost); + } +} +} // namespace horizon::ODB diff --git a/src/export_odb/features.hpp b/src/export_odb/features.hpp new file mode 100644 index 000000000..6606502f9 --- /dev/null +++ b/src/export_odb/features.hpp @@ -0,0 +1,164 @@ +#pragma once +#include "attribute_util.hpp" +#include "common/common.hpp" +#include "util/placement.hpp" +#include "surface_data.hpp" +#include + +namespace horizon { +class Shape; +} + +namespace horizon::ODB { + +class Features : public AttributeProvider { +public: + class Feature : public RecordWithAttributes { + public: + template using check_type = attribute::is_feature; + + friend Features; + virtual void write(std::ostream &ost) const; + + const unsigned int index; + + virtual ~Feature() = default; + + protected: + Feature(unsigned int i) : index(i) + { + } + + enum class Type { LINE, ARC, PAD, SURFACE }; + virtual Type get_type() const = 0; + virtual void write_feature(std::ostream &ost) const = 0; + }; + + class Line : public Feature { + public: + Type get_type() const override + { + return Type::LINE; + } + + Line(unsigned int i, const Coordi &f, const Coordi &t, unsigned int sym) + : Feature(i), from(f), to(t), symbol(sym) + { + } + Coordi from; + Coordi to; + unsigned int symbol; + + protected: + void write_feature(std::ostream &ost) const override; + }; + + + class Arc : public Feature { + public: + Type get_type() const override + { + return Type::ARC; + } + + enum class Direction { CW, CCW }; + + Arc(unsigned int i, const Coordi &f, const Coordi &t, const Coordi &c, unsigned int sym, Direction d) + : Feature(i), from(f), to(t), center(c), symbol(sym), direction(d) + { + } + Coordi from; + Coordi to; + Coordi center; + + unsigned int symbol; + Direction direction; + + protected: + void write_feature(std::ostream &ost) const override; + }; + + class Pad : public Feature { + public: + Type get_type() const override + { + return Type::PAD; + } + + Pad(unsigned int i, const Placement &pl, unsigned int sym) : Feature(i), placement(pl), symbol(sym) + { + } + + Placement placement; + unsigned int symbol; + + protected: + void write_feature(std::ostream &ost) const override; + }; + + class Surface : public Feature { + public: + Surface(unsigned int i) : Feature(i) + { + } + + void write(std::ostream &ost) const override; + Type get_type() const override + { + return Type::SURFACE; + } + + SurfaceData data; + + protected: + void write_feature(std::ostream &ost) const override; + }; + + Line &draw_line(const Coordi &from, const Coordi &to, uint64_t width); + Arc &draw_arc(const Coordi &from, const Coordi &to, const Coordi ¢er, uint64_t width); + + std::vector draw_polygon_outline(const Polygon &poly, const Placement &transform); + + Pad &draw_pad(const std::string &sym, const Placement &transform); + Pad &draw_circle(const Coordi &pos, uint64_t diameter); + Pad &draw_shape(const Shape &shape); + Surface &add_surface(); + + void write(std::ostream &ost) const; + +private: + unsigned int get_or_create_symbol_circle(uint64_t diameter); + unsigned int get_or_create_symbol_pad(const std::string &name); + unsigned int get_or_create_symbol_rect(uint64_t width, uint64_t height); + unsigned int get_or_create_symbol_oval(uint64_t width, uint64_t height); + + unsigned int symbol_n = 0; + + template unsigned int get_or_create_symbol(std::map &syms, const T &key) + { + if (syms.count(key)) { + return syms.at(key); + } + else { + auto n = symbol_n++; + syms.emplace(key, n); + return n; + } + } + + std::map symbols_circle; // diameter -> symbol index + std::map symbols_pad; // name -> symbol index + std::map, unsigned int> symbols_rect; // w,h -> symbol index + std::map, unsigned int> symbols_oval; // w,h -> symbol index + + template T &add_feature(Args &&... args) + { + auto f = std::make_unique(features.size(), std::forward(args)...); + auto &r = *f; + features.push_back(std::move(f)); + return r; + } + std::list> features; +}; + +} // namespace horizon::ODB diff --git a/src/export_odb/odb_export.cpp b/src/export_odb/odb_export.cpp new file mode 100644 index 000000000..3c37aded0 --- /dev/null +++ b/src/export_odb/odb_export.cpp @@ -0,0 +1,238 @@ +#include "odb_export.hpp" +#include "board/board.hpp" +#include "board/gerber_output_settings.hpp" +#include "board/board_layers.hpp" +#include "canvas_odb.hpp" +#include "db.hpp" +#include "odb_util.hpp" +#include "export_util/tree_writer_fs.hpp" +#include "export_util/tree_writer_archive.hpp" +#include "util/util.hpp" +#include "track_graph.hpp" +#include "util/str_util.hpp" + +namespace horizon { + + +struct Context { + Context(const Board &brd) : canvas(job, brd) + { + } + ODB::Job job; + CanvasODB canvas; + + auto &add_layer(const std::string &step_name, int layer_n, ODB::Matrix::Layer::Context context, + ODB::Matrix::Layer::Type type) + { + const auto name = ODB::get_layer_name(layer_n); + auto &layer = job.add_matrix_layer(name); + layer.context = context; + layer.type = type; + canvas.layer_features.emplace(layer_n, &job.steps.at(step_name).layer_features.at(name)); + return layer; + } +}; + +static std::string get_step_name(const Block &block) +{ + if (block.project_meta.count("project_name")) + return ODB::make_legal_entity_name(block.project_meta.at("project_name")); + else + return "pcb"; +} + +void export_odb(const Board &brd, const ODBOutputSettings &settings) +{ + if (brd.board_panels.size()) + throw std::runtime_error("panels aren't supported yet"); + + Context ctx{brd}; + const std::string step_name = get_step_name(*brd.block); + ctx.job.add_step(step_name); + auto &step = ctx.job.steps.at(step_name); + ctx.canvas.eda_data = &step.eda_data; + + auto job_name_from_settings = settings.job_name; + trim(job_name_from_settings); + if (job_name_from_settings.size()) + ctx.job.job_name = ODB::make_legal_entity_name(job_name_from_settings); + else + ctx.job.job_name = step_name; + + bool have_top_comp = false; + bool have_bottom_comp = false; + { + std::map pkgs; + for (const auto &[uu, pkg] : brd.packages) { + pkgs.emplace(pkg.package.uuid, &pkg.package); + if (pkg.flip) + have_bottom_comp = true; + else + have_top_comp = true; + } + for (auto &[uu, pkg] : pkgs) { + step.eda_data.add_package(*pkg); + } + } + + + using OLayer = ODB::Matrix::Layer; + + // comp top + if (have_top_comp) { + auto &layer = ctx.job.add_matrix_layer("comp_+_top"); + layer.context = ODB::Matrix::Layer::Context::BOARD; + layer.type = ODB::Matrix::Layer::Type::COMPONENT; + step.comp_top.emplace(); + } + + // paste top + ctx.add_layer(step_name, BoardLayers::TOP_PASTE, OLayer::Context::BOARD, OLayer::Type::SOLDER_PASTE); + + // silk top + ctx.add_layer(step_name, BoardLayers::TOP_SILKSCREEN, OLayer::Context::BOARD, OLayer::Type::SILK_SCREEN); + + // mask top + ctx.add_layer(step_name, BoardLayers::TOP_MASK, OLayer::Context::BOARD, OLayer::Type::SOLDER_MASK); + + // copper(signal) layers + ctx.add_layer(step_name, BoardLayers::TOP_COPPER, OLayer::Context::BOARD, OLayer::Type::SIGNAL); + for (unsigned int i = 0; i < brd.get_n_inner_layers(); i++) { + ctx.add_layer(step_name, -((int)i) - 1, OLayer::Context::BOARD, OLayer::Type::SIGNAL); + } + ctx.add_layer(step_name, BoardLayers::BOTTOM_COPPER, OLayer::Context::BOARD, OLayer::Type::SIGNAL); + + // mask bot + ctx.add_layer(step_name, BoardLayers::BOTTOM_MASK, OLayer::Context::BOARD, OLayer::Type::SOLDER_MASK); + + // silk bot + ctx.add_layer(step_name, BoardLayers::BOTTOM_SILKSCREEN, OLayer::Context::BOARD, OLayer::Type::SILK_SCREEN); + + // paste bot + ctx.add_layer(step_name, BoardLayers::BOTTOM_PASTE, OLayer::Context::BOARD, OLayer::Type::SOLDER_PASTE); + + // comp bot + if (have_bottom_comp) { + auto &layer = ctx.job.add_matrix_layer("comp_+_bot"); + layer.context = ODB::Matrix::Layer::Context::BOARD; + layer.type = ODB::Matrix::Layer::Type::COMPONENT; + step.comp_bot.emplace(); + } + + // drills + { + auto &layer = ctx.job.add_matrix_layer(ODB::drills_layer); + layer.context = ODB::Matrix::Layer::Context::BOARD; + layer.type = ODB::Matrix::Layer::Type::DRILL; + layer.span = {ODB::get_layer_name(BoardLayers::TOP_COPPER), ODB::get_layer_name(BoardLayers::BOTTOM_COPPER)}; + + ctx.canvas.drill_features = &step.layer_features.at(ODB::drills_layer); + } + // rout + // misc/doc + + ctx.add_layer(step_name, BoardLayers::TOP_ASSEMBLY, OLayer::Context::MISC, OLayer::Type::DOCUMENT); + ctx.add_layer(step_name, BoardLayers::BOTTOM_ASSEMBLY, OLayer::Context::MISC, OLayer::Type::DOCUMENT); + + for (const auto &[uu, net] : brd.block->nets) { + step.eda_data.add_net(net); + } + for (const auto &[uu, pkg] : brd.packages) { + auto &comp = step.add_component(pkg); + auto &opkg = step.eda_data.get_package(pkg.package.uuid); + + + { + auto pads_sorted = pkg.package.get_pads_sorted(); + for (const auto pad : pads_sorted) { + const auto net_uu = pad->net ? pad->net->uuid : UUID{}; + auto &net = step.eda_data.get_net(net_uu); + using ST = ODB::EDAData::SubnetToeprint; + const auto toep_num = comp.toeprints.size(); + auto &subnet = net.add_subnet(pkg.flip ? ST::Side::BOTTOM : ST::Side::TOP, comp.index, toep_num); + ctx.canvas.pad_subnets.emplace(std::piecewise_construct, std::forward_as_tuple(uu, pad->uuid), + std::forward_as_tuple(&subnet)); + + auto &toep = comp.toeprints.emplace_back(opkg.get_pin(pad->uuid)); + toep.net_num = net.index; + toep.subnet_num = subnet.index; + + auto pl = pkg.placement; + if (pkg.flip) + pl.invert_angle(); + toep.placement.set_angle(pl.get_angle()); + toep.placement.shift = pl.transform(pad->placement.shift); + } + } + } + + { + std::map graphs; + for (const auto &[uu, track] : brd.tracks) { + if (!track.net) + continue; + auto &gr = graphs[track.net->uuid]; + gr.add_track(track); + } + for (auto &[uu, gr] : graphs) { +//#define DUMP_TRACK_GRAPH +#ifdef DUMP_TRACK_GRAPH + const auto name = brd.block->nets.at(uu).name + "_" + (std::string)uu; + const auto filename = "/tmp/nets/" + name + ".dot"; + gr.dump(brd, filename); +#endif + + gr.merge_edges(); + +#ifdef DUMP_TRACK_GRAPH + const auto filename_merged = "/tmp/nets/" + name + "-merged.dot"; + gr.dump(brd, filename_merged); +#endif + auto &net = step.eda_data.get_net(uu); + for (auto &edge : gr.edges) { + if (edge.tracks.size()) { + auto &subnet = net.add_subnet(); + for (auto track : edge.tracks) { + ctx.canvas.track_subnets.emplace(track, &subnet); + } + } + } + } + } + + ctx.canvas.update(brd); + + if (auto outline = brd.get_outline(); outline.outline.vertices.size()) { + auto &profile_feats = step.profile.emplace(); + auto &surf = profile_feats.add_surface(); + surf.data.append_polygon(outline.outline); + for (const auto &hole : outline.holes) { + surf.data.append_polygon(hole); + } + } + + + if (settings.format == ODBOutputSettings::Format::DIRECTORY) { + const auto dest_dir = fs::u8path(settings.output_directory) / ctx.job.job_name; + if (fs::exists(dest_dir)) { + if (!fs::is_empty(dest_dir)) { + // make sure that we're not removing something that doesn't look like a valid ODB++ job + if (!fs::is_directory(dest_dir / "matrix")) + throw std::runtime_error(dest_dir.u8string() + " doesn't look like a valid ODB++ job"); + } + fs::remove_all(dest_dir); + } + TreeWriterFS tree_writer(fs::u8path(settings.output_directory)); + ctx.job.write(tree_writer); + } + else { + static const std::map type_map = { + {ODBOutputSettings::Format::TGZ, TreeWriterArchive::Type::TGZ}, + {ODBOutputSettings::Format::ZIP, TreeWriterArchive::Type::ZIP}, + }; + TreeWriterArchive tree_writer(fs::u8path(settings.output_filename), type_map.at(settings.format)); + ctx.job.write(tree_writer); + } +} + +} // namespace horizon diff --git a/src/export_odb/odb_export.hpp b/src/export_odb/odb_export.hpp new file mode 100644 index 000000000..e570e52c6 --- /dev/null +++ b/src/export_odb/odb_export.hpp @@ -0,0 +1,9 @@ +#pragma once +#include +#include + +namespace horizon { + +void export_odb(const class Board &brd, const class ODBOutputSettings &settings); + +} // namespace horizon diff --git a/src/export_odb/odb_util.cpp b/src/export_odb/odb_util.cpp new file mode 100644 index 000000000..144922552 --- /dev/null +++ b/src/export_odb/odb_util.cpp @@ -0,0 +1,132 @@ +#include "odb_util.hpp" +#include +#include +#include +#include "board/board_layers.hpp" + +namespace horizon::ODB { + +const char *endl = "\r\n"; +const char *drills_layer = "drills"; + +std::ostream &operator<<(std::ostream &os, const Coordi &c) +{ + return os << std::fixed << std::setprecision(6) << c.x / 1e6 << " " << c.y / 1e6; +} + +std::ostream &operator<<(std::ostream &os, Angle a) +{ + return os << std::fixed << std::setprecision(1) << std::fixed << a.angle; +} + +std::ostream &operator<<(std::ostream &os, Dim d) +{ + return os << std::fixed << std::setprecision(6) << std::fixed << d.dim; +} + +std::ostream &operator<<(std::ostream &os, DimUm d) +{ + return os << std::fixed << std::setprecision(3) << std::fixed << d.dim; +} + +std::string utf8_to_ascii(const std::string &s) +{ + return Glib::convert_with_fallback(s, "ascii//TRANSLIT", "utf-8"); +} + +std::string make_legal_name(const std::string &n) +{ + std::string out; + out.reserve(n.size()); + for (auto c : utf8_to_ascii(n)) { + if (c == ';' || isspace(c)) + c = '_'; + else if (isgraph(c)) + ; + else + c = '_'; + out.append(1, c); + } + + return out; +} + +std::string make_legal_entity_name(const std::string &s) +{ + std::string out; + out.reserve(s.size()); + for (auto c : utf8_to_ascii(s)) { + if (isalpha(c)) + c = tolower(c); + else if (isdigit(c) || (c == '-') || (c == '_') || (c == '+')) + ; // it's okay + else + c = '_'; + out.append(1, c); + } + + return out; +} + +std::string get_layer_name(int id) +{ + if (id == BoardLayers::TOP_COPPER) { + return "signal_top"; + } + else if (id == BoardLayers::BOTTOM_COPPER) { + return "signal_bottom"; + } + else if (id < BoardLayers::TOP_COPPER && id > BoardLayers::BOTTOM_COPPER) { + return "signal_inner_" + std::to_string(-id); + } + else if (id == BoardLayers::TOP_SILKSCREEN) { + return "silkscreen_top"; + } + else if (id == BoardLayers::BOTTOM_SILKSCREEN) { + return "silkscreen_bottom"; + } + else if (id == BoardLayers::TOP_MASK) { + return "mask_top"; + } + else if (id == BoardLayers::BOTTOM_MASK) { + return "mask_bottom"; + } + else if (id == BoardLayers::TOP_PASTE) { + return "paste_top"; + } + else if (id == BoardLayers::BOTTOM_PASTE) { + return "paste_bottom"; + } + else if (id == BoardLayers::TOP_ASSEMBLY) { + return "assembly_top"; + } + else if (id == BoardLayers::BOTTOM_ASSEMBLY) { + return "assembly_bottom"; + } + else { + return "layer_id_" + std::to_string(id); + } +} + +std::string make_symbol_circle(uint64_t diameter) +{ + std::ostringstream oss; + oss << "r" << DimUm{diameter} << " M"; + return oss.str(); +} + +std::string make_symbol_rect(uint64_t w, uint64_t h) +{ + std::ostringstream oss; + oss << "rect" << DimUm{w} << "x" << DimUm{h} << " M"; + return oss.str(); +} + +std::string make_symbol_oval(uint64_t w, uint64_t h) +{ + std::ostringstream oss; + oss << "oval" << DimUm{w} << "x" << DimUm{h} << " M"; + return oss.str(); +} + +} // namespace horizon::ODB diff --git a/src/export_odb/odb_util.hpp b/src/export_odb/odb_util.hpp new file mode 100644 index 000000000..d5e531d82 --- /dev/null +++ b/src/export_odb/odb_util.hpp @@ -0,0 +1,62 @@ +#pragma once +#include "common/common.hpp" +#include "util/placement.hpp" +#include + +namespace horizon::ODB { + +extern const char *endl; +extern const char *drills_layer; + +std::ostream &operator<<(std::ostream &os, const Coordi &c); + +struct Angle { + explicit Angle(int a) : angle((((65536 - a) % 65536) * (360. / 65536.))) + { + } + explicit Angle(const Placement &pl) : Angle(pl.get_angle()) + { + } + const double angle; +}; + +std::ostream &operator<<(std::ostream &os, Angle a); + +struct Dim { + explicit Dim(int64_t x) : dim(x / 1e6) + { + } + explicit Dim(uint64_t x) : dim(x / 1e6) + { + } + explicit Dim(double x) : dim(x / 1e6) + { + } + const double dim; +}; + +std::ostream &operator<<(std::ostream &os, Dim d); + +struct DimUm { + explicit DimUm(int64_t x) : dim(x / 1e3) + { + } + explicit DimUm(uint64_t x) : dim(x / 1e3) + { + } + const double dim; +}; + +std::ostream &operator<<(std::ostream &os, DimUm d); + +std::string utf8_to_ascii(const std::string &s); +std::string make_legal_name(const std::string &n); +std::string make_legal_entity_name(const std::string &s); +std::string get_layer_name(int id); + +std::string make_symbol_circle(uint64_t diameter); +std::string make_symbol_rect(uint64_t w, uint64_t h); +std::string make_symbol_oval(uint64_t w, uint64_t h); + + +} // namespace horizon::ODB diff --git a/src/export_odb/structured_text_writer.cpp b/src/export_odb/structured_text_writer.cpp new file mode 100644 index 000000000..bffa16061 --- /dev/null +++ b/src/export_odb/structured_text_writer.cpp @@ -0,0 +1,52 @@ +#include "structured_text_writer.hpp" + +namespace horizon { + +StructuredTextWriter::StructuredTextWriter(std::ostream &s) : ost(s) +{ +} + +void StructuredTextWriter::write_line(const std::string &var, const std::string &value) +{ + write_indent(); + ost << var << "=" << value << "\r\n"; +} + +void StructuredTextWriter::write_line(const std::string &var, int value) +{ + write_indent(); + ost << var << "=" << value << "\r\n"; +} + +void StructuredTextWriter::write_indent() +{ + if (in_array) + ost << " "; +} + +void StructuredTextWriter::begin_array(const std::string &a) +{ + if (in_array) + throw std::runtime_error("already in array"); + in_array = true; + ost << a << " {\r\n"; +} + +void StructuredTextWriter::end_array() +{ + if (!in_array) + throw std::runtime_error("not in array"); + in_array = false; + ost << "}\r\n\r\n"; +} + +StructuredTextWriter::ArrayProxy::ArrayProxy(StructuredTextWriter &wr, const std::string &a) : writer(wr) +{ + writer.begin_array(a); +} + +StructuredTextWriter::ArrayProxy::~ArrayProxy() +{ + writer.end_array(); +} +} // namespace horizon diff --git a/src/export_odb/structured_text_writer.hpp b/src/export_odb/structured_text_writer.hpp new file mode 100644 index 000000000..4a278e0e1 --- /dev/null +++ b/src/export_odb/structured_text_writer.hpp @@ -0,0 +1,45 @@ +#pragma once +#include + +namespace horizon { +class StructuredTextWriter { +public: + StructuredTextWriter(std::ostream &s); + void write_line(const std::string &var, const std::string &value); + void write_line(const std::string &var, int value); + template void write_line_enum(const std::string &var, const T &value) + { + write_line(var, enum_to_string(value)); + } + + class ArrayProxy { + friend StructuredTextWriter; + + public: + ~ArrayProxy(); + + private: + ArrayProxy(StructuredTextWriter &writer, const std::string &a); + + StructuredTextWriter &writer; + + ArrayProxy(ArrayProxy &&) = delete; + ArrayProxy &operator=(ArrayProxy &&) = delete; + + ArrayProxy(ArrayProxy const &) = delete; + ArrayProxy &operator=(ArrayProxy const &) = delete; + }; + + [[nodiscard]] ArrayProxy make_array_proxy(const std::string &a) + { + return ArrayProxy(*this, a); + } + +private: + void write_indent(); + void begin_array(const std::string &a); + void end_array(); + std::ostream &ost; + bool in_array = false; +}; +} // namespace horizon diff --git a/src/export_odb/surface_data.cpp b/src/export_odb/surface_data.cpp new file mode 100644 index 000000000..db1668b5d --- /dev/null +++ b/src/export_odb/surface_data.cpp @@ -0,0 +1,59 @@ +#include "surface_data.hpp" +#include "common/polygon.hpp" +#include "util/once.hpp" +#include "odb_util.hpp" + +namespace horizon::ODB { +void SurfaceData::append_polygon(const Polygon &poly, const Placement &transform) +{ + lines.emplace_back(); + auto &contour = lines.back(); + contour.reserve(poly.vertices.size()); + for (size_t i = 0; i < poly.vertices.size(); i++) { + const auto &v = poly.vertices.at(i); + const auto &v_next = poly.get_vertex(i - 1); + using D = SurfaceLine::Direction; + if (v_next.type == Polygon::Vertex::Type::LINE) + contour.emplace_back(transform.transform(v.position)); + else + contour.emplace_back(transform.transform(v.position), transform.transform(v_next.arc_center), + (v_next.arc_reverse != transform.mirror) ? D::CW : D::CCW); + } +} + +void SurfaceData::append_polygon_auto_orientation(const Polygon &poly, const Placement &transform) +{ + const bool need_cw = lines.size() == 0; // first one is cw + const bool need_reverse = (poly.is_ccw() != transform.mirror) != need_cw; + if (need_reverse) { + auto poly2 = poly; + poly2.reverse(); + append_polygon(poly2, transform); + } + else { + append_polygon(poly, transform); + } +} + + +void SurfaceData::write(std::ostream &ost) const +{ + Once is_island; + for (const auto &contour : lines) { + ost << "OB " << contour.back().end << " "; + if (is_island()) + ost << "I"; + else + ost << "H"; + ost << endl; + for (const auto &line : contour) { + if (line.type == SurfaceLine::Type::SEGMENT) + ost << "OS " << line.end << endl; + else + ost << "OC " << line.end << " " << line.center << " " + << (line.direction == SurfaceLine::Direction::CW ? "Y" : "N") << endl; + } + ost << "OE" << endl; + } +} +} // namespace horizon::ODB diff --git a/src/export_odb/surface_data.hpp b/src/export_odb/surface_data.hpp new file mode 100644 index 000000000..b885b1243 --- /dev/null +++ b/src/export_odb/surface_data.hpp @@ -0,0 +1,50 @@ +#pragma once +#include +#include +#include +#include "attributes.hpp" +#include +#include +#include "common/common.hpp" +#include "util/placement.hpp" + +namespace horizon { +class Polygon; +} // namespace horizon + +namespace horizon::ODB { + +class SurfaceData { +public: + class SurfaceLine { + public: + enum class Direction { CW, CCW }; + enum class Type { SEGMENT, ARC }; + + SurfaceLine(const Coordi &c) : end(c) + { + } + SurfaceLine(const Coordi &e, const Coordi &c, Direction d) : end(e), type(Type::ARC), center(c), direction(d) + { + } + + Coordi end; + Type type = Type::SEGMENT; + + + Coordi center; + Direction direction; + }; + + void write(std::ostream &ost) const; + + void append_polygon(const Polygon &poly, const Placement &transform = Placement()); + void append_polygon_auto_orientation(const Polygon &poly, const Placement &transform = Placement()); + + + // first one is contour (island) oriented clockwise + // remainder are holes oriented counter clockwise + std::vector> lines; +}; + +} // namespace horizon::ODB diff --git a/src/export_odb/symbol.cpp b/src/export_odb/symbol.cpp new file mode 100644 index 000000000..9334c871f --- /dev/null +++ b/src/export_odb/symbol.cpp @@ -0,0 +1,32 @@ +#include "symbol.hpp" +#include "odb_util.hpp" +#include "pool/padstack.hpp" +#include "export_util/tree_writer.hpp" + +namespace horizon::ODB { +Symbol::Symbol(const Padstack &ps, int layer) +{ + name = "_" + make_legal_entity_name(ps.name) + "_" + get_layer_name(layer); + for (const auto &[uu, shape] : ps.shapes) { + if (layer == shape.layer) + features.draw_shape(shape); + } + for (const auto &[uu, ipoly] : ps.polygons) { + if (layer == ipoly.layer) { + auto poly = ipoly; + if (poly.is_ccw()) + poly.reverse(); + + auto &surf = features.add_surface(); + surf.data.append_polygon(poly); + } + } +} + +void Symbol::write(TreeWriter &writer) const +{ + auto file = writer.create_file("features"); + features.write(file.stream); +} + +} // namespace horizon::ODB diff --git a/src/export_odb/symbol.hpp b/src/export_odb/symbol.hpp new file mode 100644 index 000000000..0739d9634 --- /dev/null +++ b/src/export_odb/symbol.hpp @@ -0,0 +1,21 @@ +#pragma once +#include "features.hpp" +#include + +namespace horizon { +class Padstack; +class TreeWriter; +} // namespace horizon + +namespace horizon::ODB { + +class Symbol { +public: + Symbol(const Padstack &ps, int layer); + + std::string name; + Features features; + + void write(TreeWriter &writer) const; +}; +} // namespace horizon::ODB diff --git a/src/export_odb/track_graph.cpp b/src/export_odb/track_graph.cpp new file mode 100644 index 000000000..10b116370 --- /dev/null +++ b/src/export_odb/track_graph.cpp @@ -0,0 +1,118 @@ +#include "track_graph.hpp" +#include "util/util.hpp" +#include "util/geom_util.hpp" +#include "board/board.hpp" + +namespace horizon { + +static std::pair key_from_connection(const Track::Connection &conn) +{ + if (conn.is_junc()) + return {conn.junc->uuid, UUID()}; + else if (conn.is_pad()) + return {conn.package->uuid, conn.pad->uuid}; + else + assert(false); +} + +TrackGraph::Node &TrackGraph::get_or_create_node(const Track::Connection &conn) +{ + const auto key = key_from_connection(conn); + if (nodes.count(key)) { + return nodes.at(key); + } + else { + auto &n = nodes[key]; + if (conn.is_junc()) + n.keep = conn.junc->needs_via; + else if (conn.is_pad()) + n.keep = true; + + return n; + } +} + +void TrackGraph::add_track(const Track &track) +{ + auto &n_from = get_or_create_node(track.from); + auto &n_to = get_or_create_node(track.to); + edges.emplace_back(n_from, n_to, track); + auto &edge = edges.back(); + n_from.edges.push_back(&edge); + n_to.edges.push_back(&edge); +} + +TrackGraph::Node *TrackGraph::Edge::get_other_node(Node *n) const +{ + if (from == n) + return to; + else if (to == n) + return from; + else + assert(false); +} + +void TrackGraph::merge_edges() +{ + // before [n1] --e1-- [node] --e2-- [n2] + // after [n1] ---------e1--------- [n2] + + for (auto &[conn, node] : nodes) { + if (node.edges.size() == 2 && !node.keep) { + auto x = node.edges.begin(); + auto e1 = *x; + x++; + auto e2 = *x; + Node *n1 = e1->get_other_node(&node); + Node *n2 = e2->get_other_node(&node); + + assert(std::count(n2->edges.begin(), n2->edges.end(), e2)); + n2->edges.remove(e2); + n2->edges.push_back(e1); + + e1->from = n1; + e1->to = n2; + e1->tracks.insert(e2->tracks.begin(), e2->tracks.end()); + + node.edges.clear(); + e2->from = nullptr; + e2->to = nullptr; + e2->tracks.clear(); + } + } +} + +static std::string get_conn_label(const class Board &brd, const std::pair &key) +{ + if (key.second == UUID()) { + return "Junction " + coord_to_string(brd.junctions.at(key.first).position); + } + else { + const auto &pkg = brd.packages.at(key.first); + const auto &pad = pkg.package.pads.at(key.second); + return pkg.component->refdes + "." + pad.name; + } +} + +void TrackGraph::dump(const class Board &brd, const std::string &filename) const +{ + auto ofs = make_ofstream(filename); + ofs << "graph {\n"; + for (const auto &[key, node] : nodes) { + if (node.edges.size()) + ofs << "N" << &node << " [label=\"" << get_conn_label(brd, key) << "\"" << (node.keep ? "shape=box" : "") + << "]\n"; + } + for (const auto &edge : edges) { + if (edge.from && edge.to) { + ofs << "N" << edge.from << " -- N" << edge.to << "[label=\""; + for (auto track : edge.tracks) { + ofs << static_cast(track) << " "; + } + ofs << "\"]\n"; + } + } + ofs << "}"; +} + +} // namespace horizon diff --git a/src/export_odb/track_graph.hpp b/src/export_odb/track_graph.hpp new file mode 100644 index 000000000..70413438c --- /dev/null +++ b/src/export_odb/track_graph.hpp @@ -0,0 +1,38 @@ +#pragma once +#include +#include "board/track.hpp" + +namespace horizon { +class TrackGraph { +public: + class Edge; + + class Node { + public: + bool keep = false; + std::list edges; + }; + + class Edge { + public: + Edge(Node &f, Node &t, const Track &tr) : from(&f), to(&t), tracks({tr.uuid}) + { + } + + Node *from = nullptr; + Node *to = nullptr; + + Node *get_other_node(Node *n) const; + + std::set tracks; + }; + + void merge_edges(); + void dump(const class Board &brd, const std::string &filename) const; + + Node &get_or_create_node(const Track::Connection &conn); + void add_track(const Track &track); + std::map, Node> nodes; + std::list edges; +}; +} // namespace horizon diff --git a/src/imp/fab_output.ui b/src/imp/fab_output.ui index 0fd76486f..5be98193a 100644 --- a/src/imp/fab_output.ui +++ b/src/imp/fab_output.ui @@ -69,49 +69,175 @@ False window-close-symbolic + + True + False + window-close-symbolic + False dialog - + True False - 20 - 20 - 20 - vertical - 20 + False + False + crossfade True False + 20 + 20 + 20 + vertical 20 - + True False + 20 - + True False - 20 - vertical - 20 True False + 20 vertical - 10 + 20 - + True False - NC-Drill - 0 - - - + vertical + 10 + + + True + False + NC-Drill + 0 + + + + + + False + True + 0 + + + + + + True + False + 10 + 10 + + + True + False + end + NPTH suffix + 1 + + + + 0 + 1 + + + + + True + False + end + PTH suffix + 1 + + + + 0 + 2 + + + + + True + True + True + + + 1 + 1 + + + + + True + True + True + + + 1 + 2 + + + + + True + False + Mode + 1 + + + + 0 + 0 + + + + + True + False + True + + Merge PTH & NPTH + Individual + + + + 1 + 0 + + + + + + + + + + + + + + False + True + 1 + + False @@ -120,134 +246,540 @@ - - + True False - 10 - 10 + vertical + 10 - + True False - end - NPTH suffix - 1 - + Gerber Settings + 0 + + + - 0 - 1 + False + True + 0 - + + True False - end - PTH suffix - 1 - + 10 + 10 + + + True + False + end + Outline width + 1 + + + + 0 + 0 + + + + + True + False + True + vertical + + + + + + 1 + 0 + + + + + + + + + + + + + + + + + + + + + + + - 0 - 2 + False + True + 1 + + + False + True + 1 + + + + + True + False + vertical + 10 - + True - True - True + False + Output Settings + 0 + + + - 1 - 1 + False + True + 0 - + + True - True - True + False + 10 + 10 + + + True + False + end + Directory + 1 + + + + 0 + 1 + + + + + True + False + end + Base filename + 1 + + + + 0 + 0 + + + + + True + True + True + + + 1 + 0 + + + + + True + False + + + True + True + + + True + True + 0 + + + + + Browse… + True + True + True + + + False + True + 1 + + + + + + 1 + 1 + + + + + True + False + end + Generate Zip + 1 + + + + 0 + 2 + + + + + True + True + start + + + 1 + 2 + + + + + + + + + + + - 1 - 2 + False + True + 1 - + True False - Mode - 1 + Resulting filenames are the concatentation of base filename and suffix. + True + 0 + + + - 0 - 0 + False + True + 2 + + + False + True + 2 + + + + + -1 + + + + + True + False + center + start + + + True + False + 0 + none - + True False - True - - Merge PTH & NPTH - Individual - + 5 + + + True + False + wrote to somewhere + + + False + True + 0 + + + + + True + True + True + image1 + + + False + True + 1 + + - - 1 - 0 - - - - - + + + + + + + + + False + True + 0 + + + + + True + False + 20 + vertical + 10 + + + True + False + Gerber Layers + 0 + + + + + + False + True + 0 + + + + + True + True + never + in + + + True + False - + + True + False + none + False + + + + + True + True + 1 + + + + + False + True + 1 + + + + + True + True + 0 + + + + + True + True + never + in + 100 + + + True + True + False + + + + + False + True + 2 + + + + + gerber + Gerber + + + + + True + False + 20 + 20 + 20 + vertical + + + True + False + + + + True + False + 20 + 10 + 10 + + + True + False + end + Filename + 1 + + + + 0 + 2 + + + + + True + False + True + + + True + True + + + True + True + 0 + + + + + Browse… + True + True + True + False True 1 + - False - True - 0 + 1 + 2 + + + + + True + False + end + Format + 1 + + + + 0 + 1 True False - vertical - 10 + True - + + Tarball (.tgz) True - False - Gerber Settings - 0 - - - + True + False + True + False + odb_format_directory_rb False @@ -256,64 +788,13 @@ - - + + Directory True - False - 10 - 10 - - - True - False - end - Outline width - 1 - - - - 0 - 0 - - - - - True - False - True - vertical - - - - - - 1 - 0 - - - - - - - - - - - - - - - - - - - - - - - + True + False + True + False False @@ -321,11 +802,63 @@ 1 + + + ZIP archive (.zip) + True + True + False + True + False + odb_format_directory_rb + + + False + True + 2 + + + - False - True - 1 + 1 + 1 + + + + + True + False + end + start + Directory + 1 + + + + 0 + 3 + + + + + True + False + end + start + Job name + 1 + + + + 0 + 0 @@ -333,159 +866,93 @@ True False vertical - 10 + 5 + + + True + True + + + False + True + 0 + + - + True False - Output Settings + Leave blank to use project name 0 - + + False True - 0 + 1 + + + 1 + 0 + + + + + True + False + vertical + 5 - - + True False - 10 - 10 - - - True - False - end - Directory - 1 - - - - 0 - 1 - - - - - True - False - end - Base filename - 1 - - - - 0 - 0 - - + True - + True True - True - - - 1 - 0 - - - - - True - False - - - True - True - - - True - True - 0 - - - - - Browse… - True - True - True - - - False - True - 1 - - - - - - 1 - 1 - - - - - True - False - end - Generate Zip - 1 - - 0 - 2 + True + True + 0 - + + Browse… True True - start + True - 1 - 2 + False + True + 1 - - - - - - - - - + False True - 1 + 0 - + True False - Resulting filenames are the concatentation of base filename and suffix. - True + Directory in which the job directory will be created 0 @@ -497,14 +964,13 @@ False True - 2 + 1 - False - True - 2 + 1 + 3 @@ -513,7 +979,7 @@ - + True False center @@ -530,7 +996,7 @@ False 5 - + True False wrote to somewhere @@ -542,11 +1008,11 @@ - + True True True - image1 + image2 False @@ -570,92 +1036,14 @@ False True - 0 - - - - - True - False - 20 - vertical - 10 - - - True - False - Gerber Layers - 0 - - - - - - False - True - 0 - - - - - True - True - never - in - - - True - False - - - True - False - none - False - - - - - - - True - True - 1 - - - - - False - True - 2 + 1 - True - True - 0 - - - - - True - True - never - in - 100 - - - True - True - False - - - - - False - True - 2 + odb + ODB++ + 1 @@ -664,8 +1052,14 @@ True False - Fabrication outputs True + + + True + False + stack + + Generate @@ -677,6 +1071,9 @@ + + + @@ -690,4 +1087,12 @@ + + vertical + + + + + + diff --git a/src/imp/fab_output_window.cpp b/src/imp/fab_output_window.cpp index 8c4949cf8..49f8725a6 100644 --- a/src/imp/fab_output_window.cpp +++ b/src/imp/fab_output_window.cpp @@ -1,11 +1,13 @@ #include "fab_output_window.hpp" #include "board/board.hpp" #include "export_gerber/gerber_export.hpp" +#include "export_odb/odb_export.hpp" #include "util/gtk_util.hpp" #include "widgets/spin_button_dim.hpp" #include "document/idocument_board.hpp" #include "rules/rules_with_core.hpp" #include "rules/cache.hpp" +#include "util/util.hpp" namespace horizon { @@ -48,9 +50,44 @@ GerberLayerEditor *GerberLayerEditor::create(FabOutputWindow &pa, GerberOutputSe return w; } +FabOutputWindow::ODBExportFileChooserFilename::ODBExportFileChooserFilename(const ODBOutputSettings &set) + : settings(set) +{ +} + + +void FabOutputWindow::ODBExportFileChooserFilename::prepare_chooser(Glib::RefPtr chooser) +{ + auto filter = Gtk::FileFilter::create(); + if (settings.format == ODBOutputSettings::Format::TGZ) { + filter->set_name("Tarballs"); + filter->add_pattern("*.tgz"); + } + else if (settings.format == ODBOutputSettings::Format::ZIP) { + filter->set_name("ZIP archives"); + filter->add_pattern("*.zip"); + } + chooser->add_filter(filter); +} + +void FabOutputWindow::ODBExportFileChooserFilename::prepare_filename(std::string &filename) +{ + if (settings.format == ODBOutputSettings::Format::TGZ) { + if (!endswith(filename, ".tgz")) { + filename += ".tgz"; + } + } + else if (settings.format == ODBOutputSettings::Format::ZIP) { + if (!endswith(filename, ".zip")) { + filename += ".zip"; + } + } +} + FabOutputWindow::FabOutputWindow(BaseObjectType *cobject, const Glib::RefPtr &x, IDocumentBoard &c, const std::string &project_dir) : Gtk::Window(cobject), core(c), brd(*core.get_board()), settings(core.get_gerber_output_settings()), + odb_settings(core.get_odb_output_settings()), odb_export_filechooser_filename(odb_settings), state_store(this, "imp-fab-output") { GET_WIDGET(gerber_layers_box); @@ -68,8 +105,25 @@ FabOutputWindow::FabOutputWindow(BaseObjectType *cobject, const Glib::RefPtrset_visible_child(Board::output_format_lut.lookup_reverse(brd.output_format)); + stack->property_visible_child_name().signal_changed().connect([this] { + brd.output_format = Board::output_format_lut.lookup(stack->get_visible_child_name()); + s_signal_changed.emit(); + update_export_button(); + }); + + { + std::map format_map = { + {ODBOutputSettings::Format::DIRECTORY, odb_format_directory_rb}, + {ODBOutputSettings::Format::TGZ, odb_format_tgz_rb}, + {ODBOutputSettings::Format::ZIP, odb_format_zip_rb}, + }; + bind_widget(format_map, odb_settings.format, [this](auto v) { + s_signal_changed.emit(); + update_odb_visibility(); + update_export_button(); + }); + } + update_odb_visibility(); + + bind_widget(odb_job_name_entry, odb_settings.job_name, [this](std::string &) { s_signal_changed.emit(); }); + reload_layers(); update_export_button(); } @@ -171,6 +266,15 @@ void FabOutputWindow::update_drill_visibility() } } +void FabOutputWindow::update_odb_visibility() +{ + const bool is_dir = odb_settings.format == ODBOutputSettings::Format::DIRECTORY; + odb_directory_box->set_visible(is_dir); + odb_directory_label->set_visible(is_dir); + odb_filename_box->set_visible(!is_dir); + odb_filename_label->set_visible(!is_dir); +} + static void cb_nop(const std::string &) { } @@ -195,28 +299,45 @@ void FabOutputWindow::generate() } } - std::string error_str; - try { - GerberOutputSettings my_settings = settings; - my_settings.output_directory = export_filechooser.get_filename_abs(); - my_settings.zip_output = zip_output_switch->get_active(); - GerberExporter ex(brd, my_settings); - ex.generate(); - log_textview->get_buffer()->set_text(ex.get_log()); - done_revealer_controller.show_success("Gerber output done"); - } - catch (const std::exception &e) { - error_str = std::string("Error: ") + e.what(); - } - catch (const Gio::Error &e) { - error_str = std::string("Error: ") + e.what(); - } - catch (...) { - error_str = "Other error"; + if (brd.output_format == Board::OutputFormat::GERBER) { + std::string error_str; + try { + GerberOutputSettings my_settings = settings; + my_settings.output_directory = export_filechooser.get_filename_abs(); + my_settings.zip_output = zip_output_switch->get_active(); + GerberExporter ex(brd, my_settings); + ex.generate(); + log_textview->get_buffer()->set_text(ex.get_log()); + done_revealer_controller.show_success("Gerber output done"); + } + catch (const std::exception &e) { + error_str = std::string("Error: ") + e.what(); + } + catch (const Gio::Error &e) { + error_str = std::string("Error: ") + e.what(); + } + catch (...) { + error_str = "Other error"; + } + if (error_str.size()) { + log_textview->get_buffer()->set_text(error_str); + done_revealer_controller.show_error(error_str); + } } - if (error_str.size()) { - log_textview->get_buffer()->set_text(error_str); - done_revealer_controller.show_error(error_str); + else { + try { + ODBOutputSettings my_settings = odb_settings; + my_settings.output_filename = odb_export_filechooser_filename.get_filename_abs(); + my_settings.output_directory = odb_export_filechooser_directory.get_filename_abs(); + export_odb(brd, my_settings); + odb_done_revealer_controller.show_success("ODB++ output done"); + } + catch (const std::exception &e) { + odb_done_revealer_controller.show_error("Error: " + std::string(e.what())); + } + catch (...) { + odb_done_revealer_controller.show_error("unknown error"); + } } } @@ -230,33 +351,51 @@ void FabOutputWindow::update_export_button() { std::string txt; if (can_export) { - if (settings.output_directory.size()) { - if (settings.prefix.size()) { - if (settings.drill_mode == GerberOutputSettings::DrillMode::INDIVIDUAL) { - if (settings.drill_pth_filename.size() == 0 || settings.drill_npth_filename.size() == 0) { - txt = "drill filenames not set"; + if (brd.output_format == Board::OutputFormat::GERBER) { + if (settings.output_directory.size()) { + if (settings.prefix.size()) { + if (settings.drill_mode == GerberOutputSettings::DrillMode::INDIVIDUAL) { + if (settings.drill_pth_filename.size() == 0 || settings.drill_npth_filename.size() == 0) { + txt = "drill filenames not set"; + } } - } - else { - if (settings.drill_pth_filename.size() == 0) { - txt = "drill filename not set"; + else { + if (settings.drill_pth_filename.size() == 0) { + txt = "drill filename not set"; + } } - } - if (txt.size() == 0) { // still okay - for (const auto &it : settings.layers) { - if (it.second.enabled && it.second.filename.size() == 0) { - txt = brd.get_layers().at(it.first).name + " filename not set"; - break; + if (txt.size() == 0) { // still okay + for (const auto &it : settings.layers) { + if (it.second.enabled && it.second.filename.size() == 0) { + txt = brd.get_layers().at(it.first).name + " filename not set"; + break; + } } } } + else { + txt = "prefix not set"; + } } else { - txt = "prefix not set"; + txt = "output directory not set"; } } else { - txt = "output directory not set"; + switch (odb_settings.format) { + case ODBOutputSettings::Format::DIRECTORY: + if (odb_settings.output_directory.size() == 0) { + txt = "ODB++ directory not set"; + } + break; + + case ODBOutputSettings::Format::ZIP: + case ODBOutputSettings::Format::TGZ: + if (odb_settings.output_filename.size() == 0) { + txt = "ODB++ filename not set"; + } + break; + } } } else { diff --git a/src/imp/fab_output_window.hpp b/src/imp/fab_output_window.hpp index df7c88b8c..c53d22d03 100644 --- a/src/imp/fab_output_window.hpp +++ b/src/imp/fab_output_window.hpp @@ -25,6 +25,7 @@ class FabOutputWindow : public Gtk::Window, public Changeable { class IDocumentBoard &core; class Board &brd; class GerberOutputSettings &settings; + class ODBOutputSettings &odb_settings; Gtk::ListBox *gerber_layers_box = nullptr; Gtk::Entry *npth_filename_entry = nullptr; Gtk::Entry *pth_filename_entry = nullptr; @@ -41,18 +42,55 @@ class FabOutputWindow : public Gtk::Window, public Changeable { bool can_export = true; void update_export_button(); + Gtk::Entry *odb_filename_entry = nullptr; + Gtk::Button *odb_filename_button = nullptr; + Gtk::Box *odb_filename_box = nullptr; + Gtk::Label *odb_filename_label = nullptr; + + Gtk::Entry *odb_directory_entry = nullptr; + Gtk::Button *odb_directory_button = nullptr; + Gtk::Box *odb_directory_box = nullptr; + Gtk::Label *odb_directory_label = nullptr; + + Gtk::RadioButton *odb_format_tgz_rb = nullptr; + Gtk::RadioButton *odb_format_directory_rb = nullptr; + Gtk::RadioButton *odb_format_zip_rb = nullptr; + + Gtk::Entry *odb_job_name_entry = nullptr; + + Gtk::Stack *stack = nullptr; + ExportFileChooser export_filechooser; + class ODBExportFileChooserFilename : public ExportFileChooser { + public: + ODBExportFileChooserFilename(const ODBOutputSettings &settings); + + protected: + void prepare_chooser(Glib::RefPtr chooser) override; + void prepare_filename(std::string &filename) override; + + const ODBOutputSettings &settings; + }; + ODBExportFileChooserFilename odb_export_filechooser_filename; + + ExportFileChooser odb_export_filechooser_directory; Gtk::Revealer *done_revealer = nullptr; Gtk::Label *done_label = nullptr; Gtk::Button *done_close_button = nullptr; DoneRevealerController done_revealer_controller; + Gtk::Revealer *odb_done_revealer = nullptr; + Gtk::Label *odb_done_label = nullptr; + Gtk::Button *odb_done_close_button = nullptr; + DoneRevealerController odb_done_revealer_controller; + Glib::RefPtr sg_layer_name; WindowStateStore state_store; void update_drill_visibility(); + void update_odb_visibility(); unsigned int n_layers = 0; }; } // namespace horizon diff --git a/src/python_module/board.cpp b/src/python_module/board.cpp index 25d4e7b5c..f02dabd5d 100644 --- a/src/python_module/board.cpp +++ b/src/python_module/board.cpp @@ -2,6 +2,7 @@ #include "nlohmann/json.hpp" #include "util.hpp" #include "export_gerber/gerber_export.hpp" +#include "export_odb/odb_export.hpp" #include "export_pdf/export_pdf_board.hpp" #include "export_pnp/export_pnp.hpp" #include "export_step/export_step.hpp" @@ -50,6 +51,10 @@ class BoardWrapper : public horizon::DocumentBoard { { return board.gerber_output_settings; } + horizon::ODBOutputSettings &get_odb_output_settings() override + { + return board.odb_output_settings; + } horizon::PDFExportSettings &get_pdf_export_settings() override { return board.pdf_export_settings; @@ -122,6 +127,13 @@ static PyObject *PyBoard_get_gerber_export_settings(PyObject *pself, PyObject *a return py_from_json(settings); } +static PyObject *PyBoard_get_odb_export_settings(PyObject *pself, PyObject *args) +{ + auto self = reinterpret_cast(pself); + auto settings = self->board->board.odb_output_settings.serialize(); + return py_from_json(settings); +} + static PyObject *PyBoard_export_gerber(PyObject *pself, PyObject *args) { auto self = reinterpret_cast(pself); @@ -145,6 +157,28 @@ static PyObject *PyBoard_export_gerber(PyObject *pself, PyObject *args) Py_RETURN_NONE; } +static PyObject *PyBoard_export_odb(PyObject *pself, PyObject *args) +{ + auto self = reinterpret_cast(pself); + PyObject *py_export_settings = nullptr; + if (!PyArg_ParseTuple(args, "O!", &PyDict_Type, &py_export_settings)) + return NULL; + try { + auto settings_json = json_from_py(py_export_settings); + horizon::ODBOutputSettings settings(settings_json); + horizon::export_odb(self->board->board, settings); + } + catch (const std::exception &e) { + PyErr_SetString(PyExc_IOError, e.what()); + return NULL; + } + catch (...) { + PyErr_SetString(PyExc_IOError, "unknown exception"); + return NULL; + } + Py_RETURN_NONE; +} + static PyObject *PyBoard_get_pdf_export_settings(PyObject *pself, PyObject *args) { auto self = reinterpret_cast(pself); @@ -384,7 +418,9 @@ static PyObject *PyBoard_export_3d(PyObject *pself, PyObject *args) static PyMethodDef PyBoard_methods[] = { {"get_gerber_export_settings", PyBoard_get_gerber_export_settings, METH_NOARGS, "Return gerber export settings"}, + {"get_odb_export_settings", PyBoard_get_odb_export_settings, METH_NOARGS, "Return ODB++ export settings"}, {"export_gerber", PyBoard_export_gerber, METH_VARARGS, "Export gerber"}, + {"export_odb", PyBoard_export_odb, METH_VARARGS, "Export ODB++"}, {"get_pdf_export_settings", PyBoard_get_pdf_export_settings, METH_NOARGS, "Return PDF export settings"}, {"get_pnp_export_settings", PyBoard_get_pnp_export_settings, METH_NOARGS, "Return PnP export settings"}, {"get_step_export_settings", PyBoard_get_step_export_settings, METH_NOARGS, "Return STEP export settings"},