qcad_script_intro.png

Вы знаете многие вещи проходят незаметно, особенно если давно чем-то не пользоваться, а я уже не брал в руки CAD года два. Итак о незаметных вещах, как Вы знаете существуют не так много свободных 2D САПР. Ещё меньше существует таких в которых можно попытаться сдать чертеж по ГОСТ. История QCAD CE и его форка LibreCAD наталкивает на грустные мысли о крупных C++ проектах, энтузиазме и реальной жизни кода(с другой стороны Open CASCADE заметно перепилили, после того как его открыла Salome).

Но да не суть в QCAD CE есть скрипты и если раньше это было слабо заметно то теперь примеров в проекте достаточно, а не только вкладка «Нарисовать линию». Слабая распространенность скриптов видимо связана с тем что JS(на самом деле ECMA) не настолько известен инженерным работникам, нежели Python. Ниже переведено руководство. Ну также комментарии и дополнения.

Подготовка

Сборка QCAD CE

Соберем версию посвежее, так как там будет работать запуск скриптов из библиотеки.

Вот что мне ещё понадобилось, к сожалению процесс итеративный с последующим запуском apt-file и выяснением какого пакета не хватило qmake, на чистую систему я не ставил а qt библиотеки у меня как минимум поставлены FreeCAD-ом. Но кое что доставить пришлось:

qt5-image-formats-plugins qtscript5-dev qttools5-dev libqt5xmlpatterns5-dev

Запускаем скрипты

В открывшемся нам окне qcad идем «Прочее» -> «Разработка» -> «Оболочка скрипта» ну или используем горячие клавиши GE они теперь привязаны почти ко всему(и это тоже очень круто).

В открывшейся оболочке с приглашением вида ecma> вбиваем. Да Ctrl-V в окне скриптов не работает, так что ручками правой клавишей мыши и Paste, не знаю почему так сделано и почему прыжок в консоль нельзя осуществить как Ctl+Space.

for (i=0; i<=10; i++) {
    addLine(i*10,0, i*10,100);
}

И получаем, 11 вертикальных полос.

Доступное api https://www.qcad.org/doc/qcad/latest/developer/group__ecma__simple.html

Рисуем нечто

qcad_script_sinus.png

Попробуем нарисовать синусоиду, как по мне для отрисовки графиков для чертежа хватало gnuplot экспорта в dxf. Но вдруг Вам зачем-то понадобится. Покопавшись в qcad/scripts/Misc/Examples нашли обращения к модулю Math.

Играемся с консолью:

ecma> Math.abs(-5)
5
ecma> aDelta = 2 * Math.PI / 100;
0.06283185307179587
ecma> aDelta
0.06283185307179587

Синус из 20 отрезков:

for (i=0; i<20; i++) {
    addLine(i*10,200*Math.sin(2 * Math.PI * i / 20) , (i+1)*10,200*Math.sin(2 * Math.PI * (i+1) / 20));
}

Library Browser Scripts

Официальный перевод ‘Обзор библиотек’, так что буду придерживаться его.

Script Part как только не называются я бы их назвал Параметрическими или Параметризованным Объектами,но в тексте их перевод скачет.

Новый Обзорщик Библиотек QCAD(Part Library Browser) может содержать не только статичные детали, но также динамически генерируемые детали, который создаются основываясь на параметрах, формулах, данных введенных пользователем, SQL базах данных, XML и других источников данных. Динамические объекты будут отрисованы, перед тем как вы будете их вставлять.

Скриптованные объекты могут показывать пользователю интерфейс куда он будет вводить параметры для скрипта. Например, объект являющийся видом сверху обеденного стола, позволяет пользователю вводить длину и ширину этого стола.

Библиотека деталей поставляемых с QCAD содержит простой скрипт «DiningTable.js» расположенный в «library/misc».

Примечание скрип /libraries/default/Examples/DiningTable.js

Создание Параметризованной Библиотечной Детали

В этом руководстве мы создадим новый параметризованный объект для нашего Обзорщика Библиотек. Объект должен генерировать, выкройку для куба как ниже:

