diff --git a/qml/LogGraph.qml b/qml/LogGraph.qml index 7d94c75..b18b2ac 100644 --- a/qml/LogGraph.qml +++ b/qml/LogGraph.qml @@ -147,6 +147,7 @@ ApplicationWindow { root.height = data["height"] root.width = data["width"] + Objects.currentObjects = {} Object.keys(data['objects']).forEach(function(objType){ Objects.currentObjects[objType] = [] data['objects'][objType].forEach(function(objData){ diff --git a/qml/ObjectLists.qml b/qml/ObjectLists.qml index f4f2acd..727c22d 100644 --- a/qml/ObjectLists.qml +++ b/qml/ObjectLists.qml @@ -32,7 +32,7 @@ ListView { property var listViews: {'':''} // Needs to be initialized or will be undefined -_- model: Object.keys(Objects.types) - implicitHeight: contentItem.childrenRect.height + implicitHeight: contentItem.childrenRect.height + footer.height + 10 delegate: ListView { id: objTypeList @@ -47,7 +47,7 @@ ListView { header: Text { verticalAlignment: TextInput.AlignVCenter color: sysPalette.windowText - text: objectListList.model[index] + "s:" + text: Objects.types[objType].typeMultiple() + ":" font.pixelSize: 20 visible: objTypeList.visible height: visible ? 20 : 0 @@ -64,7 +64,7 @@ ListView { checked: Objects.currentObjects[objType][index].visible anchors.verticalCenter: parent.verticalCenter onClicked: { - Objects.currentObjects[objType][index].visible = !Objects.currentObjects[objType][index].visible + Objects.currentObjects[objType][index].visible = this.checked objectListList.changed() controlRow.obj = Objects.currentObjects[objType][index] } @@ -76,6 +76,7 @@ ListView { Text { id: objDescription anchors.left: visibilityCheckBox.right + anchors.right: deleteButton.left height: parent.height verticalAlignment: TextInput.AlignVCenter text: obj.getReadableString() @@ -94,9 +95,28 @@ ListView { } } + Button { + id: deleteButton + width: parent.height - 10 + height: width + anchors.right: colorPickRect.left + anchors.rightMargin: 5 + anchors.topMargin: 5 + icon.source: './icons/delete.svg' + icon.name: 'delete' + + onClicked: { + Objects.currentObjects[objType][index].delete() + Objects.currentObjects[objType].splice(index, 1) + objectListList.update() + } + } + Rectangle { + id: colorPickRect anchors.right: parent.right anchors.rightMargin: 5 + anchors.topMargin: 5 color: obj.color width: parent.height - 10 height: width @@ -227,6 +247,19 @@ ListView { } } + CheckBox { + id: customPropCheckBox + visible: modelData[1] == 'Boolean' + width: parent.width + text: parent.label + checked: visible ? objEditor.obj[modelData[0]] : false + onClicked: { + objEditor.obj[modelData[0]] = this.checked + Objects.currentObjects[objEditor.objType][objEditor.objIndex].update() + objectListList.update() + } + } + ComboBoxSetting { id: customPropCombo height: 30 @@ -249,6 +282,8 @@ ListView { model = Objects.getObjectsName(modelData[1]).concat(['+ Create new ' + modelData[1]]) currentIndex = model.indexOf(selectedObj.name) } + Objects.currentObjects[objEditor.objType][objEditor.objIndex][modelData[0]].requiredBy = objEditor.obj[modelData[0]].filter(function(obj) {objEditor.obj.name != obj.name}) + selectedObj.requiredBy.push(Objects.currentObjects[objEditor.objType][objEditor.objIndex]) Objects.currentObjects[objEditor.objType][objEditor.objIndex][modelData[0]] = selectedObj } else { Objects.currentObjects[objEditor.objType][objEditor.objIndex][modelData[0]] = model[newIndex] diff --git a/qml/Settings.qml b/qml/Settings.qml index ecdd26a..eef4ba9 100644 --- a/qml/Settings.qml +++ b/qml/Settings.qml @@ -156,6 +156,7 @@ Column { ListElement { text: "" } ListElement { text: "y" } ListElement { text: "G (dB)" } + ListElement { text: "φ (°)" } ListElement { text: "φ (deg)" } ListElement { text: "φ (rad)" } } diff --git a/qml/icons/Point.svg b/qml/icons/Point.svg index 093f939..0022a46 100644 --- a/qml/icons/Point.svg +++ b/qml/icons/Point.svg @@ -25,16 +25,16 @@ inkscape:pageshadow="2" inkscape:zoom="22.4" inkscape:cx="23.448337" - inkscape:cy="8.0833333" + inkscape:cy="1.0201712" inkscape:document-units="px" inkscape:current-layer="layer1" inkscape:document-rotation="0" showgrid="true" - inkscape:window-width="1920" - inkscape:window-height="1011" - inkscape:window-x="1920" - inkscape:window-y="0" - inkscape:window-maximized="1"> + inkscape:window-width="1829" + inkscape:window-height="916" + inkscape:window-x="48" + inkscape:window-y="31" + inkscape:window-maximized="0"> @@ -50,7 +50,7 @@ image/svg+xml - + @@ -62,13 +62,17 @@ id="path1414" style="opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0" d="M 10.019082,18.146312 A 3.5,3.5 0 0 1 6.955752,22.00089 3.5,3.5 0 0 1 3.0690998,18.978357 3.5,3.5 0 0 1 6.0505,15.060064 3.5,3.5 0 0 1 10,18" /> - - - + A diff --git a/qml/icons/X Cursor.svg b/qml/icons/X Cursor.svg new file mode 100644 index 0000000..d49203b --- /dev/null +++ b/qml/icons/X Cursor.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + image/svg+xml + + + + + + + X + + + diff --git a/qml/js/mathlib.js b/qml/js/mathlib.js index 3ba573d..d1a06b2 100644 --- a/qml/js/mathlib.js +++ b/qml/js/mathlib.js @@ -49,7 +49,7 @@ class Expression { return this.calc.evaluate(Object.assign({'x': x}, evalVariables)) } - simplify(x = 1) { + simplify(x) { var expr = this.calc.substitute('x', x).simplify() if(expr.evaluate(evalVariables) == 0) return '0' return Utils.makeExpressionReadable(expr.toString()) @@ -69,6 +69,10 @@ class Expression { } } +function executeExpression(expr){ + return (new Expression(expr.toString())).execute() +} + // Domains class EmptySet { @@ -93,6 +97,7 @@ class Domain { } includes(x) { + if(typeof x == 'string') x = executeExpression(x) return ((this.openBegin && x > this.begin.execute()) || (!this.openBegin && x >= this.begin.execute())) && ((this.openEnd && x < this.end.execute()) || (!this.openEnd && x <= this.end.execute())) } @@ -168,10 +173,10 @@ class DomainSet { } includes(x) { - var xcomputed = new Expression(x.toString()).execute() + if(typeof x == 'string') x = executeExpression(x) var found = false this.values.forEach(function(value){ - if(xcomputed == value.execute()) { + if(x == value.execute()) { found = true return } diff --git a/qml/js/objects.js b/qml/js/objects.js index d8f246e..6653543 100644 --- a/qml/js/objects.js +++ b/qml/js/objects.js @@ -60,6 +60,11 @@ class DrawableObject { this.requiredBy = [] } + export() { + // Should return what will be input as arguments when a file is loaded (serializable form) + return [this.name, this.visible, this.color.toString(), this.labelContent] + } + getReadableString() { return `${this.name} = Unknown` } @@ -76,11 +81,19 @@ class DrawableObject { } } - export() { - return [this.name, this.visible, this.color.toString(), this.labelContent] + update() { + for(var i = 0; i < this.requiredBy.length; i++) { + this.requiredBy[i].update() + } } - update() {} + delete() { + for(var i = 0; i < this.requiredBy.length; i++) { + var toRemove = this.requiredBy[i] + toRemove.delete() + currentObjects[toRemove.type] = currentObjects[toRemove.type].filter(obj => obj.name != toRemove.name) + } + } draw(canvas, ctx) {} } @@ -103,12 +116,12 @@ class Point extends DrawableObject { static properties() {return { 'x': 'Expression', 'y': 'Expression', - 'labelPos': ['top', 'bottom', 'left', 'right'], + 'labelPosition': ['top', 'bottom', 'left', 'right'], 'pointStyle': ['●', '✕', '+'], }} constructor(name = null, visible = true, color = null, labelContent = 'name + value', - x = 1, y = 0, labelPos = 'top', pointStyle = '●') { + x = 1, y = 0, labelPosition = 'top', pointStyle = '●') { if(name == null) name = getNewName('ABCDEFJKLMNOPQRSTUVW') super(name, visible, color, labelContent) this.type = 'Point' @@ -116,7 +129,7 @@ class Point extends DrawableObject { this.x = x if(typeof y == 'number' || typeof y == 'string') y = new MathLib.Expression(y.toString()) this.y = y - this.labelPos = labelPos + this.labelPosition = labelPosition this.pointStyle = pointStyle } @@ -125,7 +138,7 @@ class Point extends DrawableObject { } export() { - return [this.name, this.visible, this.color.toString(), this.labelContent, this.x.toEditableString(), this.y.toEditableString(), this.labelPos, this.pointStyle] + return [this.name, this.visible, this.color.toString(), this.labelContent, this.x.toEditableString(), this.y.toEditableString(), this.labelPosition, this.pointStyle] } draw(canvas, ctx) { @@ -149,7 +162,7 @@ class Point extends DrawableObject { var text = this.getLabel() ctx.font = "14px sans-serif" var textSize = ctx.measureText(text).width - switch(this.labelPos) { + switch(this.labelPosition) { case 'top': canvas.drawVisibleText(ctx, text, canvasX-textSize/2, canvasY-16) break; @@ -165,14 +178,6 @@ class Point extends DrawableObject { } } - - update() { - if(currentObjects['Somme gains Bode'] != undefined && currentObjects['Gain Bode'] != undefined) { - for(var i = 0; i < currentObjects['Gain Bode'].length; i++) { - if(currentObjects['Gain Bode'][i].ω_0.name == this.name) currentObjects['Gain Bode'][i].update() - } - } - } } class Function extends ExecutableObject { @@ -182,16 +187,17 @@ class Function extends ExecutableObject { 'expression': 'Expression', 'inDomain': 'Domain', 'outDomain': 'Domain', - 'labelPos': ['above', 'below'], + 'labelPosition': ['above', 'below'], 'displayMode': ['application', 'function'], 'labelX': 'number' }} constructor(name = null, visible = true, color = null, labelContent = 'name + value', expression = 'x', inDomain = 'RPE', outDomain = 'R', - displayMode = 'application', labelPos = 'above', labelX = 1) { + displayMode = 'application', labelPosition = 'above', labelX = 1) { if(name == null) name = getNewName('fghjqlmnopqrstuvwabcde') super(name, visible, color, labelContent) + this.type = 'Function' if(typeof expression == 'number' || typeof expression == 'string') expression = new MathLib.Expression(expression.toString()) this.expression = expression if(typeof inDomain == 'string') inDomain = MathLib.parseDomain(inDomain) @@ -199,7 +205,7 @@ class Function extends ExecutableObject { if(typeof outDomain == 'string') outDomain = MathLib.parseDomain(outDomain) this.outDomain = outDomain this.displayMode = displayMode - this.labelPos = labelPos + this.labelPosition = labelPosition this.labelX = labelX } @@ -214,12 +220,12 @@ class Function extends ExecutableObject { export() { return [this.name, this.visible, this.color.toString(), this.labelContent, this.expression.toEditableString(), this.inDomain.toString(), this.outDomain.toString(), - this.displayMode, this.labelPos, this.labelX] + this.displayMode, this.labelPosition, this.labelX] } execute(x = 1) { if(this.inDomain.includes(x)) - return this.expr.execute(x) + return this.expression.execute(x) return null } @@ -229,7 +235,7 @@ class Function extends ExecutableObject { simplify(x = 1) { if(this.inDomain.includes(x)) - return this.expr.simplify(x) + return this.expression.simplify(x) return '' } @@ -241,7 +247,7 @@ class Function extends ExecutableObject { var textSize = canvas.measureText(ctx, text) var posX = canvas.x2px(this.labelX) var posY = canvas.y2px(this.expression.execute(this.labelX)) - switch(this.labelPos) { + switch(this.labelPosition) { case 'above': canvas.drawVisibleText(ctx, text, posX-textSize.width/2, posY-textSize.height) break; @@ -279,15 +285,16 @@ class GainBode extends ExecutableObject { 'ω_0': 'Point', 'pass': ['high', 'low'], 'gain': 'Expression', - 'labelPos': ['above', 'below'], + 'labelPosition': ['above', 'below'], 'labelX': 'number' }} constructor(name = null, visible = true, color = null, labelContent = 'name + value', - ω_0 = '', pass = 'high', gain = '20', labelPos = 'above', labelX = 1) { + ω_0 = '', pass = 'high', gain = '20', labelPosition = 'above', labelX = 1) { if(name == null) name = getNewName('G') - if(name == 'G') name = 'G₀' // G is reserved for sum of BODE magnitues (Somme gains Bode). + if(name == 'G') name = 'G₀' // G is reserved for sum of BODE magnitudes (Somme gains Bode). super(name, visible, color, labelContent) + this.type = 'Gain Bode' if(typeof ω_0 == "string") { // Point name or create one ω_0 = getObjectByName(ω_0, 'Point') @@ -296,14 +303,15 @@ class GainBode extends ExecutableObject { ω_0 = createNewRegisteredObject('Point') ω_0.name = getNewName('ω') ω_0.color = this.color - labelPos = 'below' + labelPosition = 'below' } + ω_0.requiredBy.push(this) } this.ω_0 = ω_0 this.pass = pass if(typeof gain == 'number' || typeof gain == 'string') gain = new MathLib.Expression(gain.toString()) this.gain = gain - this.labelPos = labelPos + this.labelPosition = labelPosition this.labelX = labelX } @@ -313,7 +321,7 @@ class GainBode extends ExecutableObject { export() { return [this.name, this.visible, this.color.toString(), this.labelContent, - this.ω_0.name, this.pass.toString(), this.gain.toEditableString(), this.labelPos, this.labelX] + this.ω_0.name, this.pass.toString(), this.gain.toEditableString(), this.labelPosition, this.labelX] } execute(x=1) { @@ -360,7 +368,7 @@ class GainBode extends ExecutableObject { var textSize = canvas.measureText(ctx, text) var posX = canvas.x2px(this.labelX) var posY = canvas.y2px(this.execute(this.labelX)) - switch(this.labelPos) { + switch(this.labelPosition) { case 'above': canvas.drawVisibleText(ctx, text, posX-textSize.width/2, posY-textSize.height) break; @@ -384,21 +392,21 @@ class SommeGainsBode extends DrawableObject { static typeMultiple(){return 'Somme gains Bode'} static createable() {return false} static properties() {return { - 'labelPos': ['above', 'below'], + 'labelPosition': ['above', 'below'], 'labelX': 'number' }} constructor(name = null, visible = true, color = null, labelContent = 'name + value', - labelPos = 'above', labelX = 1) { + labelPosition = 'above', labelX = 1) { if(name == null) name = 'G' super(name, visible, color, labelContent) - this.labelPos = labelPos + this.labelPosition = labelPosition this.labelX = labelX this.recalculateCache() } export() { - return [this.name, this.visible, this.color.toString(), this.labelContent, this.labelPos, this.labelX] + return [this.name, this.visible, this.color.toString(), this.labelContent, this.labelPosition, this.labelX] } getReadableString() { @@ -493,7 +501,7 @@ class SommeGainsBode extends DrawableObject { var textSize = canvas.measureText(ctx, text) var posX = canvas.x2px(this.labelX) var posY = canvas.y2px(dbfn.execute(this.labelX)) - switch(this.labelPos) { + switch(this.labelPosition) { case 'above': canvas.drawVisibleText(ctx, text, posX-textSize.width/2, posY-textSize.height) break; @@ -508,12 +516,55 @@ class SommeGainsBode extends DrawableObject { } class PhaseBode extends ExecutableObject { + static type(){return 'Phase Bode'} + static typeMultiple(){return 'Phases Bode'} + static properties() {return { + 'ω_0': 'Point', + 'phase': 'Expression', + 'unit': ['°', 'deg', 'rad'], + 'labelPosition': ['above', 'below'], + 'labelX': 'number' + }} + constructor(name = null, visible = true, color = null, labelContent = 'name + value', + ω_0 = '', phase = 90, unit = '°', labelPosition = 'above', labelX = 1) { + if(name == null) name = getNewName('φ') + if(name == 'φ') name = 'φ₀' // φ is reserved for sum of BODE phases (Somme phases Bode). + super(name, visible, color, labelContent) + this.type = 'Phase Bode' + if(typeof ω_0 == "string") { + // Point name or create one + ω_0 = getObjectByName(ω_0, 'Point') + if(ω_0 == null) { + // Create new point + ω_0 = createNewRegisteredObject('Point') + ω_0.name = getNewName('ω') + ω_0.color = this.color + labelPosition = 'below' + } + ω_0.requiredBy.push(this) + } + this.ω_0 = ω_0 + if(typeof phase == 'number' || typeof phase == 'string') phase = new MathLib.Expression(phase.toString()) + this.phase = phase + this.unit = unit + this.labelPosition = labelPosition + this.labelX = labelX + } + + export() { + return [this.name, this.visible, this.color.toString(), this.labelContent, + this.ω_0.name, this.phase.toEditableString(), this.unit, this.labelPosition, this.labelX] + } + + getReadableString() { + return `${this.name}: ${this.phase.toString(true)}${this.unit} at ω₀ = ${this.ω_0.x}\n` + } } class CursorX extends DrawableObject { - static type(){return 'CursorX'} - static typeMultiple(){return 'CursorX'} + static type(){return 'X Cursor'} + static typeMultiple(){return 'X Cursors'} static properties() { var elementTypes = Object.keys(currentObjects).filter(objType => types[objType].prototype instanceof ExecutableObject) var elementNames = [''] @@ -522,33 +573,134 @@ class CursorX extends DrawableObject { }) return { 'x': 'Expression', - 'element': elementNames, - 'labelPos': ['left', 'right'], + 'targetElement': elementNames, + 'labelPosition': ['left', 'right'], + 'approximate': 'Boolean', + 'rounding': 'number', 'displayStyle': [ - '⸻⸻⸻', - '— — — — —', - '• • • • •' - ] + '— — — — — — —', + '⸺⸺⸺⸺⸺⸺', + '• • • • • • • • • •' + ], + 'targetValuePosition' : ['Next to target', 'With label', 'Hidden'] } } constructor(name = null, visible = true, color = null, labelContent = 'name + value', - x = 1, element = null, labelPos = 'left', displayStyle = '⸻⸻⸻') { + x = 1, targetElement = null, labelPosition = 'left', approximate = true, + rounding = 3, displayStyle = '— — — — — — —', targetValuePosition = 'Next to target') { if(name == null) name = getNewName('X') super(name, visible, color, labelContent) - this.type = 'CursorX' + this.type = 'X Cursor' + this.approximate = approximate + this.rounding = rounding if(typeof x == 'number' || typeof x == 'string') x = new MathLib.Expression(x.toString()) this.x = x var elementTypes = Object.keys(currentObjects).filter(objType => types[objType].prototype instanceof ExecutableObject) - this.element = getObjectByName(this.element, elementTypes) - this.labelPos = labelPos + this.targetElement = getObjectByName(this.targetElement, elementTypes) + this.labelPosition = labelPosition this.displayStyle = displayStyle + this.targetValuePosition = targetValuePosition + } + + export() { + return [this.name, this.visible, this.color.toString(), this.labelContent, + this.x.toEditableString(), this.targetElement.name, this.labelPosition, + this.approximate, this.rounding, this.displayStyle, this.targetValuePosition] + } + + getReadableString() { + if(this.targetElement == null) return `${this.name} = ${this.x.toString()}` + return `${this.name} = ${this.x.toString()}\n${this.getTargetValueLabel()}` + } + + getTargetValueLabel() { + var t = this.targetElement + var approx = '' + if(this.approximate) { + approx = t.execute(this.x.execute()) + approx = approx.toPrecision(this.rounding + Math.round(approx).toString().length) + } + return `${t.name}(${this.name}) = ${t.simplify(this.x.toEditableString())}` + + (this.approximate ? ' ≃ ' + approx : '') + } + + getLabel() { + switch(this.labelContent) { + case 'name': + return this.name + break; + case 'name + value': + switch(this.targetValuePosition) { + case 'Next to target': + case 'Hidden': + return `${this.name} = ${this.x.toString()}` + break; + case 'With label': + return this.getReadableString() + break; + } + case 'null': + return '' + } + } + + draw(canvas, ctx) { + var xpos = canvas.x2px(this.x.execute()) + switch(this.displayStyle) { + case '— — — — — — —': + var dashPxSize = 10 + for(var i = 0; i < canvas.canvasSize.height; i += dashPxSize*2) + canvas.drawLine(ctx, xpos, i, xpos, i+dashPxSize) + break; + case '⸺⸺⸺⸺⸺⸺': + canvas.drawXLine(ctx, this.x.execute()) + break; + case '• • • • • • • • • •': + var pointDistancePx = 10 + var pointSize = 2 + ctx.beginPath(); + for(var i = 0; i < canvas.canvasSize.height; i += pointDistancePx) + ctx.ellipse(xpos-pointSize/2, i-pointSize/2, pointSize, pointSize) + ctx.fill(); + break; + } + + // Label + var text = this.getLabel() + ctx.font = "14px sans-serif" + var textSize = canvas.measureText(ctx, text, 7) + + switch(this.labelPosition) { + case 'left': + canvas.drawVisibleText(ctx, text, xpos-textSize.width-5, textSize.height+5) + break; + case 'right': + canvas.drawVisibleText(ctx, text, xpos+5, textSize.height+5) + break; + } + + if(this.targetValuePosition == 'Next to target' && this.targetElement != null) { + var text = this.getTargetValueLabel() + var textSize = canvas.measureText(ctx, text, 7) + var ypox = canvas.y2px(this.targetElement.execute(this.x.execute())) + switch(this.labelPosition) { + case 'left': + canvas.drawVisibleText(ctx, text, xpos-textSize.width-5, ypox+textSize.height) + break; + case 'right': + canvas.drawVisibleText(ctx, text, xpos+5, ypox.textSize.height) + break; + } + } + } update() { - if(typeof this.element == 'string') + if(typeof this.targetElement == 'string') { var elementTypes = Object.keys(currentObjects).filter(objType => types[objType].prototype instanceof ExecutableObject) - this.element = getObjectByName(this.element, elementTypes) + this.targetElement = getObjectByName(this.targetElement, elementTypes) + } } } @@ -557,7 +709,8 @@ const types = { 'Function': Function, 'Gain Bode': GainBode, 'Somme gains Bode': SommeGainsBode, - 'CursorX': CursorX + 'Phase Bode': PhaseBode, + 'X Cursor': CursorX } var currentObjects = {} diff --git a/qml/js/utils.js b/qml/js/utils.js index 16ec3ad..dc7cc4a 100644 --- a/qml/js/utils.js +++ b/qml/js/utils.js @@ -285,7 +285,7 @@ function getRandomColor() { var clrs = '0123456789ABCDEF'; var color = '#'; for (var i = 0; i < 6; i++) { - color += clrs[Math.floor(Math.random() * (16-6*(i%2==0)))]; + color += clrs[Math.floor(Math.random() * (16-5*(i%2==0)))]; } return color; }