diff --git a/.gitignore b/.gitignore index 7fc7ed4..ce5ff32 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,7 @@ dist/ build docs/html .directory -loggraph.kdev4 +*.kdev4 *.json .kdev4 AccountFree.pro diff --git a/qml/AppMenuBar.qml b/qml/AppMenuBar.qml index cbd027f..fc01fb9 100644 --- a/qml/AppMenuBar.qml +++ b/qml/AppMenuBar.qml @@ -27,20 +27,20 @@ MenuBar { text: qsTr("&Load...") shortcut: StandardKey.Open onTriggered: settings.load() - icon.name: 'fileopen' + icon.name: 'document-open' } Action { text: qsTr("&Save") shortcut: StandardKey.Save onTriggered: settings.save() - icon.name: 'filesave' + icon.name: 'document-save' } Action { text: qsTr("Save &As...") shortcut: StandardKey.SaveAs onTriggered: settings.saveAs() - icon.name: 'filesaveas' + icon.name: 'document-save-as' } MenuSeparator { } @@ -57,7 +57,7 @@ MenuBar { text: qsTr("&Copy diagram") shortcut: StandardKey.Copy onTriggered: root.copyDiagramToClipboard() - icon.name: 'editcopy' + icon.name: 'edit-copy' } } Menu { diff --git a/qml/ComboBoxSetting.qml b/qml/ComboBoxSetting.qml index 7d3a2c5..4e79ce3 100644 --- a/qml/ComboBoxSetting.qml +++ b/qml/ComboBoxSetting.qml @@ -24,10 +24,16 @@ Item { height: 30 signal activated(int newIndex) + signal accepted() - property var model: [] property string label: '' + property alias model: combox.model + property alias editable: combox.editable + property alias editText: combox.editText property alias currentIndex: combox.currentIndex + function find(elementName) { + return combox.find(elementName) + } Text { id: labelItem @@ -43,11 +49,10 @@ Item { height: 30 anchors.left: labelItem.right anchors.leftMargin: 5 - width: control.width - labelItem.width - model: control.model - currentIndex: model.indexOf(defValue) + width: control.width - labelItem.width - 5 onActivated: function(newIndex) { control.activated(newIndex) } + onAccepted: control.accepted() } } diff --git a/qml/LogGraphCanvas.qml b/qml/LogGraphCanvas.qml index 850daf1..3aaa216 100644 --- a/qml/LogGraphCanvas.qml +++ b/qml/LogGraphCanvas.qml @@ -19,6 +19,7 @@ import QtQuick 2.12 import "js/objects.js" as Objects import "js/utils.js" as Utils +import "js/mathlib.js" as MathLib Canvas { @@ -32,12 +33,18 @@ Canvas { property double ymax: 0 property int xzoom: 10 property int yzoom: 10 - property double yaxisstep: 3 + property string yaxisstep: "3" property string xlabel: "" property string ylabel: "" property int maxgradx: 8 property int maxgrady: 1000 + property var yaxisstepExpr: (new MathLib.Expression(`x*(${yaxisstep})`)) + property double yaxisstep1: yaxisstepExpr.execute(1) + property int drawMaxY: Math.ceil(Math.max(Math.abs(ymax), Math.abs(px2y(canvasSize.height)))/yaxisstep1) + + Component.onCompleted: console.log(yaxisstepExpr.toEditableString()) + onPaint: { //console.log('Redrawing') var ctx = getContext("2d"); @@ -71,8 +78,9 @@ Canvas { drawXLine(ctx, Math.pow(10, xpow)*xmulti) } } - for(var y = -Math.round(100/yaxisstep)*yaxisstep; y < canvas.ymax; y+=yaxisstep) { - drawYLine(ctx, y) + for(var y = 0; y < drawMaxY; y+=1) { + drawYLine(ctx, y*yaxisstep1) + drawYLine(ctx, -y*yaxisstep1) } } @@ -99,16 +107,21 @@ Canvas { var textSize = ctx.measureText(canvas.xlabel).width ctx.fillText(canvas.xlabel, canvas.canvasSize.width-14-textSize, axisxpx-5) // Axis graduation labels - ctx.font = "14px sans-serif" + ctx.font = "12px sans-serif" for(var xpow = -maxgradx; xpow <= maxgradx; xpow+=1) { var textSize = ctx.measureText("10"+Utils.textsup(xpow)).width if(xpow != 0) - drawVisibleText(ctx, "10"+Utils.textsup(xpow), x2px(Math.pow(10,xpow))-textSize/2, axisxpx+12+(6*(y==0))) + drawVisibleText(ctx, "10"+Utils.textsup(xpow), x2px(Math.pow(10,xpow))-textSize/2, axisxpx+16+(6*(y==0))) } - for(var y = -Math.round(maxgrady/yaxisstep)*yaxisstep; y < canvas.ymax; y+=yaxisstep) { - var textSize = ctx.measureText(y).width - drawVisibleText(ctx, y, axisypx-3-textSize, y2px(y)+6+(6*(y==0))) + var txtMinus = ctx.measureText('-').width + for(var y = 0; y < drawMaxY; y += 1) { + var drawY = y*yaxisstep1 + var txtY = yaxisstepExpr.simplify(y) + var textSize = ctx.measureText(txtY).width + drawVisibleText(ctx, txtY, axisypx-6-textSize, y2px(drawY)+4+(10*(y==0))) + if(y != 0) + drawVisibleText(ctx, '-'+txtY, axisypx-6-textSize-txtMinus, y2px(-drawY)+4) } ctx.fillStyle = "#FFFFFF" } diff --git a/qml/ObjectLists.qml b/qml/ObjectLists.qml index e7f2308..e1b2929 100644 --- a/qml/ObjectLists.qml +++ b/qml/ObjectLists.qml @@ -201,7 +201,7 @@ ListView { isDouble: modelData[1] == 'number' visible: ['Expression', 'Domain', 'string', 'number'].indexOf(modelData[1]) >= 0 defValue: visible ? { - 'Expression': function(){return objEditor.obj[modelData[0]].toEditableString()}, + 'Expression': function(){return Utils.simplifyExpression(objEditor.obj[modelData[0]].toEditableString())}, 'Domain': function(){return objEditor.obj[modelData[0]].toString()}, 'string': function(){return objEditor.obj[modelData[0]]}, 'number': function(){return objEditor.obj[modelData[0]]} diff --git a/qml/Settings.qml b/qml/Settings.qml index 46f3916..ecdd26a 100644 --- a/qml/Settings.qml +++ b/qml/Settings.qml @@ -20,23 +20,23 @@ import QtQuick.Controls 2.12 import QtQuick 2.12 import "js/utils.js" as Utils -Grid { +Column { id: settings - height: 30*Math.max(1, Math.ceil(7 / columns)) - columns: Math.floor(width / settingWidth) + height: 30*9 //30*Math.max(1, Math.ceil(7 / columns)) + //columns: Math.floor(width / settingWidth) spacing: 10 signal changed() - property int settingWidth: 135 + property int settingWidth: settings.width property int xzoom: 100 property int yzoom: 10 property double xmin: 5/10 property double ymax: 25 - property int yaxisstep: 4 - property string xaxislabel: "ω (rad/s)" - property string yaxislabel: "G (dB)" + property string yaxisstep: "4" + property string xaxislabel: "" + property string yaxislabel: "" property string saveFilename: "" FileDialog { @@ -48,6 +48,10 @@ Grid { root.saveDiagram(filePath) } else { root.loadDiagram(filePath) + if(xAxisLabel.find(settings.xaxislabel) == -1) xAxisLabel.model.append({text: settings.xaxislabel}) + xAxisLabel.editText = settings.xaxislabel + if(yAxisLabel.find(settings.yaxislabel) == -1) yAxisLabel.model.append({text: settings.yaxislabel}) + yAxisLabel.editText = settings.yaxislabel } } } @@ -108,7 +112,7 @@ Grid { TextSetting { id: yAxisStep height: 30 - isInt: true + //isInt: true label: "Y Axis Step" width: settings.settingWidth defValue: settings.yaxisstep @@ -118,6 +122,58 @@ Grid { } } + ComboBoxSetting { + id: xAxisLabel + height: 30 + width: settings.settingWidth + label: 'X Label' + model: ListModel { + ListElement { text: "" } + ListElement { text: "x" } + ListElement { text: "ω (rad/s)" } + } + currentIndex: find(settings.xaxislabel) + editable: true + onAccepted: function(){ + editText = Utils.parseName(editText, false) + if (find(editText) === -1) model.append({text: editText}) + settings.xaxislabel = editText + settings.changed() + } + onActivated: function(selectedId) { + settings.xaxislabel = model.get(selectedId).text + settings.changed() + } + Component.onCompleted: editText = settings.xaxislabel + } + + ComboBoxSetting { + id: yAxisLabel + height: 30 + width: settings.settingWidth + label: 'Y Label' + model: ListModel { + ListElement { text: "" } + ListElement { text: "y" } + ListElement { text: "G (dB)" } + ListElement { text: "φ (deg)" } + ListElement { text: "φ (rad)" } + } + currentIndex: find(settings.yaxislabel) + editable: true + onAccepted: function(){ + editText = Utils.parseName(editText, false) + if (find(editText) === -1) model.append({text: editText, yaxisstep: root.yaxisstep}) + settings.yaxislabel = editText + settings.changed() + } + onActivated: function(selectedId) { + settings.yaxislabel = model.get(selectedId).text + settings.changed() + } + Component.onCompleted: editText = settings.yaxislabel + } + Button { id: copyToClipboard height: 30 @@ -127,30 +183,6 @@ Grid { onClicked: root.copyDiagramToClipboard() } - TextSetting { - id: xAxisLabel - height: 30 - label: "X Label" - width: settings.settingWidth - defValue: settings.xaxislabel - onChanged: function(newValue) { - settings.xaxislabel = Utils.parseName(newValue, false) - settings.changed() - } - } - - TextSetting { - id: yAxisLabel - height: 30 - label: "Y Label" - width: settings.settingWidth - defValue: settings.yaxislabel - onChanged: function(newValue) { - settings.yaxislabel = Utils.parseName(newValue, false) - settings.changed() - } - } - Button { id: saveDiagram height: 30 diff --git a/qml/TextSetting.qml b/qml/TextSetting.qml index d215a74..228aa5d 100644 --- a/qml/TextSetting.qml +++ b/qml/TextSetting.qml @@ -30,49 +30,35 @@ Item { property double min: 1 property string label property string defValue + + Text { + id: labelItem + height: 30 + anchors.top: parent.top + verticalAlignment: TextInput.AlignVCenter + color: sysPalette.windowText + text: " "+ control.label +": " + } - Item { - anchors.centerIn: parent - width: labelItem.width + input.width - height: Math.max(labelItem.height, input.height) - Text { - id: labelItem - height: 30 - anchors.top: parent.top - verticalAlignment: TextInput.AlignVCenter - color: sysPalette.windowText - text: " "+ control.label +": " - } - - - TextInput { - id: input - anchors.top: parent.top - anchors.left: labelItem.right - anchors.leftMargin: 5 - width: control.width - labelItem.width - height: 30 - verticalAlignment: TextInput.AlignVCenter - horizontalAlignment: TextInput.AlignHCenter - color: sysPalette.windowText - focus: true - text: control.defValue - selectByMouse: true - onEditingFinished: { - var value = text - if(control.isInt) value = Math.max(control.min,parseInt(value).toString()=="NaN"?control.min:parseInt(value)) - if(control.isDouble) value = Math.max(control.min,parseFloat(value).toString()=="NaN"?control.min:parseFloat(value)) - if(value != "") control.changed(value) - } - } - - Rectangle { - color: sysPalette.windowText - anchors.left: input.left - anchors.right: input.right - anchors.bottom: input.bottom - height: 2 + TextField { + id: input + anchors.top: parent.top + anchors.left: labelItem.right + anchors.leftMargin: 5 + width: control.width - labelItem.width - 5 + height: 30 + verticalAlignment: TextInput.AlignVCenter + horizontalAlignment: TextInput.AlignHCenter + color: sysPalette.windowText + focus: true + text: control.defValue + selectByMouse: true + onEditingFinished: { + var value = text + if(control.isInt) value = Math.max(control.min,parseInt(value).toString()=="NaN"?control.min:parseInt(value)) + if(control.isDouble) value = Math.max(control.min,parseFloat(value).toString()=="NaN"?control.min:parseFloat(value)) + if(value != "") control.changed(value) } } } diff --git a/qml/js/mathlib.js b/qml/js/mathlib.js index 74bbf4d..3ba573d 100644 --- a/qml/js/mathlib.js +++ b/qml/js/mathlib.js @@ -23,10 +23,21 @@ const parser = new ExprEval.Parser() +var evalVariables = { // Variables not provided by expr-eval.js, needs to be provided manualy + "pi": Math.PI, + "π": Math.PI, + "inf": Infinity, + "Infinity": Infinity, + "∞": Infinity, + "e": Math.E +} + class Expression { constructor(expr) { this.expr = expr this.calc = parser.parse(expr).simplify() + this.cached = this.isConstant() + this.cachedValue = this.cached ? this.calc.evaluate(evalVariables) : null } isConstant() { @@ -34,19 +45,14 @@ class Expression { } execute(x = 1) { - return this.calc.evaluate({ - "x": x, - "pi": Math.PI, - "π": Math.PI, - "inf": Infinity, - "Infinity": Infinity, - "∞": Infinity, - "e": Math.E - }) + if(this.cached) return this.cachedValue + return this.calc.evaluate(Object.assign({'x': x}, evalVariables)) } simplify(x = 1) { - return Utils.makeExpressionReadable(this.calc.substitute('x', x).simplify().toString()) + var expr = this.calc.substitute('x', x).simplify() + if(expr.evaluate(evalVariables) == 0) return '0' + return Utils.makeExpressionReadable(expr.toString()) } toEditableString() { diff --git a/qml/js/objects.js b/qml/js/objects.js index 10bba62..32b0d51 100644 --- a/qml/js/objects.js +++ b/qml/js/objects.js @@ -200,7 +200,7 @@ class Function extends ExecutableObject { getReadableString() { if(this.displayMode == 'application') { - return `${this.name}: ${this.inDomain} ⸺˃ ${this.outDomain}\n ${' '.repeat(this.name.length)}x ⸺˃ ${this.expression.toString()}` + return `${this.name}: ${this.inDomain} ⸺> ${this.outDomain}\n ${' '.repeat(this.name.length)}x ⸺> ${this.expression.toString()}` } else { return `${this.name}(x) = ${this.expression.toString()}` } diff --git a/qml/js/utils.js b/qml/js/utils.js index 6f682fc..eee2192 100644 --- a/qml/js/utils.js +++ b/qml/js/utils.js @@ -119,27 +119,105 @@ function textsub(text) { return ret } +function simplifyExpression(str) { + var replacements = [ + // Operations not done by parser. + [ // Removing parenthesis when content is only added from both sides. + /(^.?|[+-] |\()\(([^)(]+)\)(.?$| [+-]|\))/g, + function(match, b4, middle, after) {return `${b4}${middle}${after}`} + ], + [ // Removing parenthesis when content is only multiplied. + /(^.?|[*\/] |\()\(([^)(+-]+)\)(.?$| [*\/]|\))/g, + function(match, b4, middle, after) {return `${b4}${middle}${after}`} + ], + [// Simplification additions/substractions. + /(^.?|[^*\/] |\()([-.\d]+) (\+|\-) (\([^)(]+\)|[^)(]+) (\+|\-) ([-.\d]+)(.?$| [^*\/]|\))/g, + function(match, b4, n1, op1, middle, op2, n2, after) { + var total + if(op2 == '+') { + total = parseFloat(n1) + parseFloat(n2) + } else { + total = parseFloat(n1) - parseFloat(n2) + } + return `${b4}${total} ${op1} ${middle}${after}` + } + ], + [// Simplification multiplications/divisions. + /([-.\d]+) (\*|\/) (\([^)(]+\)|[^)(+-]+) (\*|\/) ([-.\d]+)/g, + function(match, n1, op1, middle, op2, n2) { + if(parseInt(n1) == n1 && parseInt(n2) == n2 && op2 == '/' && + (parseInt(n1) / parseInt(n2)) % 1 != 0) { + // Non int result for int division. + return `(${n1} / ${n2}) ${op1} ${middle}` + } else { + if(op2 == '*') { + return `${parseFloat(n1) * parseFloat(n2)} ${op1} ${middle}` + } else { + return `${parseFloat(n1) / parseFloat(n2)} ${op1} ${middle}` + } + } + } + ], + [// Starting & ending parenthesis if not needed. + /^\((.*)\)$/g, + function(match, middle) { + var str = middle + // Replace all groups + while(/\([^)(]+\)/g.test(str)) + str = str.replace(/\([^)(]+\)/g, '') + // There shouldn't be any more parenthesis + // If there is, that means the 2 parenthesis are needed. + if(!str.includes(')') && !str.includes('(')) { + return middle + } else { + return `(${middle})` + } + + } + ], + // Simple simplifications + [/(\s|^|\()0 \* (\([^)(]+\))/g, '$10'], + [/(\s|^|\()0 \* ([^)(+-]+)/g, '$10'], + [/(\([^)(]\)) \* 0(\s|$|\))/g, '0$2'], + [/([^)(+-]) \* 0(\s|$|\))/g, '0$2'], + [/(\s|^|\()1 (\*|\/) /g, '$1'], + [/(\s|^|\()0 (\+|\-) /g, '$1'], + [/ (\*|\/) 1(\s|$|\))/g, '$2'], + [/ (\+|\-) 0(\s|$|\))/g, '$2'], + [/(^| |\() /g, '$1'], + [/ ($|\))/g, '$1'], + ] + + console.log(str) + // Replacements + replacements.forEach(function(replacement){ + while(replacement[0].test(str)) + str = str.replace(replacement[0], replacement[1]) + }) + return str +} + function makeExpressionReadable(str) { var replacements = [ + // variables [/pi/g, 'π'], [/Infinity/g, '∞'], [/inf/g, '∞'], + // Other [/ \* /g, '×'], [/ \^ /g, '^'], [/\^\(([^\^]+)\)/g, function(match, p1) { return textsup(p1) }], [/\^([^ ]+)/g, function(match, p1) { return textsup(p1) }], [/(\d|\))×/g, '$1'], [/×(\d|\()/g, '$1'], - [/\(([^)(+.-]+)\)/g, "$1"], - [/\(([^)(+.-]+)\)/g, "$1"], - [/\(([^)(+.-]+)\)/g, "$1"], - [/\(([^)(+.-]+)\)/g, "$1"], - [/\(([^)(+.-]+)\)/g, "$1"], - // Doing it 4 times to be recursive until better implementation + [/\(([^)(+.\/-]+)\)/g, "$1"], ] + + str = simplifyExpression(str) // Replacements replacements.forEach(function(replacement){ - str = str.replace(replacement[0], replacement[1]) + while(replacement[0].test(str)) + str = str.replace(replacement[0], replacement[1]) }) return str } @@ -184,7 +262,7 @@ function parseName(str, removeUnallowed = true) { [/_\(([^\^]+)\)/g, function(match, p1) { return textsub(p1) }], [/_([^ ]+)/g, function(match, p1) { return textsub(p1) }], // Removing - [/[xπℝℕ\\∪∩\]\[ ()^/÷*×+=\d-]/g , function(match){console.log('removing', match); return ''}], + [/[xπℝℕ\\∪∩\]\[ ()^/÷*×+=\d-]/g , ''], ] if(!removeUnallowed) replacements.pop() // Replacements @@ -194,6 +272,7 @@ function parseName(str, removeUnallowed = true) { return str } + String.prototype.toLatinUppercase = function() { return this.replace(/[a-z]/g, function(match){return match.toUpperCase()}) } diff --git a/run.py b/run.py index 4a28c91..4a2d0f8 100644 --- a/run.py +++ b/run.py @@ -30,7 +30,7 @@ tempfile = tempfile.mkstemp(suffix = '.png')[1] def get_linux_theme(): des = { - "KDE": "org.kde.desktop", # org.kde.desktop resolves to universal in PySide2. + "KDE": "fusion", # org.kde.desktop resolves to universal in PySide2. "gnome": "default", "lxqt": "fusion", "mate": "fusion",