cube_template.png

Параметризированный объект обладает двумя параметрах, устанавливаемых пользователем, перед тем как скрипт будет отрисован на кульмане(рабочей области?):

  • Длинна грани куба (в единицах установленных в программе мм или дюймы).
  • Нужно или нет генерировать клеевые вкладки (true или false).

Интересно наверное будет получить доступ к системе установленной в настройках дюйм или мм

Сперва создадим файл со сценарием, который и будет работать при вызове нового библиотечного объекта:

  1. Создадим папку для наших скриптов. Папка может быть создана внутри библиотек с деталями поставляемых с QCAD или где угодно на вашем компьютере.
  2. Запустите свой любимый текстовый редактор (emacs) и создайте там файл.
  3. Сохраните файл как «CubeCuttingOut.js» в созданной ранее папке. Обратите внимание что разрешение должно быть «.js» потому как мы пишем ECMAScript (JavaScript).
  4. Если ваша папка расположена в библиотечной папке с QCAD, вы можете пропустить следующие шаги.
  5. Добавляем недавно созданную папку в источники Обзорщика библиотек:
    • Открываем Обзорщик Библиотек QCAD.
    • Правка -> Настройки Программы -> Виджеты/Обзор библиотек.
    • В разделе Источники библиотек жмем Добавить.. и выбираем созданную в первом пункте папку.
    • Перезапускаем Обзорщик Библиотек чтобы изменения возымели эффект.
    • Теперь наш обзорщик обладает дополнительным источником объектов.

Там вообще здорово, Объекты можно искать и добавлять в избранное

Как устроен скрипт

Скопируйте следующий код в ваш файл ‘CubeCuttingOut.js’:

function CubeCuttingOut() {
}
CubeCuttingOut.init = function(formWidget) {
};
CubeCuttingOut.generate = function(documentInterface, file) {
};
CubeCuttingOut.generatePreview = function(documentInterface, iconSize) {
};

Итак код выше добавляет класс CubeCuttingOut в наш файл. Запомните этот класс должен иметь тоже имя что и файл со сценарием (с учетом регистра).

По мимо конструктора, необходимо три функции чтобы сделать работающий скриптовый объект:

  • init(formWidget)
    • Функция init() вызывается когда иконка предпросмотра (смотри ниже) уже сгенерирована, или когда скриптовый объект уже вставляется на чертеж.
    • Параметр formWidget, это виджет который отображает параметры скрипта (если это необходимо).
  • generate(documentInterface, file)
    • Функция generate() вызывается когда пользователь собирается вставить(на холст) сценарный объект.
    • Параметр documentInterface это допустимый интерфейс(способ общения с) к документу (RDocumentInterface) он используется для создания библиотечного элемента. Это не редактируемый пользователем документ. Сценарий из библиотеки деталей не имеет прямого доступа к холсту/кульману пользователя(т.е. это последовательность действий которая выполнится).
    • Параметр file название текущего скриптового файла (String). Ожидается что эта функция вернет объект типа RAddObjectsOperation. Вообще ни о чем не говорит
  • generatePreview(documentInterface, iconSize)
    • Функция generatePreview() вызывается чтобы создать иконку для Обзорщика Библиотекa. Запомните, на тот момент когда иконка превью создается,пользовательский ввод недоступен. Обычно иконка генерируется с параметрами по умолчанию.
    • Параметр documentInterface допустимый интерфейс к документу (RDocumentInterface).
    • Параметр iconSize настраиваемый пользователем размер иконки (integer).
    • Ожидается что эта функция вернет объект типа RAddObjectsOperation.

Сценарный объект теперь соответствует всем требованиям. Найдите вашу библиотечную папку в вкладке ‘File System’ QCAD Обзорщика Библиотек. ПКМ на папке выберите ‘Регенерировать значки’. Ваш объект появиться как пустая иконка украшенная маленькой шестерней, что означает, что это параметризованный библиотечный элемент из файла сценария

Реализация

