diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index 37144c7d3..e6e189350 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -109,6 +109,10 @@ set(QET_RES_FILES set(QET_SRC_FILES ${QET_DIR}/sources/import/edz/edzarchive.cpp ${QET_DIR}/sources/import/edz/edzarchive.h + ${QET_DIR}/sources/import/edz/edzpart.cpp + ${QET_DIR}/sources/import/edz/edzpart.h + ${QET_DIR}/sources/import/edz/edzelementbuilder.cpp + ${QET_DIR}/sources/import/edz/edzelementbuilder.h ${QET_DIR}/sources/borderproperties.cpp ${QET_DIR}/sources/borderproperties.h ${QET_DIR}/sources/bordertitleblock.cpp diff --git a/sources/import/edz/edzelementbuilder.cpp b/sources/import/edz/edzelementbuilder.cpp new file mode 100644 index 000000000..6bccbce32 --- /dev/null +++ b/sources/import/edz/edzelementbuilder.cpp @@ -0,0 +1,235 @@ +/* + Copyright 2006 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#include "edzelementbuilder.h" + +#include "edzpart.h" + +#include +#include +#include + +namespace { + +const QString STYLE = + QStringLiteral("line-style:normal;line-weight:normal;filling:none;color:black"); +const QString FONT = + QStringLiteral("Liberation Sans,5,-1,5,50,0,0,0,0,0,Regular"); +const QString TITLE_FONT = + QStringLiteral("Liberation Sans,5,-1,5,75,0,0,0,0,0,Bold"); + +QString uuidStr() +{ + return QUuid::createUuid().toString(); // "{....}" +} + +QDomElement makeText(QDomDocument &doc, int x, int y, const QString &text, + const QString &font) +{ + QDomElement e = doc.createElement(QStringLiteral("text")); + e.setAttribute(QStringLiteral("x"), x); + e.setAttribute(QStringLiteral("y"), y); + e.setAttribute(QStringLiteral("text"), text); + e.setAttribute(QStringLiteral("rotation"), QStringLiteral("0")); + e.setAttribute(QStringLiteral("font"), font); + e.setAttribute(QStringLiteral("color"), QStringLiteral("#000000")); + return e; +} + +int ceilTo10(int v) +{ + if (v <= 0) { + return 0; + } + return ((v + 9) / 10) * 10; +} + +} // namespace + +QDomDocument EdzElementBuilder::build(const EdzPart &part) +{ + QList pins = part.pins(); + if (pins.isEmpty()) { + pins.append(EdzPin{QStringLiteral("1"), QString()}); + } + const int n = pins.size(); + const int pitch = 10; + const int body_left = 0, body_right = 90, body_top = -20; + const int body_bottom = (n - 1) * pitch + 10; + + // Coordinate accumulators for the bounding box (origin always included). + QList xs{0}, ys{0}; + xs << body_left << body_right; + ys << body_top << body_bottom; + xs << (body_left + 6); // title x + ys << (body_top + 7); // title y + for (int i = 0; i < n; ++i) { + xs << 6 << 0; // pin label x, terminal x + ys << (i * pitch + 2) << (i * pitch); // pin label y, terminal y + } + + const int pad = 10; + const int min_x = *std::min_element(xs.begin(), xs.end()) - pad; + const int max_x = *std::max_element(xs.begin(), xs.end()) + pad; + const int min_y = *std::min_element(ys.begin(), ys.end()) - pad; + const int max_y = *std::max_element(ys.begin(), ys.end()) + pad; + const int width = std::max(10, ceilTo10(max_x - min_x)); + const int height = std::max(10, ceilTo10(max_y - min_y)); + const int hotspot_x = -min_x; + const int hotspot_y = -min_y; + + QDomDocument doc; + doc.appendChild(doc.createProcessingInstruction( + QStringLiteral("xml"), + QStringLiteral("version=\"1.0\" encoding=\"utf-8\""))); + + QDomElement defn = doc.createElement(QStringLiteral("definition")); + defn.setAttribute(QStringLiteral("version"), QStringLiteral("0.100.0")); + defn.setAttribute(QStringLiteral("type"), QStringLiteral("element")); + defn.setAttribute(QStringLiteral("link_type"), QStringLiteral("simple")); + defn.setAttribute(QStringLiteral("width"), width); + defn.setAttribute(QStringLiteral("height"), height); + defn.setAttribute(QStringLiteral("hotspot_x"), hotspot_x); + defn.setAttribute(QStringLiteral("hotspot_y"), hotspot_y); + doc.appendChild(defn); + + QDomElement uuid_el = doc.createElement(QStringLiteral("uuid")); + uuid_el.setAttribute(QStringLiteral("uuid"), uuidStr()); + defn.appendChild(uuid_el); + + // Localized names: fr/en first, then the rest alphabetically. + QDomElement names = doc.createElement(QStringLiteral("names")); + QMap name_map = part.names(); + const QString fallback = !part.orderNumber().isEmpty() ? part.orderNumber() + : (!part.partNumber().isEmpty() ? part.partNumber() + : QStringLiteral("part")); + if (name_map.isEmpty()) { + name_map.insert(QStringLiteral("en"), + !part.description().isEmpty() ? part.description() + : fallback); + } + QStringList ordered; + for (const QString &c : {QStringLiteral("fr"), QStringLiteral("en")}) { + if (name_map.contains(c)) ordered << c; + } + QStringList rest; + for (const QString &c : name_map.keys()) { + if (c != QLatin1String("fr") && c != QLatin1String("en")) rest << c; + } + std::sort(rest.begin(), rest.end()); + ordered << rest; + for (const QString &lang : ordered) { + QDomElement nm = doc.createElement(QStringLiteral("name")); + nm.setAttribute(QStringLiteral("lang"), lang); + nm.appendChild(doc.createTextNode(name_map.value(lang))); + names.appendChild(nm); + } + defn.appendChild(names); + + QDomElement infos = doc.createElement(QStringLiteral("informations")); + infos.appendChild(doc.createTextNode( + QStringLiteral("Imported from EPLAN .edz by edz2qet (%1)") + .arg(part.partNumber()))); + defn.appendChild(infos); + + // Element information -> QET BOM / nomenclature. + QDomElement einfos = doc.createElement(QStringLiteral("elementInformations")); + const QList> info_map{ + {QStringLiteral("manufacturer"), part.manufacturer()}, + {QStringLiteral("manufacturer_reference"), part.orderNumber()}, + {QStringLiteral("designation"), part.description()}, + {QStringLiteral("comment"), part.comment()}, + }; + for (const auto &kv : info_map) { + if (kv.second.isEmpty()) { + continue; + } + QDomElement e = doc.createElement(QStringLiteral("elementInformation")); + e.setAttribute(QStringLiteral("name"), kv.first); + e.setAttribute(QStringLiteral("show"), QStringLiteral("1")); + e.appendChild(doc.createTextNode(kv.second)); + einfos.appendChild(e); + } + defn.appendChild(einfos); + + QDomElement desc = doc.createElement(QStringLiteral("description")); + + // Body rectangle. + QDomElement rect = doc.createElement(QStringLiteral("rect")); + rect.setAttribute(QStringLiteral("x"), body_left); + rect.setAttribute(QStringLiteral("y"), body_top); + rect.setAttribute(QStringLiteral("width"), body_right - body_left); + rect.setAttribute(QStringLiteral("height"), body_bottom - body_top); + rect.setAttribute(QStringLiteral("style"), STYLE); + rect.setAttribute(QStringLiteral("antialias"), QStringLiteral("false")); + desc.appendChild(rect); + + // Title (order number) in its own band at the top. + const QString title = !part.orderNumber().isEmpty() ? part.orderNumber() + : (!part.partNumber().isEmpty() ? part.partNumber() + : QStringLiteral("PART")); + desc.appendChild(makeText(doc, body_left + 6, body_top + 7, title, TITLE_FONT)); + + // Per-pin labels. + for (int i = 0; i < n; ++i) { + QString label = pins.at(i).designation; + if (!pins.at(i).description.isEmpty()) { + label += QStringLiteral(" ") + pins.at(i).description; + } + desc.appendChild(makeText(doc, 6, i * pitch + 2, label, FONT)); + } + + // Device-tag label (per-instance, above the body). + QDomElement dyn = doc.createElement(QStringLiteral("dynamic_text")); + dyn.setAttribute(QStringLiteral("x"), min_x + pad); + dyn.setAttribute(QStringLiteral("y"), min_y - 2); + dyn.setAttribute(QStringLiteral("z"), QStringLiteral("5")); + dyn.setAttribute(QStringLiteral("text_width"), QStringLiteral("-1")); + dyn.setAttribute(QStringLiteral("Halignment"), QStringLiteral("AlignLeft")); + dyn.setAttribute(QStringLiteral("Valignment"), QStringLiteral("AlignTop")); + dyn.setAttribute(QStringLiteral("frame"), QStringLiteral("false")); + dyn.setAttribute(QStringLiteral("rotation"), QStringLiteral("0")); + dyn.setAttribute(QStringLiteral("keep_visual_rotation"), QStringLiteral("false")); + dyn.setAttribute(QStringLiteral("text_from"), QStringLiteral("ElementInfo")); + dyn.setAttribute(QStringLiteral("uuid"), uuidStr()); + dyn.setAttribute(QStringLiteral("font"), FONT); + dyn.appendChild(doc.createElement(QStringLiteral("text"))); + QDomElement info_name = doc.createElement(QStringLiteral("info_name")); + info_name.appendChild(doc.createTextNode(QStringLiteral("label"))); + dyn.appendChild(info_name); + desc.appendChild(dyn); + + // Terminals. + for (int i = 0; i < n; ++i) { + QDomElement t = doc.createElement(QStringLiteral("terminal")); + t.setAttribute(QStringLiteral("uuid"), uuidStr()); + t.setAttribute(QStringLiteral("name"), pins.at(i).designation); + t.setAttribute(QStringLiteral("x"), 0); + t.setAttribute(QStringLiteral("y"), i * pitch); + t.setAttribute(QStringLiteral("orientation"), QStringLiteral("w")); + t.setAttribute(QStringLiteral("type"), QStringLiteral("Generic")); + desc.appendChild(t); + } + + defn.appendChild(desc); + return doc; +} + +QString EdzElementBuilder::toElmtString(const QDomDocument &doc) +{ + return doc.toString(0); +} diff --git a/sources/import/edz/edzelementbuilder.h b/sources/import/edz/edzelementbuilder.h new file mode 100644 index 000000000..8af8350a8 --- /dev/null +++ b/sources/import/edz/edzelementbuilder.h @@ -0,0 +1,40 @@ +/* + Copyright 2006 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#ifndef EDZELEMENTBUILDER_H +#define EDZELEMENTBUILDER_H + +#include + +class EdzPart; + +/** + @brief Builds a QElectroTech element (.elmt) from an EdzPart. + + Generates a generic symbol: a body rectangle with one west-facing terminal + per pin (stacked on the left edge, on the 10px wiring grid), per-pin labels, + localized names and the element-information fields QET uses for the BOM. + Faithful port of the edz2qet.py prototype's builder. +*/ +class EdzElementBuilder +{ + public: + static QDomDocument build(const EdzPart &part); + static QString toElmtString(const QDomDocument &doc); +}; + +#endif // EDZELEMENTBUILDER_H diff --git a/sources/import/edz/edzpart.cpp b/sources/import/edz/edzpart.cpp new file mode 100644 index 000000000..5a2364698 --- /dev/null +++ b/sources/import/edz/edzpart.cpp @@ -0,0 +1,217 @@ +/* + Copyright 2006 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#include "edzpart.h" + +#include +#include +#include +#include +#include + +namespace { + +/** + Parse EPLAN's `de_DE@..;en_EN@..;..` multilingual blob into ordered + (full_code, text) pairs. Values may contain `;;` as an in-text separator, so + we split on language *markers* (xx_YY@) rather than on bare `;`. +*/ +QList> langDict(const QString &value) +{ + QList> out; + if (value.isEmpty()) { + return out; + } + static const QRegularExpression re(QStringLiteral("[a-z]{2}_[A-Z]{2}@")); + QList marks; + auto it = re.globalMatch(value); + while (it.hasNext()) { + marks.append(it.next()); + } + for (int i = 0; i < marks.size(); ++i) { + const QRegularExpressionMatch &m = marks.at(i); + const QString code = value.mid(m.capturedStart(), + m.capturedLength() - 1); // drop '@' + const int text_start = m.capturedEnd(); + const int text_end = (i + 1 < marks.size()) + ? marks.at(i + 1).capturedStart() + : value.size(); + QString text = value.mid(text_start, text_end - text_start); + while (!text.isEmpty() && text.back().isSpace()) { + text.chop(1); + } + while (!text.isEmpty() && text.back() == QLatin1Char(';')) { + text.chop(1); + } + text.replace(QStringLiteral(";;"), QStringLiteral("; ")); + text = text.trimmed(); + if (!text.isEmpty()) { + out.append({code, text}); + } + } + return out; +} + +/** Pick one language: preferred English variants, then any English, then first. */ +QString pickLang(const QString &value) +{ + if (value.isEmpty()) { + return QString(); + } + const QList> d = langDict(value); + if (d.isEmpty()) { + return value.trimmed(); + } + for (const QString &code : {QStringLiteral("en_EN"), QStringLiteral("en_US"), + QStringLiteral("en_GB")}) { + for (const auto &p : d) { + if (p.first == code && !p.second.isEmpty()) { + return p.second; + } + } + } + for (const auto &p : d) { + if (p.first.startsWith(QStringLiteral("en_")) && !p.second.isEmpty()) { + return p.second; + } + } + return d.first().second; +} + +/** {2-letter QET lang -> text}; first variant of each language wins. */ +QMap multilangNames(const QString &value) +{ + QMap names; + for (const auto &p : langDict(value)) { + const QString s = p.first.left(2).toLower(); + if (!names.contains(s)) { + names.insert(s, p.second); + } + } + return names; +} + +/** Tokenise a designation into (is-text, number, text) runs for natural sort. */ +struct NatTok { int type; qlonglong num; QString str; }; + +QList natKey(const QString &s) +{ + QList out; + static const QRegularExpression re(QStringLiteral("\\d+|\\D+")); + auto it = re.globalMatch(s); + while (it.hasNext()) { + const QString t = it.next().captured(); + bool all_digit = !t.isEmpty(); + for (const QChar c : t) { + if (!c.isDigit()) { all_digit = false; break; } + } + if (all_digit) { + out.append({0, t.toLongLong(), QString()}); + } else { + out.append({1, 0, t.toLower()}); + } + } + return out; +} + +bool natLess(const QString &a, const QString &b) +{ + const QList ka = natKey(a), kb = natKey(b); + const int n = qMin(ka.size(), kb.size()); + for (int i = 0; i < n; ++i) { + if (ka[i].type != kb[i].type) { + return ka[i].type < kb[i].type; + } + if (ka[i].type == 0) { + if (ka[i].num != kb[i].num) return ka[i].num < kb[i].num; + } else { + if (ka[i].str != kb[i].str) return ka[i].str < kb[i].str; + } + } + return ka.size() < kb.size(); +} + +} // namespace + +/** Parse the part.xml at @a part_xml_path into this object. */ +bool EdzPart::parse(const QString &part_xml_path) +{ + m_error.clear(); + + QFile file(part_xml_path); + if (!file.open(QIODevice::ReadOnly)) { + m_error = QStringLiteral("Cannot open %1").arg(part_xml_path); + return false; + } + QDomDocument doc; + QString parse_err; + int line = 0, col = 0; + if (!doc.setContent(&file, &parse_err, &line, &col)) { + m_error = QStringLiteral("Malformed part.xml (line %1): %2") + .arg(line).arg(parse_err); + return false; + } + file.close(); + + const QDomElement part = + doc.documentElement().firstChildElement(QStringLiteral("part")); + if (part.isNull()) { + m_error = QStringLiteral("No element in part.xml"); + return false; + } + + m_part_number = part.attribute(QStringLiteral("P_ARTICLE_PARTNR")); + m_manufacturer = part.attribute(QStringLiteral("P_ARTICLE_MANUFACTURER")); + m_order_number = part.attribute(QStringLiteral("P_ARTICLE_ORDERNR")); + m_type_number = part.attribute(QStringLiteral("P_ARTICLE_TYPENR")); + + const QString descr1 = part.attribute(QStringLiteral("P_ARTICLE_DESCR1")); + const QString descr2 = part.attribute(QStringLiteral("P_ARTICLE_DESCR2")); + m_description = pickLang(descr1); + m_comment = pickLang(descr2); + m_names = multilangNames(descr1); + + const QString pic = part.attribute(QStringLiteral("P_ARTICLE_PICTUREFILE")); + if (!pic.isEmpty()) { + m_picture = QFileInfo(QString(pic).replace('\\', '/')).fileName(); + } + + // Pins = function templates that carry a physical connection designation. + const QDomNodeList fts = + part.elementsByTagName(QStringLiteral("functiontemplate")); + for (int i = 0; i < fts.size(); ++i) { + const QDomElement ft = fts.at(i).toElement(); + const QString desig = + ft.attribute(QStringLiteral("connectionDesignation")).trimmed(); + if (desig.isEmpty()) { + continue; + } + EdzPin pin; + pin.designation = desig; + pin.description = + ft.attribute(QStringLiteral("connectiondescription")).trimmed(); + m_pins.append(pin); + } + + // EPLAN lists pins in its own order (e.g. MFH200: 1,3,4,2); order them by + // pin number so they stack 1,2,3,4 on the symbol. + std::stable_sort(m_pins.begin(), m_pins.end(), + [](const EdzPin &a, const EdzPin &b) { + return natLess(a.designation, b.designation); + }); + return true; +} diff --git a/sources/import/edz/edzpart.h b/sources/import/edz/edzpart.h new file mode 100644 index 000000000..45d31620a --- /dev/null +++ b/sources/import/edz/edzpart.h @@ -0,0 +1,68 @@ +/* + Copyright 2006 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#ifndef EDZPART_H +#define EDZPART_H + +#include +#include +#include + +/** One physical connection of an EPLAN part (a pin/terminal). */ +struct EdzPin { + QString designation; ///< terminal id, e.g. "1", "PE" + QString description; ///< function label, e.g. "L+" +}; + +/** + @brief The portable data of an EPLAN part, read from its part.xml. + + Mirrors the fields the standalone edz2qet.py prototype maps: identity and + metadata, localized names (2-letter language -> text) and the connection + list. Geometry from the EPLAN macro is intentionally ignored — the importer + generates a generic symbol from the pin list instead. +*/ +class EdzPart +{ + public: + bool parse(const QString &part_xml_path); + QString errorString() const { return m_error; } + + QString partNumber() const { return m_part_number; } + QString manufacturer() const { return m_manufacturer; } + QString orderNumber() const { return m_order_number; } + QString typeNumber() const { return m_type_number; } + QString description() const { return m_description; } + QString comment() const { return m_comment; } + QString picture() const { return m_picture; } + QMap names() const { return m_names; } + QList pins() const { return m_pins; } + + private: + QString m_error; + QString m_part_number; + QString m_manufacturer; + QString m_order_number; + QString m_type_number; + QString m_description; ///< DESCR1, preferred English (for element-info) + QString m_comment; ///< DESCR2, preferred English + QString m_picture; ///< picture file name, if any + QMap m_names; ///< 2-letter lang -> localized name + QList m_pins; +}; + +#endif // EDZPART_H