We have been looking for a documentation solution for a long time that allows us to document the structure of an electrical distribution board quickly, conveniently, and above all, precisely. Many electrical engineers prefer some kind of CAD application for single-line electrical documentation. After trying several manufacturer-specific apps, we also ended up with a CAD solution — QCAD. QCAD is a multi-platform application, it does not force an overpriced subscription model, and its current version is lightning-fast on Apple Silicon.
Among other projects, we are currently working on the distribution board of a family house in Szeged — the documentation of this cabinet has already been created 100% in QCAD:

Within the application, reusable components can be created extremely quickly (these are called blocks in QCAD). We produced all the modules needed for documentation in just a few moments.
I had read earlier that the application has mature scripting support, but only recently did I discover that any drawing entity can have an arbitrary number of custom properties defined. This immediately sparked our imagination, and we quickly built the following solution:
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();
Within the used blocks, we defined a parameter called wire and assigned an integer value to each component, defining how many conductors can be connected to the given hardware element. The script collects all used blocks, counts them, sums up the required connection points, and exports everything into a tab-delimited text file (which both Numbers and Excel natively interpret as a table).
From this point on, we have an exact material requirement per block type, and based on the number of connection points, a fairly accurate estimate of the time required for wiring.
We only need to add how many fine-stranded conductors we use, and we immediately know how many ferrules are required. With explicitly defined connection points, we could even calculate wire lengths — the sky is the limit!