В большинстве случаев generate() и generatePreview() делают одно и то же. Главное отличие generate() и generatePreview() в том что generate() работает с данными введенными пользователем, в то время как generatePreview() не получает никаких данных от пользователя и будет генерировать объект на основании параметров по умолчанию, для создания узнаваемой иконки.

Обычно рекомендуется написать вспомогательную функцию которая создает RAddObjectsOperation объект, который будут возвращать обе функции на основе параметров скрипта.

Например, мы назовем такую функцию createCuttingOut():

CubeCuttingOut.createCuttingOut = function(documentInterface) {
    CubeCuttingOut.size = 10;
    var va = new Array(
            new RVector(0, 0),
            new RVector(0, CubeCuttingOut.size),
            new RVector(CubeCuttingOut.size, CubeCuttingOut.size),
            new RVector(CubeCuttingOut.size, 0)
    );
    var addOperation = new RAddObjectsOperation(false);
    for ( var i = 0; i < va.length; ++i) {
        var lineData = new RLineData(va[i], va[(i + 1) % va.length]);
        var line = new RLineEntity(documentInterface.getDocument(), lineData);
        addOperation.addObject(line);
    }
    return addOperation;
};

На данный момент(в данной версии этого скрипта), размер куба зафиксирован в 10 единицах (переменная CubeCuttingOut.size). Позже мы добавим CubeCuttingOut.size как входной параметр в нашу вспомогательную функцию.

Основываясь на cube size, вспомогательная функция создает CAD объекты представляющие квадрат. Эти объекты будут добавлены к НаборуОпераций, которые применяться к документу при вставке.

Создается набор точек RVector потом RAddObjectsOperation -это судя по названию класс который содержит внутри себя набор операций, которые потом применяются к объекту. Т.е. мы не создаем объект а лишь храним операции необходимые для его создания

В функции generate(), мы просто вызываем вспомогательную функцию:

CubeCuttingOut.generate = function(documentInterface, file) {
    return CubeCuttingOut.createCuttingOut(documentInterface);
};

Сценарий теперь рабочий, но он не отображается на иконке в Обзоршике, и создает всего один квадрат фиксированного размера.

При перемещении курсора внутри области рисования, будет отрисован квадрат со стороной 10. ЛКМ разместит квадрат где-нибудь на холсте(кульмане).

Запомните вы также можете указать масштаб и угол поворота, или зеркальное отображение объекта на панели инструментов.Это стандартные операции доступные всем объектам которые могут быть вставлены, в том числе и сценарным.

qcad_script_rotate_insert.png

Сохраните измененный файл сценария, а затем перетащите мышкой скриптовый объект, из обзорщика на чертеж.

lb01.png

Теперь мы модифицируем скрипт для создания полного шаблона выкройки куба:

CubeCuttingOut.init = function(formWidget) {
    if (!isNull(formWidget)) {
        CubeCuttingOut.widgets = getWidgets(formWidget);
    }
};
// ....
CubeCuttingOut.createCuttingOut = function(documentInterface) {
    var addOperation = new RAddObjectsOperation(false);

    // create squares
    for ( var i = 0; i < 4; ++i) {
        var pos = new RVector(i * CubeCuttingOut.size, 0);
        CubeCuttingOut.createSquare(documentInterface, addOperation, pos);
    }
    var posTop = new RVector(CubeCuttingOut.size * 2, CubeCuttingOut.size);
    CubeCuttingOut.createSquare(documentInterface, addOperation, posTop);
    var posBottom = new RVector(CubeCuttingOut.size * 2, -CubeCuttingOut.size);
    CubeCuttingOut.createSquare(documentInterface, addOperation, posBottom);

    // create plates
    if (CubeCuttingOut.drawPlates) {
        var plates = new Array(
                [ new RVector(4 * CubeCuttingOut.size, 0), 0 ],
                [ new RVector(2 * CubeCuttingOut.size, -3 * CubeCuttingOut.size), 90 ],
                [ new RVector(1 * CubeCuttingOut.size, -4 * CubeCuttingOut.size), 90 ],
                [ new RVector(-2 * CubeCuttingOut.size, -2 * CubeCuttingOut.size), 180 ],
                [ new RVector(-2 * CubeCuttingOut.size, 0), 180 ],
                [ new RVector(1 * CubeCuttingOut.size, 2 * CubeCuttingOut.size), 270 ],
                [ new RVector(0, 3 * CubeCuttingOut.size), 270 ]
        );
        for ( var i = 0; i < plates.length; ++i) {
            var pos = plates[i][0];
            var angle = RMath.deg2rad(plates[i][1]);
            CubeCuttingOut.createPlate(documentInterface, addOperation, pos, angle);
        }
    }

    return addOperation;
};

