Всем доброго времени суток! Это мой первый пост на Хабре в расчете на конструктивную критику и советы на дальнейшее развитие.

Предыстория

Я обучаюсь веб-разработке и во время обучения ко мне обратилась знакомая психолог и попросила:

Не мог ли бы ты написать программу для подсчета мандал?

И требования к проекту:

  1. перевод слова в цифры по алфавиту;

  2. генерация шестиугольной сетки радиусом в количество цифр переведенного слова с заданным размером грани ячейки и размером холста;

  3. расстановка цифр в каждую ячейку по заранее определенному методу;

  4. иметь возможность изменения цвета во всех ячейках с одинаковым номером;

  5. иметь возможность подбора цвета с изображения;

  6. вывод полученного изображения в файл для последующего распечатывания;

  7. опционально иметь возможность вернуться к ранее созданной мандале для ее редактирования.

Для обучения мне это показалось классным кейсом. Я взялся за работу и принялся обдумывать какими средствами ее выполнить. Сразу на ум пришло: PyQt 5, книгу по которому мне заботливо подарили год назад, и веб-приложение. PyQt 5 я не знаю, да и не моя сфера обучения, а потому буду писать веб приложение. Для бекенда был выбран Flask, т.к. один из кейсов во время обучения был интеграция приложения Salesforce с Telegram ботом (Да. Я обучаюсь разработке на Salesforce) и Flask показался весьма простым и гибким фреймворком. Фронтенд было решено делать на Bootstrap и JQuery.

В связи с тем, что у моего знакомого психолога нет навыков работы с командной строкой, а так же работы с Photoshop или CorelDraw, было решено написать фронтенд, бекенд, скрипт Powershell для запуска виртуального окружения Python и открытия браузера по умолчанию на заданный адрес.

Ниже присланное мне изображение по расчету мандалы.

Вариант размещения слова по грани
Вариант размещения слова по грани
Вариант размещения слова по лучам
Вариант размещения слова по лучам

Генерация изображения

Как произвести генерацию шестиугольной сетки? Проведя поиск я наткнулся на статью. Пройдя к источнику, нашел библиотеки для различных языков программирования.

Это первый подобный опыт работы с изображением, используя JS, первая реализация была выполнена на Canvas, сетку выводила библиотека Honeycomb.

Длинный кусок кода
function buildMandalaVer12(countHex, strToHex) {
    var element = document.getElementById("stage");
    element.height = window.innerHeight;
    element.width = window.innerWidth;
    var stage = new createjs.Stage("stage");
    stage.x = window.innerWidth / 2;
    stage.y = window.innerHeight / 2;
    var grid = new Grid();
    grid.tileSize = 30;
    grid.tileSpacing = 0;
    grid.pointyTiles = false;
    var stageTransformer = new StageTransformer().initialize({
        element: element,
        stage: stage
    });
    stageTransformer.addEventListeners();
    var coordinates = grid.hexagon(0, 0, countHex, true);
    let breakIter = false;
    let stepRay = 0;
    let stepBreak = 0;
    let interimStep = 0;
    let u = 1;
    for (var i = 0; i < coordinates.length; i++) {
        var q = coordinates[i].q,
            r = coordinates[i].r,
            center = grid.getCenterXY(q, r),
            hexagon = new createjs.Shape();
        if (u == stepBreak){
            breakIter = false;
            u = 0;
        }
        if (!breakIter) {
            let str = strToHex[stepRay];
            var text = new createjs.Text(str, "10px Arial", "#ff7700");
            text.textBaseline = "alphabetic";
            text.set({
                textAlign: "center",
                textBaseline: "middle",
                x: center.x,
                y: center.y,
                rotation: -90
            });
            if (stepRay > 0) {
                interimStep += 1;
            }
        }
        if (interimStep == 6) {
            breakIter = true;
            interimStep = 1;
            stepBreak += 1;
        } else {
            u += 1;
        }
        if (i + 1 == 1) {
            stepRay = 1
        }
        hexagon.id = i;
        hexagon.id2 = i;
        // раскаска диагональных линий
        if ((q == 0 && r == 0) || q == 0 || r == 0 || ((q * -1) == r)) {
            hexagon.graphics
                .beginFill("rgba(150,0,0,1)")
                // .beginFill("rgba(150,150,150,1)")
                .beginStroke("rgba(250,250,250,1)")
                .drawPolyStar(0, 0, grid.tileSize, 6, 0, 0);
        } else {
            hexagon.graphics
                .beginFill("rgba(150,150,150,1)")
                .beginStroke("rgba(250,250,250,1)")
                .drawPolyStar(0, 0, grid.tileSize, 6, 0, 0);
        }
        hexagon.q = q;
        hexagon.r = r;
        hexagon.x = center.x;
        hexagon.y = center.y;
        hexagon.addEventListener("click", function (event) {
            if (!stageTransformer.mouse.moved) {
                console.log(event.target.id)
                event.target.graphics
                    .clear()
                    .beginFill("rgba(150,0,0,1)")
                    .beginStroke("rgba(50,0,0,1)")
                    .drawPolyStar(0, 0, grid.tileSize, 6, 0, 0);
            }
        });
        stage.addChild(hexagon);
        stage.addChild(text);
    }
    stage.set({
        rotation: 90
    });
    var tick = function (event) {
        stage.update();
    };
    tick();
    createjs.Ticker.setFPS(60);
    createjs.Ticker.addEventListener("tick", tick);
    return true;
}

