Add EdzPart + EdzElementBuilder: .edz part -> .elmt (M1)

Port of the edz2qet.py prototype's mapping to C++. EdzPart parses an EPLAN
part.xml into a portable model (identity/metadata, localized names, connection
list); EdzElementBuilder turns that into a QET element (generic symbol: body
rectangle + one west-facing terminal per pin, per-pin labels, localized <name>s,
and elementInformations for the BOM).

Pins are natural-sorted by designation so they stack 1,2,3,4 regardless of the
order EPLAN lists them (MFH200 lists 1,3,4,2). Output verified structurally
identical (uuids aside) to the Python oracle for three ifm samples — KG6000
(4-pin), MFH200 (4-pin, reordered) and R1D200 (5-pin, incl. Dutch name) — and
the generated element loads in the QET editor with correct UTF-8.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Shane Ringrose
2026-06-20 07:11:09 +12:00
parent 82884d1cf7
commit 7cc5790f7c
5 changed files with 564 additions and 0 deletions
+4
View File
@@ -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
+235
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "edzelementbuilder.h"
#include "edzpart.h"
#include <QUuid>
#include <algorithm>
#include <cmath>
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<EdzPin> 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<int> 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<QString, QString> 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<QPair<QString, QString>> 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);
}
+40
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#ifndef EDZELEMENTBUILDER_H
#define EDZELEMENTBUILDER_H
#include <QDomDocument>
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
+217
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "edzpart.h"
#include <QDomDocument>
#include <QFile>
#include <QFileInfo>
#include <QPair>
#include <QRegularExpression>
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<QPair<QString, QString>> langDict(const QString &value)
{
QList<QPair<QString, QString>> out;
if (value.isEmpty()) {
return out;
}
static const QRegularExpression re(QStringLiteral("[a-z]{2}_[A-Z]{2}@"));
QList<QRegularExpressionMatch> 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<QPair<QString, QString>> 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<QString, QString> multilangNames(const QString &value)
{
QMap<QString, QString> 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<NatTok> natKey(const QString &s)
{
QList<NatTok> 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<NatTok> 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 <part> 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;
}
+68
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#ifndef EDZPART_H
#define EDZPART_H
#include <QString>
#include <QList>
#include <QMap>
/** 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<QString, QString> names() const { return m_names; }
QList<EdzPin> 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<QString, QString> m_names; ///< 2-letter lang -> localized name
QList<EdzPin> m_pins;
};
#endif // EDZPART_H