CubeCuttingOut.createSquare = function(documentInterface, addOperation, pos) {
    var va = new Array(
            new RVector(0, 0),
            new RVector(0, CubeCuttingOut.size),
            new RVector(CubeCuttingOut.size, CubeCuttingOut.size),
            new RVector(CubeCuttingOut.size, 0)
    );
    for ( var i = 0; i < va.length; ++i) {
        var v1 = va[i].operator_add(pos);
        var v2 = va[(i + 1) % va.length].operator_add(pos);
        var lineData = new RLineData(v1, v2);
        var line = new RLineEntity(documentInterface.getDocument(), lineData);
        addOperation.addObject(line);
    }
};

Так ну тут есть функция создания квадратов и вкладок(но не показана), сначала рисуем 4 квадрата в линию, потом крестом. Затем создается массив данных о Вкладках, Начальная точка и угол поворота.

Передача Параметров Скрипту

Для того чтобы пользователь мог вводить параметры скрипта, мы должны задать для этого интерфейс(виджет). Для того чтобы создать макет интерфейса мы используем Qt Designer. Qt Designer свободен как часть Qt SDK или Qt Creator и доступен: http://qt.nokia.com/downloads

Файл интерфейса пользователя (UI) должен обладать тем же именем что и скрипт, но иметь расширение .ui.

  • Запустите Qt Designer.
  • Создайте новый файл(автоматически делается при входе) и выберете шаблон Виджет.
  • Добавьте элемент QLineEdit, установите имя этого объекта «CubeSize» и его значение в «10».
  • Добавьте элемент QCheckBox, установите имя объекта как «DrawPlates» и поставьте флаг в свойстве checked.

Также вы можете добавить Labels(Метки) для того чтобы пользователь понимал какой параметр он здесь вводит. В конце, ваш виджет должен выглядеть примерно так:

widget.png

Сохраните UI файл как «CubeCuttingOut.ui». Если у вас нет Qt Designer, вы сможете найти исходный код этого файла далее по тексту.

Теперь обзорщик библиотек может отобразить пользовательский интерфейс компонента, когда вы начнете вставлять этот элемент.

Все скриптовые представления объекта должны делать следующее чтобы получать параметры из своих интерфейсных компонентов:

include("scripts/library.js");
// ....
CubeCuttingOut.init = function(formWidget) {
    if (!isNull(formWidget)) {
        CubeCuttingOut.widgets = getWidgets(formWidget);
    }
};
//...

Сохраните файл сценария опять и вставьте ваш параметризированный объект на чертеж. Выкройка по-прежнему отрисовывается с размером стороны 10. Как только вы введете другое значение в пользовательский интерфейс компонента, выкройка отрисуется с этим размером.

Параметр отвечающий за отрисовку Вкладок(DrawPlates), может быть задан тем же путем:

CubeCuttingOut.generate = function(documentInterface, file) {
    CubeCuttingOut.size = parseInt(CubeCuttingOut.widgets["CubeSize"].text, 10);
    if (isNaN(CubeCuttingOut.size)) {
        // can't parse value as integer, set default size
        CubeCuttingOut.size = 10;
    }

    if (CubeCuttingOut.widgets["DrawPlates"].checked) {
        CubeCuttingOut.drawPlates = true;
    } else {
        CubeCuttingOut.drawPlates = false;
    }

    return CubeCuttingOut.createCuttingOut(documentInterface);
};

Создание Превью для сценария

