Régóta keresünk olyan dokumentáló megoldást, amivel gyorsan, kényelmesen és főleg precízen lehet egy elosztó felépítését dokumentálni. Sok villamosmérnök valamilyen CAD alkalmazást preferál a villamos tervek egyvonalas dokumentációihoz. Több gyártóspecifikus app kipróbálása után mi is egy CAD-nél kötöttünk ki - ez a QCAD. A QCAD multiplatform alkalmazás, nem erőlteti mindenáron a méregdrága előfizetési modellt és az aktuális verziója Apple Siliconon villámgyors.
Többek között Szegeden dolgozunk épp egy családi ház elosztóján - ennek a szekrénynek a dokumentációja már 100%-ig QCAD-ben készült:

Az alkalmazásban villámgyorsan lehet újrahasználható komponenseket gyártani (QCAD-ben ezek a blokkok). Pillanatok alatt gyártottuk a dokumentáláshoz épp elégséges modulokat.
Arról már korábban olvastam, hogy az alkalmazásnak kifejlett a scripting támogatása, de azt csak nemrég fedeztem fel, hogy bármilyen rajzelemben lehet tetszőleges számú egyedi propertyt definiálni. Ez aztán beindította a fantáziánkat és gyorsan legyártottuk ezt:
include("scripts/EAction.js");
function showMsg(title, text) {
try {
var mw = RMainWindowQt.getMainWindow();
QMessageBox.information(mw, title, text);
} catch (e) {
print(title + ": " + text);
}
}
function copyToClipboard(text) {
try {
// Try Qt6-style first:
if (typeof QGuiApplication !== "undefined" && QGuiApplication.clipboard) {
QGuiApplication.clipboard().setText(text);
return true;
}
} catch (e1) {}
try {
// Try Qt5-style:
if (typeof QApplication !== "undefined" && QApplication.clipboard) {
QApplication.clipboard().setText(text);
return true;
}
} catch (e2) {}
// No clipboard API available in this environment
return false;
}
function saveTextFile(path, text) {
try {
var f = new QFile(path);
if (!f.open(QIODevice.WriteOnly | QIODevice.Truncate | QIODevice.Text)) {
return { ok: false, error: "Cannot open file for writing: " + path };
}
var ts = new QTextStream(f);
// Do NOT call setCodec / setEncoding here (bindings differ across QCAD builds)
// Just write text. This avoids 0-byte files.
if (typeof ts.writeString === "function") {
ts.writeString(text);
} else {
// Fallback: operator<< is usually exposed as "write"
ts.write(text);
}
ts.flush();
f.flush();
f.close();
// Safety check: prevent silent 0-byte results
var fi = new QFileInfo(path);
if (fi.exists() && fi.size() <= 0) {
return { ok: false, error: "File written but size is 0 bytes: " + path };
}
return { ok: true, error: "" };
} catch (e) {
return { ok: false, error: "File save failed: " + e };
}
}
function getWireValueFromEntity(e) {
// Reads Custom -> QCAD -> wire as integer, default 0
return e.getCustomIntProperty("QCAD", "wire", 0);
}
function sumWireInBlockDefinition(doc, blockId, cache, stack) {
if (cache.hasOwnProperty(blockId)) {
return cache[blockId];
}
// Prevent infinite recursion if blocks reference themselves
if (stack.indexOf(blockId) !== -1) {
return 0;
}
stack.push(blockId);
var sum = 0;
var ids = doc.queryBlockEntities(blockId);
for (var i = 0; i < ids.length; i++) {
var ent = doc.queryEntity(ids[i]);
if (isNull(ent)) continue;
if (isBlockReferenceEntity(ent)) {
var childBlockId = ent.getReferencedBlockId();
sum += sumWireInBlockDefinition(doc, childBlockId, cache, stack);
} else {
sum += getWireValueFromEntity(ent);
}
}
stack.pop();
cache[blockId] = sum;
return sum;
}
function getBlockName(doc, blockId) {
var b = doc.queryBlock(blockId);
if (isNull(b)) return "(unknown)";
return b.getName();
}
function buildStatPath(doc) {
// doc.getFileName() should return full path if drawing is saved
var fn = doc.getFileName();
if (!fn || String(fn).trim() === "") {
// Unsaved drawing: fallback to home directory
var home = QDir.homePath();
return home + QDir.separator + "drawing-stat.tsv";
}
var fi = new QFileInfo(fn);
var dir = fi.absolutePath();
var base = fi.completeBaseName(); // filename without extension
return dir + QDir.separator + base + "-stat.tsv";
}
function main() {
var di = EAction.getDocumentInterface();
if (isNull(di)) {
print("No document interface.");
return;
}
var doc = di.getDocument();
if (isNull(doc)) {
showMsg("Sum wire", "No active document.");
return;
}
var modelSpaceBlockId = doc.getModelSpaceBlockId();
var modelIds = doc.queryBlockEntities(modelSpaceBlockId);
var cache = {}; // blockId -> wire sum in its definition (incl. nested block refs)
var total = 0;
var nonZeroContributors = 0;
// blockName -> { instances, pointsPerInstance, totalPoints }
var byType = {};
for (var i = 0; i < modelIds.length; i++) {
var e = doc.queryEntity(modelIds[i]);
if (isNull(e)) continue;
if (isBlockReferenceEntity(e)) {
var bid = e.getReferencedBlockId();
var pointsPerInstance = sumWireInBlockDefinition(doc, bid, cache, []);
total += pointsPerInstance;
if (pointsPerInstance !== 0) nonZeroContributors++;
var name = getBlockName(doc, bid);
if (!byType.hasOwnProperty(name)) {
byType[name] = {
instances: 0,
pointsPerInstance: pointsPerInstance,
totalPoints: 0
};
}
byType[name].instances++;
byType[name].totalPoints += pointsPerInstance;
} else {
var w = getWireValueFromEntity(e);
total += w;
if (w !== 0) nonZeroContributors++;
var name2 = "(model-entities)";
if (!byType.hasOwnProperty(name2)) {
byType[name2] = {
instances: 0,
pointsPerInstance: 0,
totalPoints: 0
};
}
byType[name2].instances++;
byType[name2].totalPoints += w;
}
}
// Build TAB-delimited report (Excel-friendly)
var lines = [];
lines.push("Block\tInstances\tPointsPerInstance\tTotalPoints");
var keys = Object.keys(byType);
// Alphabetical order by block name (case-insensitive)
keys.sort(function(a, b) {
var aa = a.toLowerCase();
var bb = b.toLowerCase();
if (aa < bb) return -1;
if (aa > bb) return 1;
return 0;
});
for (var k = 0; k < keys.length; k++) {
var bn = keys[k];
var row = byType[bn];
lines.push(
bn + "\t" +
row.instances + "\t" +
row.pointsPerInstance + "\t" +
row.totalPoints
);
}
var summary =
"Wire sum (counting block instances): " + total + "\n" +
"Non-zero contributors (block refs or entities): " + nonZeroContributors + "\n";
// This is what goes to clipboard + TSV file:
var tsv = lines.join("\n");
// Clipboard
var clipOk = copyToClipboard(tsv);
// File save
var statPath = buildStatPath(doc);
var saveRes = saveTextFile(statPath, tsv);
// User-visible message
var msg =
summary + "\n" +
"Clipboard: " + (clipOk ? "OK" : "FAILED") + "\n" +
"Saved TSV: " + (saveRes.ok ? ("OK\n" + statPath) : ("FAILED\n" + saveRes.error));
print(msg);
showMsg("Sum wire", msg);
}
main();
A felhasznált blokkokban definiáltunk egy "wire" nevű paramétert és egy egész számot rendelünk minden komponenshez, ami azt definiálja, hogy hány vezetéket lehet bekötni az adott hardverelembe. A script összeszedi a felhasznált blokkokat, összeszámolja azokat és a hozzájuk szükséges kötéseket, majd az egészet leteszi egy Tab delimited textfileba (amit a Numbers és az Excel is natívan táblázatnak olvas). Innentől megvan a pontos blokktípusonkénti anyagigény, és a kötések száma alapján nagyjából a vezetékezésre fordítandó idő.
Csak hozzárakjuk, hogy hány finomszálú sodrott vezetéket használunk és már megvan mennyi érvéghüvelyre van szükség. Az explicit kötési pontok definiálásával akár vezetékhosszt is számolhatnánk - the sky is the limit!