Размеры в SVG считаются в пикселях, а значит миллиметры необходимо перевести. На developer.mozilla.org сказано:

https://developer.mozilla.org/ru/docs/Web/SVG/Tutorial/Positions
https://developer.mozilla.org/ru/docs/Web/SVG/Tutorial/Positions

Ок. Значит параметры вводимые в форме настроек генерации переводим и устанавливаем для параметров сетки.

Код генерации сетки
    let preload = document.getElementById('preloader');
    // перевод мм в пиксели
    let dWith = modelMandala.source.pageSize.width * 3.543307;
    let dHeight = modelMandala.source.pageSize.height * 3.543307;
    let dRangeMm = modelMandala.source.rangeMm * 3.543307;
    // генерация сетки и пересчет координат
    let options = new BHex.Drawing.Options(dRangeMm, BHex.Drawing.Static.Orientation.PointyTop, new BHex.Drawing.Point(dWith, dHeight));
    let gridBHex = new BHex.Grid(modelMandala.source.countWord);
    let gridForPaint = new BHex.Drawing.Drawing(gridBHex, options);

Теперь нужно создать само изображение на основе координат. Для этого SVG.js передаем координаты для отрисовки Polygon и элемента Text для большей наглядности.

		// создание нового объекта svg и добавление его в <object> на странице
    modelMandala.source.drawThisFigure = SVG().addTo(preload).size(dWith, dHeight).id("svgImg2");
    document.getElementById("svgImg2").setAttribute('style', 'shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd');
    let fontSize = modelMandala.source.rangeFontSize;
    // отрисовка шестиугольников и элементов Text
    for (let i = 0; i < gridForPaint.grid.hexes.length; i++) {
        modelMandala.source.drawThisFigure.polygon(gridForPaint.grid.hexes[i].points.map(({x, y}) => `${x},${y}`))
            .fill('none')
            .stroke({width: 1, color: '#000000'})
            .css({cursor: 'pointer'})
            .addClass('polygon')
            .addClass('999')
            .addClass(`${gridForPaint.grid.hexes[i].x},${gridForPaint.grid.hexes[i].y}`)
            .element('title').words(`${gridForPaint.grid.hexes[i].x},${gridForPaint.grid.hexes[i].y}`)
        modelMandala.source.drawThisFigure
            .text(`${gridForPaint.grid.hexes[i].x},${gridForPaint.grid.hexes[i].y}`)
            .font({
                size: fontSize,
                anchor: 'middle',
                leading: 1.4,
                fill: modelMandala.source.colorWord
            })
            .addClass(`${gridForPaint.grid.hexes[i].x},${gridForPaint.grid.hexes[i].y}`)
            .translate(gridForPaint.grid.hexes[i].center.x, gridForPaint.grid.hexes[i].center.y + 3);
    }

И добавление события по клику для вызова модального окна с палитрой.