Наконец, мы создадим превью сценария, т.е. то что будет отображаться на иконке в обзорщике библиотек. Для этого, мы просто, установим несколько необходимых параметров скрипта по умолчанию и вызовем нашу вспомогательную функцию.

CubeCuttingOut.generatePreview = function(documentInterface, iconSize) {
    CubeCuttingOut.size = iconSize / 6;
    CubeCuttingOut.drawPlates = true;
    return CubeCuttingOut.createCuttingOut(documentInterface);
};

Иконки в обзорщике библиотек обновляются при каждом запуске QCAD. Вы также можете кликнуть ПКМ на папке в вкладке ‘Файловая система’ и выбрать ‘Регенерировать значки’ для того чтобы обновить иконки в этой папке.

lb02.png

Полный файл сценария и интерфейса пользователя

/**
 * Copyright (c) 2011-2018 by Andrew Mustun. All rights reserved.
 * 
 * This file is part of the QCAD project.
 *
 * QCAD 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 3 of the License, or
 * (at your option) any later version.
 *
 * QCAD 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 QCAD.
 */
// CubeCuttingOut.js
//! [include]
// library.js contains some convenience functions like 'isNull':
include("scripts/library.js");
//! [include]
function CubeCuttingOut() {
    CubeCuttingOut.size = 10;
    CubeCuttingOut.drawPlates = true;
}
//! [init]
CubeCuttingOut.init = function(formWidget) {
    if (!isNull(formWidget)) {
        CubeCuttingOut.widgets = getWidgets(formWidget);
    }
};
//! [init]
//! [generate]
CubeCuttingOut.generate = function(documentInterface, file) {
    CubeCuttingOut.size = parseInt(CubeCuttingOut.widgets["CubeSize"].text, 10);
    if (isNaN(CubeCuttingOut.size)) {
        // can't parse value as integer, set default size
        CubeCuttingOut.size = 10;
    }

    if (CubeCuttingOut.widgets["DrawPlates"].checked) {
        CubeCuttingOut.drawPlates = true;
    } else {
        CubeCuttingOut.drawPlates = false;
    }

    return CubeCuttingOut.createCuttingOut(documentInterface);
};
//! [generate]
//! [generatePreview]
CubeCuttingOut.generatePreview = function(documentInterface, iconSize) {
    CubeCuttingOut.size = iconSize / 6;
    CubeCuttingOut.drawPlates = true;
    return CubeCuttingOut.createCuttingOut(documentInterface);
};
//! [generatePreview]
//! [createCuttingOut]
CubeCuttingOut.createCuttingOut = function(documentInterface) {
    var addOperation = new RAddObjectsOperation(false);

    // create squares
    for ( var i = 0; i < 4; ++i) {
        var pos = new RVector(i * CubeCuttingOut.size, 0);
        CubeCuttingOut.createSquare(documentInterface, addOperation, pos);
    }
    var posTop = new RVector(CubeCuttingOut.size * 2, CubeCuttingOut.size);
    CubeCuttingOut.createSquare(documentInterface, addOperation, posTop);
    var posBottom = new RVector(CubeCuttingOut.size * 2, -CubeCuttingOut.size);
    CubeCuttingOut.createSquare(documentInterface, addOperation, posBottom);

    // create plates
    if (CubeCuttingOut.drawPlates) {
        var plates = new Array(
                [ new RVector(4 * CubeCuttingOut.size, 0), 0 ],
                [ new RVector(2 * CubeCuttingOut.size, -3 * CubeCuttingOut.size), 90 ],
                [ new RVector(1 * CubeCuttingOut.size, -4 * CubeCuttingOut.size), 90 ],
                [ new RVector(-2 * CubeCuttingOut.size, -2 * CubeCuttingOut.size), 180 ],
                [ new RVector(-2 * CubeCuttingOut.size, 0), 180 ],
                [ new RVector(1 * CubeCuttingOut.size, 2 * CubeCuttingOut.size), 270 ],
                [ new RVector(0, 3 * CubeCuttingOut.size), 270 ]
        );
        for ( var i = 0; i < plates.length; ++i) {
            var pos = plates[i][0];
            var angle = RMath.deg2rad(plates[i][1]);
            CubeCuttingOut.createPlate(documentInterface, addOperation, pos, angle);
        }
    }

    return addOperation;
};
//! [createCuttingOut]
//! [createSquare]
CubeCuttingOut.createSquare = function(documentInterface, addOperation, pos) {
    var va = new Array(
            new RVector(0, 0),
            new RVector(0, CubeCuttingOut.size),
            new RVector(CubeCuttingOut.size, CubeCuttingOut.size),
            new RVector(CubeCuttingOut.size, 0)
    );
    for ( var i = 0; i < va.length; ++i) {
        var v1 = va[i].operator_add(pos);
        var v2 = va[(i + 1) % va.length].operator_add(pos);
        var lineData = new RLineData(v1, v2);
        var line = new RLineEntity(documentInterface.getDocument(), lineData);
        addOperation.addObject(line);
    }
};
//! [createSquare]
CubeCuttingOut.createPlate = function(documentInterface, addOperation, pos, angle) {
    var plateSize = CubeCuttingOut.size / 7;
    var off = plateSize * Math.sqrt(2);
    var va = new Array(
            new RVector(0, 0),
            new RVector(off, off),
            new RVector(off, CubeCuttingOut.size - off),
            new RVector(0, CubeCuttingOut.size)
    );
    for ( var i = 0; i < va.length; ++i) {
        var v1 = va[i].operator_add(pos);
        v1 = v1.rotate(angle);
        var v2 = va[(i + 1) % va.length].operator_add(pos);
        v2 = v2.rotate(angle);
        var lineData = new RLineData(v1, v2);
        var line = new RLineEntity(documentInterface.getDocument(), lineData);
        addOperation.addObject(line);
    }
};

