Hero Image

QCAD - egy új munkaeszköz

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:

cabinet

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!

sky-is-the-limit