From 422aa5b4c7588c92df6049d418dc1b71fb472a14 Mon Sep 17 00:00:00 2001 From: Ad5001 Date: Sat, 26 Dec 2020 19:16:42 +0100 Subject: [PATCH] Imperfect implementation, but working sequences. --- qml/ListSetting.qml | 5 +- qml/LogGraphCanvas.qml | 11 ++--- qml/ObjectLists.qml | 1 + qml/js/mathlib.js | 65 ++++++++++++++++++++------ qml/js/objects.js | 102 +++++++++++++++++++++++++++++++++-------- qml/js/utils.js | 86 +++++++++++++++++++++++++--------- 6 files changed, 205 insertions(+), 65 deletions(-) diff --git a/qml/ListSetting.qml b/qml/ListSetting.qml index dcfa92e..1ee94a4 100644 --- a/qml/ListSetting.qml +++ b/qml/ListSetting.qml @@ -71,7 +71,7 @@ Column { if(value.toString()=="NaN") value = "" } - if(value != "" && valueInput.acceptableInput) { + if(value !== "" && valueInput.acceptableInput) { control.model.setProperty(index, 'key', value) control.changed() } @@ -111,7 +111,7 @@ Column { if(value.toString()=="NaN") value = "" } - if(value != "" && keyInput.acceptableInput) { + if(value !== "" && keyInput.acceptableInput) { control.model.setProperty(index, 'val', value) control.changed() } @@ -153,6 +153,7 @@ Column { var ret = [] for(var i = 0; i < model.count; i++) ret.push(model.get(i).val) + return ret } } } diff --git a/qml/LogGraphCanvas.qml b/qml/LogGraphCanvas.qml index 1045544..130d267 100644 --- a/qml/LogGraphCanvas.qml +++ b/qml/LogGraphCanvas.qml @@ -128,13 +128,12 @@ Canvas { drawVisibleText(ctx, "10"+Utils.textsup(xpow), x2px(Math.pow(10,xpow))-textSize/2, axisxpx+16+(6*(y==0))) } } else { - for(var x = 0; x < 40*maxgradx; x += 1) { - var drawX = x*yaxisstep1 - var txtX = yaxisstepExpr.simplify(x) + for(var x = 1; x < drawMaxX; x += 1) { + var drawX = x*xaxisstep1 + var txtX = xaxisstepExpr.simplify(x) var textSize = measureText(ctx, txtX, 6).height - if(x != 0) - drawVisibleText(ctx, txtX, x2px(drawX)-4, axisxpx+6+textSize) - drawVisibleText(ctx, '-'+txtX, x2px(-drawX)-4, axisxpx+6+textSize) + drawVisibleText(ctx, txtX, x2px(drawX)-4, axisxpx+6+textSize) + drawVisibleText(ctx, '-'+txtX, x2px(-drawX)-4, axisxpx+6+textSize) } } for(var y = 0; y < drawMaxY; y += 1) { diff --git a/qml/ObjectLists.qml b/qml/ObjectLists.qml index d792136..1657a72 100644 --- a/qml/ObjectLists.qml +++ b/qml/ObjectLists.qml @@ -359,6 +359,7 @@ ListView { onChanged: { Objects.currentObjects[objEditor.objType][objEditor.objIndex][modelData[0]] = exportModel() + Objects.currentObjects[objEditor.objType][objEditor.objIndex].update() objectListList.update() } diff --git a/qml/js/mathlib.js b/qml/js/mathlib.js index 7d8d72d..3c349e6 100644 --- a/qml/js/mathlib.js +++ b/qml/js/mathlib.js @@ -24,7 +24,6 @@ const parser = new ExprEval.Parser() var u = {1: 1, 2: 2, 3: 3} -console.log(parser.parse('u[n]+u[n+1]+u[n+2]').simplify().evaluate({"u": u, 'n': 1})) var evalVariables = { // Variables not provided by expr-eval.js, needs to be provided manualy "pi": Math.PI, @@ -45,7 +44,7 @@ class Expression { } isConstant() { - return this.expr.indexOf("x") == -1 + return !this.expr.includes("x") && !this.expr.includes("n") } execute(x = 1) { @@ -81,33 +80,69 @@ function executeExpression(expr){ class Sequence extends Expression { constructor(name, baseValues = {}, valuePlus = 1, expr = "") { // u[n+valuePlus] = expr - console.log('Expression', expr) super(expr) this.name = name this.baseValues = baseValues - this.valuePlus = valuePlus + this.calcValues = Object.assign({}, baseValues) + for(var n in this.calcValues) + if(['string', 'number'].includes(typeof this.calcValues[n])) + this.calcValues[n] = parser.parse(this.calcValues[n].toString()).simplify() + this.valuePlus = parseInt(valuePlus) } isConstant() { return this.expr.indexOf("n") == -1 } - execute(n = 0) { - if(this.cached) return this.cachedValue - if(n in this.baseValues) return this.baseValues[n] - var vars = Object.assign({'n': n-this.valuePlus}, evalVariables) - vars[this.name] = this.baseValues - var un = this.calc.evaluate(vars) - this.baseValues[n] = un - return un + execute(n = 1) { + if(n in this.calcValues) + return this.calcValues[n].evaluate(evalVariables) + this.cache(n) + return this.calcValues[n].evaluate(evalVariables) + } + + simplify(n = 1) { + if(n in this.calcValues) + return Utils.makeExpressionReadable(this.calcValues[n].toString()) + this.cache(n) + return Utils.makeExpressionReadable(this.calcValues[n].toString()) + } + + cache(n = 1) { + var str = this.calc.substitute('n', n-this.valuePlus).toString() + for(var newn in this.calcValues) { + var un = new RegExp(`${this.name}\\[${newn}\\]`, 'g') + if(un.test(str)) { + if (this.calcValues[newn] == undefined) + this.cache(newn) + str = str.replace(un, this.calcValues[newn]) + } + } + var expr = parser.parse(str).simplify() + if(expr.evaluate(evalVariables) == 0) expr = parser.parse('0') + console.log(n, expr.toString()) + expr = parser.parse(Utils.simplifyExpression(expr.toString())).simplify() + this.calcValues[n] = expr + } + + toString(forceSign=false) { + var str = Utils.makeExpressionReadable(this.calc.toString()) + if(str[0] != '-' && forceSign) str = '+' + str + var subtxt = this.valuePlus == 0 ? 'ₙ' : Utils.textsub('n+' + this.valuePlus) + var ret = `${this.name}${subtxt} = ${str}${this.baseValues.length == 0 ? '' : "\n"}` + ret += Object.keys(this.baseValues).map( + n => `${this.name}${Utils.textsub(n)} = ${this.baseValues[n]}` + ).join('; ') + return ret } } -var test = new Sequence('u', {0: 0, 1: 1}, 2, '3*u[n]+3') +var test = new Sequence('u', {0: '0', 1: 'π'}, 2, '3*u[n]') console.log(test) -for(var i=0; i<20; i++) +for(var i=0; i<20; i++) { + //console.log('u' + Utils.textsub(i) + ' = ' + test.simplify(i)) console.log('u' + Utils.textsub(i) + ' = ' + test.execute(i)) - +} // Domains class Domain { constructor() {} diff --git a/qml/js/objects.js b/qml/js/objects.js index 242f7b7..71e26e9 100644 --- a/qml/js/objects.js +++ b/qml/js/objects.js @@ -45,10 +45,10 @@ class DrawableObject { // or are instanciated by other objects. static createable() {return true} // Properties are set with key as property name and - // value as it's type name (e.g 'Expression', 'string', - // 'Point'...), an Array for enumerations, - // a List instance for lists, a Dictionary instance for - // dictionary + // value as it's type name (e.g 'Expression', 'string'...), + // an Enum for enumerations, an ObjectType for DrawableObjects + // with a specific type, a List instance for lists, a + // Dictionary instance for dictionaries... // Used for property modifier in the sidebar. static properties() {return {}} @@ -206,12 +206,16 @@ class Function extends ExecutableObject { 'comment1': 'Ex: R+* (ℝ⁺*), N* (ℕ*), Z-* (ℤ⁻*), ]0;1[, {3;4;5}', 'labelPosition': new P.Enum('above', 'below'), 'displayMode': new P.Enum('application', 'function'), - 'labelX': 'number' + 'labelX': 'number', + 'comment1': 'The following parameters are used in case of non-continuous ensembles\n(E.g: ℕ, ℤ, sets like {0;3}...)', + 'drawPoints': 'Boolean', + 'drawDashedLines': 'Boolean' }} constructor(name = null, visible = true, color = null, labelContent = 'name + value', expression = 'x', inDomain = 'RPE', outDomain = 'R', - displayMode = 'application', labelPosition = 'above', labelX = 1) { + displayMode = 'application', labelPosition = 'above', labelX = 1, + drawPoints = true, drawDashedLines = true) { if(name == null) name = getNewName('fghjqlmnopqrstuvwabcde') super(name, visible, color, labelContent) this.type = 'Function' @@ -224,6 +228,8 @@ class Function extends ExecutableObject { this.displayMode = displayMode this.labelPosition = labelPosition this.labelX = labelX + this.drawPoints = drawPoints + this.drawDashedLines = drawDashedLines } getReadableString() { @@ -237,7 +243,7 @@ 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.labelPosition, this.labelX] + this.displayMode, this.labelPosition, this.labelX, this.drawPoints, this.drawDashedLines] } execute(x = 1) { @@ -257,13 +263,13 @@ class Function extends ExecutableObject { } draw(canvas, ctx) { - Function.drawFunction(canvas, ctx, this.expression, this.inDomain, this.outDomain) + Function.drawFunction(canvas, ctx, this.expression, this.inDomain, this.outDomain, this.drawPoints, this.drawDashedLines) // Label var text = this.getLabel() ctx.font = "14px sans-serif" var textSize = canvas.measureText(ctx, text, 7) var posX = canvas.x2px(this.labelX) - var posY = canvas.y2px(this.expression.execute(this.labelX)) + var posY = canvas.y2px(this.execute(this.labelX)) switch(this.labelPosition) { case 'above': canvas.drawVisibleText(ctx, text, posX-textSize.width, posY-textSize.height) @@ -275,32 +281,39 @@ class Function extends ExecutableObject { } } - static drawFunction(canvas, ctx, expr, inDomain, outDomain) { + static drawFunction(canvas, ctx, expr, inDomain, outDomain, drawPoints = true, drawDash = true) { // Reusable in other objects. // Drawing small traits every 2px var pxprecision = 2 var previousX = canvas.px2x(0) var previousY; if(inDomain instanceof MathLib.SpecialDomain && inDomain.moveSupported) { + // Point based functions. previousX = inDomain.previous(previousX) if(previousX === null) previousX = inDomain.next(canvas.px2x(0)) previousY = expr.execute(previousX) + if(!drawPoints && !drawDash) return while(previousX !== null && canvas.x2px(previousX) < canvas.canvasSize.width) { var currentX = inDomain.next(previousX) var currentY = expr.execute(currentX) if(currentX === null) break; if((inDomain.includes(currentX) || inDomain.includes(previousX)) && (outDomain.includes(currentY) || outDomain.includes(previousY))) { - canvas.drawDashedLine(ctx, canvas.x2px(previousX), canvas.y2px(previousY), canvas.x2px(currentX), canvas.y2px(currentY)) - ctx.fillRect(canvas.x2px(previousX)-5, canvas.y2px(previousY)-1, 10, 2) - ctx.fillRect(canvas.x2px(previousX)-1, canvas.y2px(previousY)-5, 2, 10) + if(drawDash) + canvas.drawDashedLine(ctx, canvas.x2px(previousX), canvas.y2px(previousY), canvas.x2px(currentX), canvas.y2px(currentY)) + if(drawPoints) { + ctx.fillRect(canvas.x2px(previousX)-5, canvas.y2px(previousY)-1, 10, 2) + ctx.fillRect(canvas.x2px(previousX)-1, canvas.y2px(previousY)-5, 2, 10) + } } previousX = currentX previousY = currentY } - // Drawing the last cross - ctx.fillRect(canvas.x2px(previousX)-5, canvas.y2px(previousY)-1, 10, 2) - ctx.fillRect(canvas.x2px(previousX)-1, canvas.y2px(previousY)-5, 2, 10) + if(drawPoints) { + // Drawing the last cross + ctx.fillRect(canvas.x2px(previousX)-5, canvas.y2px(previousY)-1, 10, 2) + ctx.fillRect(canvas.x2px(previousX)-1, canvas.y2px(previousY)-5, 2, 10) + } } else { previousY = expr.execute(previousX) for(var px = pxprecision; px < canvas.canvasSize.width; px += pxprecision) { @@ -843,6 +856,7 @@ class CursorX extends DrawableObject { } getTargetElement() { + // TODO: Use the dependency system instead. var elementTypes = Object.keys(currentObjects).filter(objType => types[objType].prototype instanceof ExecutableObject) return getObjectByName(this.targetElement, elementTypes) } @@ -922,17 +936,65 @@ class Sequence extends ExecutableObject { static type(){return 'Sequence'} static typeMultiple(){return 'Sequences'} static properties() {return { - 'defaultExpression': new P.Dictionary('string', 'int', /^.+$/, /^(\d+)$/, '{name}[n+', '] = ', true), + 'drawPoints': 'Boolean', + 'drawDashedLines': 'Boolean', + 'defaultExpression': new P.Dictionary('string', 'int', /^.+$/, /^\d+$/, '{name}[n+', '] = ', true), 'comment1': 'Note: Use {name}[n] to refer to {name}ₙ, {name}[n+1] for {name}ₙ₊₁...', - 'markedValues': new P.Dictionary('string', 'int', /^.+$/, /^(\d+)$/, '{name}[', '] = '), + 'baseValues': new P.Dictionary('string', 'int', /^.+$/, /^\d+$/, '{name}[', '] = '), }} constructor(name = null, visible = true, color = null, labelContent = 'name + value', - defaultExp = {1: "u[n]"}, markedValues = {0: 0}) { + drawPoints = true, drawDashedLines = true, defaultExp = {1: "n"}, + baseValues = {0: 0}) { if(name == null) name = getNewName('uvwPSUVWabcde') super(name, visible, color, labelContent) + this.drawPoints = drawPoints + this.drawDashedLines = drawDashedLines this.defaultExpression = defaultExp - this.markedValues = markedValues + this.baseValues = baseValues + this.update() + } + + export() { + return [this.name, this.visible, this.color.toString(), this.labelContent, + this.drawPoints, this.drawDashedLines, this.defaultExpression, this.baseValues] + } + + update() { + super.update() + if( + this.sequence == null || this.baseValues != this.sequence.baseValues || + this.sequence.name != this.name || + this.sequence.expr != Object.values(this.defaultExpression)[0] || + this.sequence.valuePlus != Object.keys(this.defaultExpression)[0] + ) + this.sequence = new MathLib.Sequence( + this.name, this.baseValues, + Object.keys(this.defaultExpression)[0], + Object.values(this.defaultExpression)[0] + ) + } + + + getReadableString() { + return this.sequence.toString() + } + + execute(x = 1) { + if(x % 1 == 0) + return this.sequence.execute(x) + return null + } + canExecute(x = 1) {return x%1 == 0} + // Simplify returns the simplified string of the expression. + simplify(x = 1) { + if(x % 1 == 0) + return this.sequence.simplify(x) + return null + } + + draw(canvas, ctx) { + Function.drawFunction(canvas, ctx, this.sequence, canvas.logscalex ? MathLib.Domain.NE : MathLib.Domain.N, MathLib.Domain.R, this.drawPoints, this.drawDashedLines) } } diff --git a/qml/js/utils.js b/qml/js/utils.js index 7dba4e2..23dae44 100644 --- a/qml/js/utils.js +++ b/qml/js/utils.js @@ -20,6 +20,9 @@ var powerpos = { "-": "⁻", + "+": "⁺", + "=": "⁼", + " ": " ", "0": "⁰", "1": "¹", "2": "²", @@ -30,8 +33,6 @@ var powerpos = { "7": "⁷", "8": "⁸", "9": "⁹", - "+": "⁺", - "=": "⁼", "a": "ᵃ", "b": "ᵇ", "c": "ᶜ", @@ -56,11 +57,14 @@ var powerpos = { "w": "ʷ", "x": "ˣ", "y": "ʸ", - "z": "ᶻ", + "z": "ᶻ" } var indicepos = { "-": "₋", + "+": "₊", + "=": "₌", + " ": " ", "0": "₀", "1": "₁", "2": "₂", @@ -71,8 +75,6 @@ var indicepos = { "7": "₇", "8": "₈", "9": "₉", - "+": "₊", - "=": "₌", "a": "ₐ", "e": "ₑ", "h": "ₕ", @@ -122,12 +124,46 @@ function textsub(text) { function simplifyExpression(str) { var replacements = [ // Operations not done by parser. + [// Decomposition way 2 + /(^.?|[+-] |\()([-.\d\w]+) ([*/]) \((([-.\d\w] [*/] )?[-\d\w.]+) ([+\-]) (([-.\d\w] [*/] )?[\d\w.+]+)\)(.?$| [+-]|\))/g, + "$1$2 $3 $4 $6 $2 $3 $7$9" + ], + [ // Decomposition way 2 + /(^.?|[+-] |\()\((([-.\d\w] [*/] )?[-\d\w.]+) ([+\-]) (([-.\d\w] [*/] )?[\d\w.+]+)\) ([*/]) ([-.\d\w]+)(.?$| [+-]|\))/g, + "$1$8 $7 $2 $4 $8 $7 $5$9" + ], + [ // Factorisation of π elements. + /(([-\d\w.]+ [*/] )*)(pi|π)(( [/*] [-\d\w.]+)*) ([+-]) (([-\d\w.]+ [*/] )*)(pi|π)(( [/*] [-\d\w.]+)*)?/g, + function(match, m1, n1, pi1, m2, ope2, n2, opeM, m3, n3, pi2, m4, ope4, n4) { + // g1, g2, g3 , g4, g5, g6, g7, g8, g9, g10, g11,g12 , g13 + // We don't care about mx & pix, ope2 & ope4 are either / or * for n2 & n4. + // n1 & n3 are multiplied, opeM is the main operation (- or +). + // Putting all n in form of number + //n2 = n2 == undefined ? 1 : parseFloat(n) + n1 = m1 == undefined ? 1 : eval(m1 + '1') + n2 = m2 == undefined ? 1 : eval('1' + m2) + n3 = m3 == undefined ? 1 : eval(m3 + '1') + n4 = m4 == undefined ? 1 : eval('1' + m4) + //var [n1, n2, n3, n4] = [n1, n2, n3, n4].map(n => n == undefined ? 1 : parseFloat(n)) + // Falling back to * in case it does not exist (the corresponding n would be 1) + var [ope2, ope4] = [ope2, ope4].map(ope => ope == '/' ? '/' : '*') + var coeff1 = n1*n2 + var coeff2 = n3*n4 + var coefficient = coeff1+coeff2-(opeM == '-' ? 2*coeff2 : 0) + + return `${coefficient} * π` + } + ], [ // 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, + /(^.?|[*\/] |\()\(([^)(+-]+)\)(.?$| [*\/+-]|\))/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. @@ -176,26 +212,33 @@ function simplifyExpression(str) { } ], // 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'], + [/(\s|^|\()0(\.0+)? \* (\([^)(]+\))/g, '$10'], + [/(\s|^|\()0(\.0+)? \* ([^)(+-]+)/g, '$10'], + [/(\([^)(]\)) \* 0(\.0+)?(\s|$|\))/g, '0$3'], + [/([^)(+-]) \* 0(\.0+)?(\s|$|\))/g, '0$3'], + [/(\s|^|\()1(\.0+)? (\*|\/) /g, '$1'], + [/(\s|^|\()0(\.0+)? (\+|\-) /g, '$1'], + [/ (\*|\/) 1(\.0+)?(\s|$|\))/g, '$3'], + [/ (\+|\-) 0(\.0+)?(\s|$|\))/g, '$3'], [/(^| |\() /g, '$1'], [/ ($|\))/g, '$1'], ] // Replacements - replacements.forEach(function(replacement){ - while(replacement[0].test(str)) - str = str.replace(replacement[0], replacement[1]) - }) + var found + do { + found = false + for(var replacement of replacements) + while(replacement[0].test(str)) { + found = true + str = str.replace(replacement[0], replacement[1]) + } + } while(found) return str } +console.log(simplifyExpression("(4 * (4 * pi + pi)) + pi")) + function makeExpressionReadable(str) { var replacements = [ // variables @@ -209,6 +252,7 @@ function makeExpressionReadable(str) { [/\^([^ ]+)/g, function(match, p1) { return textsup(p1) }], [/_\(([^_]+)\)/g, function(match, p1) { return textsub(p1) }], [/_([^ ]+)/g, function(match, p1) { return textsub(p1) }], + [/\[([^\[\]]+)\]/g, function(match, p1) { return textsub(p1) }], [/(\d|\))×/g, '$1'], //[/×(\d|\()/g, '$1'], [/\(([^)(+.\/-]+)\)/g, "$1"], @@ -216,10 +260,9 @@ function makeExpressionReadable(str) { str = simplifyExpression(str) // Replacements - replacements.forEach(function(replacement){ + for(var replacement of replacements) while(replacement[0].test(str)) str = str.replace(replacement[0], replacement[1]) - }) return str } @@ -269,9 +312,8 @@ function parseName(str, removeUnallowed = true) { ] if(!removeUnallowed) replacements.pop() // Replacements - replacements.forEach(function(replacement){ + for(var replacement of replacements) str = str.replace(replacement[0], replacement[1]) - }) return str }