for (let i = 0; i < modelMandala.source.drawThisFigure.node.children.length; i++) {
        if (modelMandala.source.drawThisFigure.node.children[i].tagName === "polygon") {
            let str = modelMandala.source.drawThisFigure.node.children[i].classList[2];
            dataPolygonMap.set(str, i);
            modelMandala.source.drawThisFigure.node.children[i].onclick = function () {
                SetColorPolygonFunc();
                polygonObj = modelMandala.source.drawThisFigure.node.children[i];
            }
        }
        if (modelMandala.source.drawThisFigure.node.children[i].tagName === "text") {
            let str = modelMandala.source.drawThisFigure.node.children[i].classList[0];
            dataTextMap.set(str, i);
        }
 }

Но изображение отрисовывается не полностью,а лишь частью.

Готовое изображение со сдвигом
Готовое изображение со сдвигом

Посмотрев координаты стало понятно, что рассчет идет от центра (координаты 0,0), а в документации SVG говорится, что есть несколько областей для просмотра. Пользовательская область задается параметром Viewbox = "0 0 0 0". Ок. Делим размер всего холста на -2.

modelMandala.source.drawThisFigure.viewbox(dWith / -2 + ' ' + dHeight / -2 + ' ' + dWith + ' ' + dHeight);

Далее необходимо провести рассчет значений во внутренних многоугольниках. Условно всю мандалу можно поделить на шесть секторов. Можно взять координаты сетки, их я сохранял в виде класса для каждого polygon, и на их основе произвести рассчет как на бумаге: берем два базовых шестиугольника, складываем цифры в них и записываем полученное значение в нужный нам шестиугольник, и так до конца. Для этого при добавлении event в каждый шестиугольник я добавлял его координаты в Map, сам SVG сохранил в глобальную переменную, а при перессчете координат создавал многоуровневый массив с координатами для каждого сектора.