Файл интерфейса пользователя.

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>Form</class>
 <widget class="QWidget" name="Form">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>239</width>
    <height>76</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Form</string>
  </property>
  <layout class="QFormLayout" name="formLayout">
   <item row="0" column="0">
    <widget class="QLabel" name="label">
     <property name="text">
      <string>Cube size:</string>
     </property>
    </widget>
   </item>
   <item row="0" column="1">
    <widget class="QLineEdit" name="CubeSize">
     <property name="text">
      <string>10</string>
     </property>
    </widget>
   </item>
   <item row="1" column="1">
    <widget class="QCheckBox" name="DrawPlates">
     <property name="text">
      <string>Draw plates</string>
     </property>
    </widget>
   </item>
  </layout>
 </widget>
 <resources/>
 <connections/>
</ui>

Итоги и вопросы?

Ну с одной стороны у нас есть ура, ура CADD с полноценными скриптами. Про DWG разговор отдельный. С другой стороны у нас грубо говоря два инструмента командной строки, один встроенные и Вы переходите в него по пробелу и используете его синтаксис вводя что нибудь вроде line, text, или выполняя действия и их названия и есть ECMA консоль в которой есть R* объекты, все таки в FreeCAD это реализовано забавнее, там консоль показывает что выполняет ядро и можно эти python команды изучить и скопировать, так собственно макросы и пишутся.

В ЛЮБОМ случаев я РАД что разработчики ОТКРЫЛИ доступ к этим возможностям в CE версии, СПАСИБО.

Вопросы

Сто лет, уже за CAD-ами не сидел, обычно мои вызовы были не по сеньке шапка и я в них тонул, STEP AP210 и его парсинг, интеграция и запуск Bullet движка в FreeCAD. И обычно это никому не нужно кроме меня, а я знаю мало.

Если будет предложение готов даже что то набросать, в возможное свободное время.

Пока у меня следующие вопросы:

  • Где лежит Офлайн документация?
  • Как прописать пункт в меню/создать своё меню?
  • Как повесить шорткат?
  • Где описание и исходники классов как их в emacs прописывать(это вопрос к Emacs)?

P.S. Пока мы не взяли себя в руки и не прикрутили сюда модную систему комментариев Disqus, прошу писать по вопросам на ЛОР, там есть тег cad и можете ещё меня(DR_SL) кастовать.