// объект для сохранения всех данных в процессе работы
let modelMandala = {
    rayA: {
        rayCoord: [],
        sector: []
    },
    rayB: {
        rayCoord: [],
        sector: []
    },
    rayC: {
        rayCoord: [],
        sector: []
    },
  ...
    // вызов функции для просчета координат и собственно значений в шестиугольниках
    if (modelMandala.source.mandalaVersion === 1 || modelMandala.source.mandalaVersion === 2 || modelMandala.source.mandalaVersion === 3 || modelMandala.source.mandalaVersion === 4) {
        axialDataSetFunc();
    }
    if (modelMandala.source.mandalaVersion === 5 || modelMandala.source.mandalaVersion === 6 || modelMandala.source.mandalaVersion === 7 || modelMandala.source.mandalaVersion === 8) {
        borderDataSetFunc();
    }
// заполнение мандалы "по грани" координатами
    function getArrOnBorderAndSector() {
        let resArr = [];
        let interimArr = [];
        let step = modelMandala.source.countWord;
        // ray A
        let a = modelMandala.source.countWord, b = 0;
        let a2 = a, b2 = b, interimStep = step;
        for (let i = 1; i <= step; i++) {
            for (let y = 1; y <= interimStep; y++) {
                if (i === 1) {
                    y === 1 ? modelMandala.rayA.rayCoord.push([a2, b2]) : modelMandala.rayA.rayCoord.push([a2, --b2])
                } else {
                    y === 1 ? interimArr.push([a2, b2]) : interimArr.push([a2, --b2])
                }
            }
            a -= 1; b = 0; a2 = a; b2 = b;
            interimStep -= 1;
            if (i !== 1) {
                resArr.push(interimArr);
                interimArr = [];
            }

        }
        modelMandala.rayA.sector.push(resArr);
        resArr = [];

        // ray B
        ...
// установка значений в класс для автовыбора при расскращивании.
    function axialDataSet() {
        $('.navbar').width(0)
        getArrOnRayAndSector();
        // установка значений по осям
        let numb = 1;
        for (let key in modelMandala) {
            numb = 1;
            if (key === "source") break;
            for (let i = 0; i < modelMandala[key].rayCoord.length; i++) {
                let obj = getValOnCoordinate(modelMandala.source.drawThisFigure, modelMandala[key].rayCoord[i][0], modelMandala[key].rayCoord[i][1], false);
                obj.classList.replace('999', String(modelMandala.source.wordInInt[numb]));
                obj.firstChild.innerHTML = String(modelMandala.source.wordInInt[numb]);
                obj.attributes.fill.value = '#f62b58';
                let objText = getValOnCoordinate(modelMandala.source.drawThisFigure, modelMandala[key].rayCoord[i][0], modelMandala[key].rayCoord[i][1], true);
                objText.firstChild.innerHTML = String(modelMandala.source.wordInInt[numb]);
                numb++;
            }
        }
        // установка значений по полям
        for (let key in modelMandala) {
            let countStep = 1;
            if (key === "source") break;
            for (let i = 0; i < modelMandala[key].sector[0].length; i++) {
                for (let u = 0; u < modelMandala[key].sector[0][i].length; u++) {
                    let objForChange = getValOnCoordinate(modelMandala.source.drawThisFigure, modelMandala[key].sector[0][i][u][0], modelMandala[key].sector[0][i][u][1], false);
                    let objParent1, objParent2;
                    if (key === "rayA") {
                        objParent1 = getValOnCoordinate(modelMandala.source.drawThisFigure, modelMandala[key].sector[0][i][u][0] - 1, modelMandala[key].sector[0][i][u][1], false);
                        objParent2 = getValOnCoordinate(modelMandala.source.drawThisFigure, modelMandala[key].sector[0][i][u][0] - 1, modelMandala[key].sector[0][i][u][1] + 1, false);
                    }
                    
                    ...
                    
                    let res = Number(objParent1.classList[1]) + Number(objParent2.classList[1]);
                    if (res >= 10) {
                        res = String(res);
                        res = Number(res[0]) + Number(res[1]);
                    }
                    objForChange.classList.replace('999', String(res));
                    objForChange.firstChild.innerHTML = String(res);
                    objForChange.attributes.fill.value = '#f62bdb';
                    let objText = getValOnCoordinate(modelMandala.source.drawThisFigure, modelMandala[key].sector[0][i][u][0], modelMandala[key].sector[0][i][u][1], true);
                    objText.firstChild.innerHTML = String(res);
                }
            }
        }
        // установка центрального значения
        let obj = getValOnCoordinate(modelMandala.source.drawThisFigure, 0, 0, false);
        obj.classList.replace('999', String(modelMandala.source.wordInInt[0]));
        obj.firstChild.innerHTML = String(modelMandala.source.wordInInt[0]);
        obj.attributes.fill.value = '#f62b58';
        let objText = getValOnCoordinate(modelMandala.source.drawThisFigure, 0, 0, true);
        objText.firstChild.innerHTML = String(modelMandala.source.wordInInt[0]);
        objText.setAttribute("font-weight", "900");
        $('#getImageSchema').popover('enable');
        $('#getImageSchema').removeClass("invisible");
        $('#getImageColor').removeClass("invisible");
        $('.navbar').width(document.documentElement.scrollWidth)
        setProgress(true);
    }

В приведенном выше кода описано: перессчет координат по секторам и создание многомерных массивов, расставления значений, начиная с осей мандалы, и перессчет значений на основе порядка массива координат в каждую ячейку для каждого сектора. Получение значений двух базовых шестиугольников производится в функции getValOnCoordinate

    // получение координаты из ListStyle
    function getValOnCoordinate(stage, o1, o2, text) {
        let strForSearch = String(o1) + "," + String(o2);
        if (text) {
            let o = dataTextMap.get(strForSearch);
            return stage.node.children[o];
        } else {
            let o = dataPolygonMap.get(strForSearch);
            return stage.node.children[o];
        }
    }

Результат работы:

Готовое изображение
Готовое изображение

Интрефейс для работы

Было решено дать полную свободу действий в настройке изображения на свой вкус и цвет и создать два меню: для сохранения в базу, для генерации и сохранения в файл избражения или схемы. Предполагается, что готовое изображение будет распечатываться в виде схемы и некто будет его раскрашивать по номерам. Прошу не пинать, т.к. проект я рассматривал только с точки зрения опыта, а не подтекста использования на людях. Итого: меню для генерации и сохрания в файл и меню для сохранения в базу.

Скрины меню
Скрины меню

Так же один из элементов интерфейса является палитра, выполненная в Modal. Имеется две палитры на выбор: обыная и по требованию выбор цвета с рисунка.

Пример обычной палитры
Пример обычной палитры
Пример палитры с рисунка
Пример палитры с рисунка

И уведомляшки о сохранении, загрузке и удалении готового рисунка.

Весь HTML код написан в шаблонах Flask и отражать его в статье нет смысла, т.к. это обычная верстка с применением Bootstrap.

Сохранение изображения в файл

Изначально была задумка отдавать пользователю SVG файл, но из-за отсутствия навыков работы в Corel самым простым решением было выводить pdf с заданным размером страницы и в последующем его печатать.

Сам процесс перевода SVG в pdf на фронте (ссылка) оказался чрезвычайно долгим, т.к. происходил полный перебор SVG со страницы, генерация pdf и последущее добавление polygon в файл. Проще всего оказалось отдать файл на back-end, конвертировать его там и отдать pdf пользователю. Отдача SVG происходит Ajax в виде строки, которая записвается в файл, а дальше вступает в действие Cariosvg. После готовый pdf отдается на фронтенд ответом на Ajax в виде бинарной строки.

Код сохранения svg, его перевода в pdf и отдача файла пользователю
/*
    * сохранение изображения
    * */

    $("#getImageSchema").on('click', function () {
        setProgress(false);
        for (let i = 0; i < modelMandala.source.drawThisFigure.node.children.length; i++) {
            if (modelMandala.source.drawThisFigure.node.children[i].tagName === "polygon") {
                modelMandala.source.drawThisFigure.node.children[i].attributes.fill.value = '#ffffff';
            }
        }
        getPdf();
    });

    $("#getImageColor").on('click', function () {
        setProgress(false);
        getPdf();
    });

    function getPdf() {
        let dWith = modelMandala.source.pageSize.width * 3.543307;
        let dHeight = modelMandala.source.pageSize.height * 3.543307;
        let svh = new XMLSerializer().serializeToString(document.getElementById('svgImg2'))
        $.ajax({
            type: "POST",
            url: "/upload",
            data: {data: svh, pWidth: dWith, pHeight: dHeight},
            dataType: 'binary',
            xhrFields: {
                'responseType': 'blob'
            },
        }).done(function (data, status, xhr) {
            let link = document.createElement('a'), filename = modelMandala.source.word + '.pdf';
            link.href = URL.createObjectURL(data);
            link.download = filename;
            link.click();
            setProgress(true);
        });
    }
@app.route('/upload', methods=['POST'])
def upload():
    if os.path.exists(uploads_dir + '\\test.svg'):
        os.remove(uploads_dir + '\\test.svg')
    if os.path.exists(uploads_dir + '\\test.pdf'):
        os.remove(uploads_dir + '\\test.pdf')
    f = open(uploads_dir + '\\test.svg', 'w')
    f.write(request.form['data'])
    f.close()
    x = threading.Thread(target=cairosvg.svg2pdf(url=uploads_dir + '\\test.svg', write_to=uploads_dir + '\\test.pdf'),
                         args=(1,))
    x.start()
    x.join()
    return send_file(filename_or_fp=uploads_dir + '\\test.pdf', mimetype='application/pdf; version="1.5"',
                     as_attachment=True)

Сохранение и восстановление данных из базы также просто. На python передается json со всеми данными объекта modelMandala и сериализованный SVG. Используя sqlalchemy сохраняю эти данные в файл SQLite.

Далее должо было быть написание скрипта powershell для запуска Fask и в целом на этом можно было бы заканчивать, но...

Послесловие

Не интересуясь возможностями JS и случайно наткнувшись на пост я открыл для себя Electron JS и подумал: а что если все приложение упаковать pyinstaller и при запуске Electron приложения производит запуск упакованного файла python, а при закрытии окна завершать его работу? Для конечного пользователя это устраняет необходимость установки python и наличия открытого окна консоли powershell. В итоге так и было сделано. Ниже файл render.js для electron и package.json для сборки приложения.

render.js
"use strict";

const {app, BrowserWindow, session, Menu, MenuItem} = require("electron");
const path = require("path");
let mainWindow = null;
let subpy = null;

const PY_DIST_FOLDER = "dist-python"; 
const PY_SRC_FOLDER = "web_app"; 
const PY_MODULE = "wsgi.py"; 

const isRunningInBundle = () => {
    return require("fs").existsSync(path.join(__dirname, PY_DIST_FOLDER));
};

const getPythonScriptPath = () => {
    if (!isRunningInBundle()) {
        return path.join(__dirname, PY_SRC_FOLDER, PY_MODULE);
    }
    if (process.platform === "win32") {
        return path.join(
            __dirname,
            PY_DIST_FOLDER,
            PY_MODULE.slice(0, -3) + ".exe"
        );
    }
    return path.join(__dirname, PY_DIST_FOLDER, PY_MODULE);
};

const startPythonSubprocess = () => {
    let script = getPythonScriptPath();
    if (isRunningInBundle()) {
        subpy = require("child_process").execFile(script, []);
    } else {
        subpy = require("child_process").spawn("python", [script]);
    }
};

const killPythonSubprocesses = main_pid => {
    const python_script_name = path.basename(getPythonScriptPath());
    let cleanup_completed = false;
    const psTree = require("ps-tree");
    psTree(main_pid, function (err, children) {
        let python_pids = children
            .filter(function (el) {
                return el.COMMAND == python_script_name;
            })
            .map(function (p) {
                return p.PID;
            });
        python_pids.forEach(function (pid) {
            process.kill(pid);
        });
        subpy = null;
        cleanup_completed = true;
    });
    return new Promise(function (resolve, reject) {
        (function waitForSubProcessCleanup() {
            if (cleanup_completed) return resolve();
            setTimeout(waitForSubProcessCleanup, 30);
        })();
    });
};
const createMainWindow = () => {
    mainWindow = new BrowserWindow({
        width: 1366,
        height: 768,
        icon: __dirname + "/icon.ico",
        fullscreen: true,
        frame: true,
        resizeable: true
    });
    mainWindow.loadURL("http://localhost:5000/");
    mainWindow.on("closed", function () {
        mainWindow = null;
    });
};
app.on("ready", function () {
    startPythonSubprocess();
    createMainWindow();
    setTimeout(()=>{
       mainWindow.loadURL("http://localhost:5000/");
    }, 5);
});
const template = [
    {
        role: 'Help',
        submenu: [
            {
                role: 'reload'
            },
            {
                role: 'close'
            }
        ]
    }
]
const menu = Menu.buildFromTemplate(template)
app.on("browser-window-created", function (e, window) {
    window.setMenu(menu);
});
app.on("window-all-closed", () => {
    if (process.platform !== "darwin") {
        let main_process_pid = process.pid;
        killPythonSubprocesses(main_process_pid).then(() => {
            app.quit();
        });
    }
});

app.on("activate", () => {
    if (subpy == null) {
        startPythonSubprocess();
    }
    if (mainWindow === null) {
        createMainWindow();
    }
});

app.on("quit", function () {
    session.defaultSession.clearCache();
});
package.json для сборки приложения
{
  "name": "MandalaApp",
  "version": "1.2.0",
  "description": "",
  "main": "renderer.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "electron .",
    "package": "npm run -s package-python && npm run -s package-electron && npm run -s package-cleanup",
    "package-python": "pyinstaller -w --onefile --add-binary web_app/venv/Lib/site-packages/cairosvg;cairosvg --add-binary web_app/venv/Lib/site-packages/cairocffi;cairocffi --add-binary web_app/venv/Lib/site-packages/PIL;pillow --add-binary web_app/venv/Lib/site-packages/defusedxml;defusedxml --add-binary web_app/venv/Lib/site-packages/tinycss2;tinycss2 --add-binary web_app/venv/Lib/site-packages/cssselect2;cssselect2 --add-binary web_app/venv/Lib/site-packages/cffi;cffi --add-binary web_app/venv/Lib/site-packages/pycparser;pycparser --add-binary web_app/venv/Lib/site-packages/webencodings;webencodings --add-data web_app/templates;templates --add-data web_app/static;static --add-data web_app/instance;instance --add-data web_app/migrations;migrations --add-data web_app/instance/app.db;instance web_app/wsgi.py web_app/app.py web_app/config.py web_app/identifier.sqlite web_app/models.py web_app/routes.py --distpath dist-python",
    "package-electron": "electron-builder"
  },
  "build": {
    "appId": "com.MandalaApp.klim-app",
    "productName": "MandalaApp",
    "asar": false,
    "asarUnpack": [
      "**/*.node"
    ],
    "mac": {
      "category": "public.app-category.utilities"
    },
    "files": [
      "renderer.js",
      "icon.ico",
      "node_modules/**/*"
    ],
    "extraResources": [
      {
        "from": "dist-python/",
        "to": "app/dist-python",
        "filter": [
          "**/*"
        ]
      }
    ]
  },
  "author": "Ilia Klimchik https://github.com/klimchak",
  "license": "MIT",
  "dependencies": {
    "ps-tree": "^1.2.0"
  },
  "devDependencies": {
    "electron": "^9.2.0",
    "electron-builder": "^22.8.0"
  }
}

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

Более подробно посмотреть код можно на GitHub.

P.s. Прошу сильно не забрасывать камнями. Буду рад любой конструкивной критике, code review, советам по иной реализации, а так же по обучению и направлению развития в web-разработке.

Комментарии (9)


  1. Zada
    26.07.2021 21:51
    +1

    Что это за мандала и зачем она психологу?


    1. luxa_klim Автор
      26.07.2021 22:05

      На сколько я понял - это как элемент терапии. Что-то вроде арт-терапии только замороченной на цвете. Более точно сказать не могу т.к. не вникал в суть этой темы.


      1. vmkazakoff
        27.07.2021 08:53

        К психотерапии отношения вообще никакого, не путайте людей. Это нумерология и эзотерика, ближе к астрологии.

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

        И, честно говоря, я лично такой вариант вообще не встречал. Нашел только такое описание:

        Осторожно

        Расчёт числовой мандалы на примере живого человека

        Расчёт проводим на нашем любимом авторе Льве Александровиче Дебаркадере. Дата рождения 1.09.1983

        Для начала считаем по дате рождения число сущности и числа тел.

        • Число здоровья: 1

        • Число эмоций: 9

        • Число мыслей: 3

        Вибрационное число сущности: 4

        Теперь возьмёмся за число личности.

        • Число цели: Лев = 4

        • Число приспособления: Александрович = 9

        • Число группы: Дебаркадер = 2

        Вибрационное число личности: 6.

        Золотое алхимическое число: 1


        1. MentalBlood
          27.07.2021 09:29

          Число мыслей: 3

          Жесткое ограничение однако


        1. luxa_klim Автор
          27.07.2021 09:42

          Приму к сведению и повторюсь: мне не важна суть и я не вникал для чего это. Тема внешне безобидная. Понравилась задача.


  1. corvair
    27.07.2021 04:00

    Почему-то при виде на КДПВ возникла ассоциация с реактором РБМК.


    1. Skywrtr
      27.07.2021 12:06

      У РБМК квадратные крышки. А вот активная зона ВВЭР прям один в один.


  1. sargon5000
    27.07.2021 09:44
    +1

    Ради бога не начинай статьи с этого жуткого "Доброго времени суток". Во-первых, известное обоснование этого бреда – мол, неизвестно, когда читатель прочтет это – оно полностью абсурдное, ведь и доинтернетные времена люди писали друг другу письма, и никогда нельзя было с уверенностью сказать, прочтет корреспондент письмо утром, днём или ночью. Во-вторых, в русском языке все приветствия в родительном падеже – это всегда прощания. "Счастливого пути", "скатертью дорога", "всего хорошего", "доброй ночи" и т.д. Разве ты, носитель русского языка, это сам не чувствуешь? А в этом твоём приветствии-прощании аж два родительных падежа подряд! Ф-фу. Не знаешь, как поприветствовать аудиторию? Вспомни хотя бы слово "здравствуйте". Или оно кажется слишком официальным? Вот по сравнению с "доброго времени суток" кажется официальным?! Ну так пиши "Привет".


    1. luxa_klim Автор
      27.07.2021 09:45

      Я понял) Приму к сведению) Спасибо.