Reorganizing paths
This commit is contained in:
parent
e9d204daab
commit
34cb856dd4
249 changed files with 118 additions and 294 deletions
25
common/.mocharc.jsonc
Normal file
25
common/.mocharc.jsonc
Normal file
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
{
|
||||
"recursive": true,
|
||||
"require": [
|
||||
"esm",
|
||||
"./tests/js/hooks.mjs"
|
||||
]
|
||||
}
|
6
common/babel.config.json
Normal file
6
common/babel.config.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"presets": ["@babel/preset-env"],
|
||||
"targets": {
|
||||
"esmodules": true
|
||||
}
|
||||
}
|
4440
common/package-lock.json
generated
Normal file
4440
common/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
33
common/package.json
Normal file
33
common/package.json
Normal file
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"name": "logarithmplotter",
|
||||
"version": "0.6.0",
|
||||
"description": "2D plotter software to make Bode plots, sequences and distribution functions.",
|
||||
"main": "LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/autoload.mjs",
|
||||
"scripts": {
|
||||
"build": "rollup --config rollup.config.mjs",
|
||||
"test": "c8 mocha test/**/*.mjs"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.ad5001.eu/Ad5001/LogarithmPlotter"
|
||||
},
|
||||
"author": "Ad5001 <mail@ad5001.eu>",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@rollup/plugin-babel": "^6.0.4",
|
||||
"@rollup/plugin-commonjs": "^28.0.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"c8": "^10.1.2",
|
||||
"rollup": "^4.22.4",
|
||||
"rollup-plugin-cleanup": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^5.0.0",
|
||||
"@types/mocha": "^10.0.8",
|
||||
"chai": "^5.1.1",
|
||||
"chai-as-promised": "^8.0.0",
|
||||
"esm": "^3.2.25",
|
||||
"mocha": "^10.7.3"
|
||||
}
|
||||
}
|
44
common/rollup.config.mjs
Normal file
44
common/rollup.config.mjs
Normal file
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { nodeResolve } from "@rollup/plugin-node-resolve"
|
||||
import commonjs from "@rollup/plugin-commonjs"
|
||||
import { babel } from "@rollup/plugin-babel"
|
||||
import cleanup from "rollup-plugin-cleanup"
|
||||
|
||||
const src = "./src/index.mjs"
|
||||
const dest = "../build/runtime-pyside6/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/index.mjs"
|
||||
|
||||
export default {
|
||||
input: src,
|
||||
output: {
|
||||
file: dest,
|
||||
compact: false,
|
||||
sourcemap: true,
|
||||
format: "es"
|
||||
},
|
||||
plugins: [
|
||||
nodeResolve({ browser: true }),
|
||||
commonjs(),
|
||||
cleanup({ comments: 'some' }),
|
||||
babel({
|
||||
babelHelpers: "bundled"
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
64
common/src/history/color.mjs
Normal file
64
common/src/history/color.mjs
Normal file
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import EditedProperty from "./editproperty.mjs"
|
||||
import Objects from "../module/objects.mjs"
|
||||
|
||||
export default class ColorChanged extends EditedProperty {
|
||||
// Action used everytime when an object's color is changed
|
||||
type() {
|
||||
return "ColorChanged"
|
||||
}
|
||||
|
||||
icon() {
|
||||
return "appearance"
|
||||
}
|
||||
|
||||
constructor(targetName = "", targetType = "Point", oldColor = "black", newColor = "white") {
|
||||
super(targetName, targetType, "color", oldColor, newColor)
|
||||
}
|
||||
|
||||
export() {
|
||||
return [this.targetName, this.targetType, this.previousValue, this.newValue]
|
||||
}
|
||||
|
||||
color(darkVer = false) {
|
||||
return darkVer ? "purple" : "plum"
|
||||
}
|
||||
|
||||
getReadableString() {
|
||||
return qsTranslate("color", "%1 %2's color changed from %3 to %4.")
|
||||
.arg(Objects.types[this.targetType].displayType()).arg(this.targetName)
|
||||
.arg(this.previousValue).arg(this.newValue)
|
||||
}
|
||||
|
||||
formatColor(color) {
|
||||
return `<span style="color: ${color}; font-family: monospace; padding: 2px;">██</span>`
|
||||
}
|
||||
|
||||
getHTMLString() {
|
||||
return qsTranslate("color", "%1 %2's color changed from %3 to %4.")
|
||||
.arg(Objects.types[this.targetType].displayType())
|
||||
.arg("<b style=\"font-size: 15px;\"> " + this.targetName + " </b>")
|
||||
.arg(this.formatColor(this.previousValue)).arg(this.formatColor(this.newValue))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
114
common/src/history/common.mjs
Normal file
114
common/src/history/common.mjs
Normal file
|
@ -0,0 +1,114 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import History from "../module/history.mjs"
|
||||
import Latex from "../module/latex.mjs"
|
||||
|
||||
export class Action {
|
||||
/**
|
||||
* Type of the action.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
type() {
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
/**
|
||||
* Icon associated with the action.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
icon() {
|
||||
return "position"
|
||||
}
|
||||
|
||||
// TargetName is the name of the object that's targeted by the event.
|
||||
constructor(targetName = "", targetType = "Point") {
|
||||
this.targetName = targetName
|
||||
this.targetType = targetType
|
||||
}
|
||||
|
||||
/**
|
||||
* Undoes the action.
|
||||
*/
|
||||
undo() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Redoes the action.
|
||||
*/
|
||||
redo() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Export the action to a serializable format.
|
||||
* NOTE: These arguments will be reinputed in the constructor in this order.
|
||||
*
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export() {
|
||||
return [this.targetName, this.targetType]
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string with the human-readable description of the action.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
getReadableString() {
|
||||
return "Unknown action"
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string containing an HTML tag describing the icon of a type
|
||||
*
|
||||
* @param {string} type - Name of the icon to put in rich text.
|
||||
* @returns {string}
|
||||
*/
|
||||
getIconRichText(type) {
|
||||
return `<img src="../icons/objects/${type}.svg" style="color: ${History.themeTextColor};" width=18 height=18></img>`
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a LaTeX-formatted string to an image and wraps it in an HTML tag in a string.
|
||||
*
|
||||
* @param {string} latexString - Source string of the latex.
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async renderLatexAsHtml(latexString) {
|
||||
if(!Latex.enabled)
|
||||
throw new Error("Cannot render an item as LaTeX when LaTeX is disabled.")
|
||||
const imgDepth = History.imageDepth
|
||||
const { source, width, height } = await Latex.requestAsyncRender(
|
||||
latexString,
|
||||
imgDepth * (History.fontSize + 2),
|
||||
History.themeTextColor
|
||||
)
|
||||
return `<img src="${source}" width="${width / imgDepth}" height="${height / imgDepth}" style="vertical-align: middle"/>`
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string with the HTML-formatted description of the action.
|
||||
*
|
||||
* @returns {string|Promise<string>}
|
||||
*/
|
||||
getHTMLString() {
|
||||
return this.getReadableString()
|
||||
}
|
||||
}
|
69
common/src/history/create.mjs
Normal file
69
common/src/history/create.mjs
Normal file
|
@ -0,0 +1,69 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Objects from "../module/objects.mjs"
|
||||
import { Action } from "./common.mjs"
|
||||
|
||||
/**
|
||||
* Action used for the creation of an object.
|
||||
*/
|
||||
export default class CreateNewObject extends Action {
|
||||
type() {
|
||||
return "CreateNewObject"
|
||||
}
|
||||
|
||||
icon() {
|
||||
return "create"
|
||||
}
|
||||
|
||||
color(darkVer = false) {
|
||||
return darkVer ? "green" : "lime"
|
||||
}
|
||||
|
||||
constructor(targetName = "", targetType = "Point", properties = []) {
|
||||
super(targetName, targetType)
|
||||
this.targetProperties = properties
|
||||
}
|
||||
|
||||
|
||||
undo() {
|
||||
Objects.deleteObject(this.targetName)
|
||||
}
|
||||
|
||||
redo() {
|
||||
Objects.createNewRegisteredObject(this.targetType, this.targetProperties)
|
||||
Objects.currentObjectsByName[this.targetName].update()
|
||||
}
|
||||
|
||||
export() {
|
||||
return [this.targetName, this.targetType, this.targetProperties]
|
||||
}
|
||||
|
||||
getReadableString() {
|
||||
return qsTranslate("create", "New %1 %2 created.")
|
||||
.arg(Objects.types[this.targetType].displayType())
|
||||
.arg(this.targetName)
|
||||
}
|
||||
|
||||
getHTMLString() {
|
||||
return qsTranslate("create", "New %1 %2 created.")
|
||||
.arg(Objects.types[this.targetType].displayType())
|
||||
.arg("<b style=\"font-size: 15px;\">" + this.targetName + "</b>")
|
||||
}
|
||||
}
|
||||
|
52
common/src/history/delete.mjs
Normal file
52
common/src/history/delete.mjs
Normal file
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Objects from "../module/objects.mjs"
|
||||
import CreateNewObject from "./create.mjs"
|
||||
|
||||
|
||||
/**
|
||||
* Action used at the deletion of an object. Basically the same thing as creating a new object, except Redo & Undo are reversed.
|
||||
*/
|
||||
export default class DeleteObject extends CreateNewObject {
|
||||
type(){return 'DeleteObject'}
|
||||
|
||||
icon(){return 'delete'}
|
||||
|
||||
color(darkVer=false){return darkVer ? 'darkred' : 'salmon'}
|
||||
|
||||
undo() {
|
||||
super.redo()
|
||||
}
|
||||
|
||||
redo() {
|
||||
super.undo()
|
||||
}
|
||||
|
||||
getReadableString() {
|
||||
return qsTranslate("delete", "%1 %2 deleted.")
|
||||
.arg(Objects.types[this.targetType].displayType())
|
||||
.arg(this.targetName)
|
||||
}
|
||||
|
||||
getHTMLString() {
|
||||
return qsTranslate("delete", "%1 %2 deleted.")
|
||||
.arg(Objects.types[this.targetType].displayType())
|
||||
.arg('<b style="font-size: 15px;">' + this.targetName + "</b>")
|
||||
}
|
||||
}
|
159
common/src/history/editproperty.mjs
Normal file
159
common/src/history/editproperty.mjs
Normal file
|
@ -0,0 +1,159 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Objects from "../module/objects.mjs"
|
||||
import Latex from "../module/latex.mjs"
|
||||
import * as MathLib from "../math/index.mjs"
|
||||
import { Action } from "./common.mjs"
|
||||
import { DrawableObject } from "../objs/common.mjs"
|
||||
|
||||
/**
|
||||
* Action used everytime an object's property has been changed.
|
||||
*/
|
||||
export default class EditedProperty extends Action {
|
||||
type() {
|
||||
return "EditedProperty"
|
||||
}
|
||||
|
||||
icon() {
|
||||
return "modify"
|
||||
}
|
||||
|
||||
color(darkVer = false) {
|
||||
return darkVer ? "darkslateblue" : "cyan"
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} targetName - Name of the object to target
|
||||
* @param {string} targetType - Type of the object to target.
|
||||
* @param {string} targetProperty - Property being changed
|
||||
* @param {any} previousValue - Previous value before change
|
||||
* @param {any} newValue - New value after change
|
||||
* @param {boolean} valueIsExpressionNeedingImport - True if the value needs to be imported. (e.g expressions)
|
||||
*/
|
||||
constructor(targetName = "", targetType = "Point", targetProperty = "visible", previousValue = false, newValue = true, valueIsExpressionNeedingImport = false) {
|
||||
super(targetName, targetType)
|
||||
this.targetProperty = targetProperty
|
||||
this.targetPropertyReadable = qsTranslate("prop", this.targetProperty)
|
||||
this.previousValue = previousValue
|
||||
this.newValue = newValue
|
||||
this.propertyType = Objects.types[targetType].properties()[targetProperty]
|
||||
if(valueIsExpressionNeedingImport) {
|
||||
if(typeof this.propertyType == "object" && this.propertyType.type === "Expression") {
|
||||
this.previousValue = new MathLib.Expression(this.previousValue)
|
||||
this.newValue = new MathLib.Expression(this.newValue)
|
||||
} else if(this.propertyType === "Domain") {
|
||||
this.previousValue = MathLib.parseDomain(this.previousValue)
|
||||
this.newValue = MathLib.parseDomain(this.newValue)
|
||||
} else {
|
||||
// Objects
|
||||
this.previousValue = Objects.currentObjectsByName[this.previousValue] // Objects.getObjectByName(this.previousValue);
|
||||
this.newValue = Objects.currentObjectsByName[this.newValue] // Objects.getObjectByName(this.newValue);
|
||||
}
|
||||
}
|
||||
this.setReadableValues()
|
||||
}
|
||||
|
||||
undo() {
|
||||
Objects.currentObjectsByName[this.targetName][this.targetProperty] = this.previousValue
|
||||
Objects.currentObjectsByName[this.targetName].update()
|
||||
}
|
||||
|
||||
redo() {
|
||||
Objects.currentObjectsByName[this.targetName][this.targetProperty] = this.newValue
|
||||
Objects.currentObjectsByName[this.targetName].update()
|
||||
}
|
||||
|
||||
export() {
|
||||
if(this.previousValue instanceof MathLib.Expression) {
|
||||
return [this.targetName, this.targetType, this.targetProperty, this.previousValue.toEditableString(), this.newValue.toEditableString(), true]
|
||||
} else if(this.previousValue instanceof DrawableObject) {
|
||||
return [this.targetName, this.targetType, this.targetProperty, this.previousValue.name, this.newValue.name, true]
|
||||
} else {
|
||||
return [this.targetName, this.targetType, this.targetProperty, this.previousValue, this.newValue, false]
|
||||
}
|
||||
}
|
||||
|
||||
setReadableValues() {
|
||||
this.prevString = ""
|
||||
this.nextString = ""
|
||||
this._renderPromises = []
|
||||
if(this.propertyType instanceof Object) {
|
||||
switch(this.propertyType.type) {
|
||||
case "Enum":
|
||||
this.prevString = this.propertyType.translatedValues[this.propertyType.values.indexOf(this.previousValue)]
|
||||
this.nextString = this.propertyType.translatedValues[this.propertyType.values.indexOf(this.newValue)]
|
||||
break
|
||||
case "ObjectType":
|
||||
this.prevString = this.previousValue == null ? "null" : this.previousValue.name
|
||||
this.nextString = this.newValue == null ? "null" : this.newValue.name
|
||||
break
|
||||
case "List":
|
||||
this.prevString = this.previousValue.join(",")
|
||||
this.nextString = this.newValue.name.join(",")
|
||||
break
|
||||
case "Dict":
|
||||
this.prevString = JSON.stringify(this.previousValue)
|
||||
this.nextString = JSON.stringify(this.newValue)
|
||||
break
|
||||
case "Expression":
|
||||
this.prevString = this.previousValue == null ? "null" : this.previousValue.toString()
|
||||
this.nextString = this.newValue == null ? "null" : this.newValue.toString()
|
||||
break
|
||||
}
|
||||
} else {
|
||||
this.prevString = this.previousValue == null ? "null" : this.previousValue.toString()
|
||||
this.nextString = this.newValue == null ? "null" : this.newValue.toString()
|
||||
}
|
||||
// HTML
|
||||
this.prevHTML = "<tt style=\"background: rgba(128,128,128,0.1);\"> " + this.prevString + " </tt>"
|
||||
this.nextHTML = "<tt style=\"background: rgba(128,128,128,0.1);\"> " + this.nextString + " </tt>"
|
||||
if(Latex.enabled && typeof this.propertyType == "object" && this.propertyType.type === "Expression") {
|
||||
// Store promises so that querying can wait for them to finish.
|
||||
this._renderPromises = [
|
||||
this.renderLatexAsHtml(this.previousValue.latexMarkup).then(prev => this.prevHTML = prev),
|
||||
this.renderLatexAsHtml(this.newValue.latexMarkup).then(next => this.nextHTML = next)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
getReadableString() {
|
||||
return qsTranslate("editproperty", "%1 of %2 %3 changed from \"%4\" to \"%5\".")
|
||||
.arg(this.targetPropertyReadable)
|
||||
.arg(Objects.types[this.targetType].displayType())
|
||||
.arg(this.targetName).arg(this.prevString).arg(this.nextString)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return {Promise<string>|string}
|
||||
*/
|
||||
async getHTMLString() {
|
||||
const translation = qsTranslate("editproperty", "%1 of %2 changed from %3 to %4.")
|
||||
.arg(this.targetPropertyReadable)
|
||||
.arg("<b style=\"font-size: 15px;\"> " + this.targetName + " </b>")
|
||||
// Check if we need to wait for LaTeX HTML to be rendered.
|
||||
if(this.prevHTML === undefined || this.nextHTML === undefined) {
|
||||
const [prevHTML, nextHTML] = await Promise.all(this._renderPromises)
|
||||
this.prevHTML = this.prevHTML ?? prevHTML
|
||||
this.nextHTML = this.nextHTML ?? nextHTML
|
||||
}
|
||||
return translation.arg(this.prevHTML).arg(this.nextHTML)
|
||||
}
|
||||
}
|
50
common/src/history/index.mjs
Normal file
50
common/src/history/index.mjs
Normal file
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// This library helps containing actions to be undone or redone (in other words, editing history)
|
||||
// Each type of event is repertoried as an action that can be listed for everything that's undoable.
|
||||
|
||||
import { Action as A } from "./common.mjs"
|
||||
import Create from "./create.mjs"
|
||||
import Delete from "./delete.mjs"
|
||||
import EP from "./editproperty.mjs"
|
||||
import Pos from "./position.mjs"
|
||||
import V from "./visibility.mjs"
|
||||
import Name from "./name.mjs"
|
||||
import Color from "./color.mjs"
|
||||
|
||||
|
||||
export const Action = A
|
||||
export const CreateNewObject = Create
|
||||
export const DeleteObject = Delete
|
||||
export const EditedProperty = EP
|
||||
export const EditedPosition = Pos
|
||||
export const EditedVisibility = V
|
||||
export const NameChanged = Name
|
||||
export const ColorChanged = Color
|
||||
|
||||
export const Actions = {
|
||||
"Action": Action,
|
||||
"CreateNewObject": CreateNewObject,
|
||||
"DeleteObject": DeleteObject,
|
||||
"EditedProperty": EditedProperty,
|
||||
"EditedPosition": EditedPosition,
|
||||
"EditedVisibility": EditedVisibility,
|
||||
"NameChanged": NameChanged,
|
||||
"ColorChanged": ColorChanged,
|
||||
}
|
61
common/src/history/name.mjs
Normal file
61
common/src/history/name.mjs
Normal file
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import EditedProperty from "./editproperty.mjs"
|
||||
import Objects from "../module/objects.mjs"
|
||||
|
||||
/**
|
||||
* Action used everytime an object's name has been changed.
|
||||
*/
|
||||
export default class NameChanged extends EditedProperty {
|
||||
type(){return 'NameChanged'}
|
||||
|
||||
icon(){return 'name'}
|
||||
|
||||
color(darkVer=false){return darkVer ? 'darkorange' : 'orange'}
|
||||
|
||||
constructor(targetName = "", targetType = "Point", newName = "") {
|
||||
super(targetName, targetType, "name", targetName, newName)
|
||||
}
|
||||
|
||||
export() {
|
||||
return [this.targetName, this.targetType, this.newValue]
|
||||
}
|
||||
|
||||
undo() {
|
||||
Objects.renameObject(this.newValue, this.previousValue)
|
||||
}
|
||||
|
||||
redo() {
|
||||
Objects.renameObject(this.previousValue, this.newValue)
|
||||
}
|
||||
|
||||
getReadableString() {
|
||||
return qsTranslate("name", '%1 %2 renamed to %3.')
|
||||
.arg(Objects.types[this.targetType].displayType())
|
||||
.arg(this.targetName).arg(this.newValue)
|
||||
}
|
||||
|
||||
getHTMLString() {
|
||||
return qsTranslate("name", '%1 %2 renamed to %3.')
|
||||
.arg(Objects.types[this.targetType].displayType())
|
||||
.arg('<b style="font-size: 15px;">' + this.targetName + "</b>").arg('<b>'+this.newValue+'</b>')
|
||||
}
|
||||
}
|
||||
|
||||
|
109
common/src/history/position.mjs
Normal file
109
common/src/history/position.mjs
Normal file
|
@ -0,0 +1,109 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Objects from "../module/objects.mjs"
|
||||
import Latex from "../module/latex.mjs"
|
||||
import * as MathLib from "../math/index.mjs"
|
||||
import { escapeHTML } from "../utils.mjs"
|
||||
import { Action } from "./common.mjs"
|
||||
|
||||
/**
|
||||
* Action used for objects that have a X and Y expression properties (points, texts...)
|
||||
*/
|
||||
export default class EditedPosition extends Action {
|
||||
type() {
|
||||
return "EditedPosition"
|
||||
}
|
||||
|
||||
icon() {
|
||||
return "position"
|
||||
}
|
||||
|
||||
color(darkVer = false) {
|
||||
return darkVer ? "seagreen" : "lightseagreen"
|
||||
}
|
||||
|
||||
constructor(targetName = "", targetType = "Point", previousXValue = "", newXValue = "", previousYValue = "", newYValue = "") {
|
||||
super(targetName, targetType)
|
||||
let imports = {
|
||||
"previousXValue": previousXValue,
|
||||
"previousYValue": previousYValue,
|
||||
"newXValue": newXValue,
|
||||
"newYValue": newYValue
|
||||
}
|
||||
for(let name in imports)
|
||||
this[name] = (typeof imports[name]) == "string" ? new MathLib.Expression(imports[name]) : imports[name]
|
||||
this.setReadableValues()
|
||||
}
|
||||
|
||||
undo() {
|
||||
Objects.currentObjectsByName[this.targetName].x = this.previousXValue
|
||||
Objects.currentObjectsByName[this.targetName].y = this.previousYValue
|
||||
Objects.currentObjectsByName[this.targetName].update()
|
||||
}
|
||||
|
||||
redo() {
|
||||
Objects.currentObjectsByName[this.targetName].x = this.newXValue
|
||||
Objects.currentObjectsByName[this.targetName].y = this.newYValue
|
||||
Objects.currentObjectsByName[this.targetName].update()
|
||||
}
|
||||
|
||||
setReadableValues() {
|
||||
this.prevString = `(${this.previousXValue.toString()},${this.previousYValue.toString()})`
|
||||
this.nextString = `(${this.newXValue.toString()},${this.newYValue.toString()})`
|
||||
this._renderPromises = []
|
||||
// Render as LaTeX
|
||||
if(Latex.enabled) {
|
||||
const prevMarkup = `\\left(${this.previousXValue.latexMarkup},${this.previousYValue.latexMarkup}\\right)`
|
||||
const nextMarkup = `\\left(${this.newXValue.latexMarkup},${this.newYValue.latexMarkup}\\right)`
|
||||
this._renderPromises = [ // Will be taken in promise.all
|
||||
this.renderLatexAsHtml(prevMarkup),
|
||||
this.renderLatexAsHtml(nextMarkup)
|
||||
]
|
||||
} else {
|
||||
this.prevHTML = "<tt style=\"background: rgba(128,128,128,0.1);\"> " + escapeHTML(this.prevString) + " </tt>"
|
||||
this.nextHTML = "<tt style=\"background: rgba(128,128,128,0.1);\"> " + escapeHTML(this.nextString) + " </tt>"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export() {
|
||||
return [this.targetName, this.targetType,
|
||||
this.previousXValue.toEditableString(), this.newXValue.toEditableString(),
|
||||
this.previousYValue.toEditableString(), this.newYValue.toEditableString()]
|
||||
}
|
||||
|
||||
getReadableString() {
|
||||
return qsTranslate("position", "Position of %1 %2 set from \"%3\" to \"%4\".")
|
||||
.arg(Objects.types[this.targetType].displayType())
|
||||
.arg(this.targetName).arg(this.prevString).arg(this.nextString)
|
||||
}
|
||||
|
||||
async getHTMLString() {
|
||||
const translation = qsTranslate("position", "Position of %1 set from %2 to %3.")
|
||||
.arg("<b style=\"font-size: 15px;\"> " + this.targetName + " </b>")
|
||||
// Check if we need to wait for LaTeX HTML to be rendered.
|
||||
if(this.prevHTML === undefined || this.nextHTML === undefined) {
|
||||
const [prevHTML, nextHTML] = await Promise.all(this._renderPromises)
|
||||
this.prevHTML = this.prevHTML ?? prevHTML
|
||||
this.nextHTML = this.nextHTML ?? nextHTML
|
||||
}
|
||||
return translation.arg(this.prevHTML).arg(this.nextHTML)
|
||||
|
||||
}
|
||||
}
|
61
common/src/history/visibility.mjs
Normal file
61
common/src/history/visibility.mjs
Normal file
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import EditedProperty from "./editproperty.mjs"
|
||||
import Objects from "../module/objects.mjs"
|
||||
|
||||
|
||||
/**
|
||||
* Action used when an object's shown or hidden.
|
||||
*/
|
||||
export default class EditedVisibility extends EditedProperty {
|
||||
type() {
|
||||
return "EditedVisibility"
|
||||
}
|
||||
|
||||
icon() {
|
||||
return "visibility"
|
||||
}
|
||||
|
||||
color(darkVer = false) {
|
||||
return this.newValue ?
|
||||
(darkVer ? "darkgray" : "whitesmoke") :
|
||||
(darkVer ? "dimgray" : "lightgray")
|
||||
}
|
||||
|
||||
constructor(targetName = "", targetType = "Point", newValue = true) {
|
||||
super(targetName, targetType, "visible", !newValue, newValue)
|
||||
}
|
||||
|
||||
export() {
|
||||
return [this.targetName, this.targetType, this.newValue]
|
||||
}
|
||||
|
||||
getReadableString() {
|
||||
return (this.newValue ? qsTranslate("visibility", "%1 %2 shown.") : qsTranslate("visibility", "%1 %2 hidden."))
|
||||
.arg(Objects.types[this.targetType].displayType())
|
||||
.arg(this.targetName)
|
||||
}
|
||||
|
||||
getHTMLString() {
|
||||
return (this.newValue ? qsTranslate("visibility", "%1 %2 shown.") : qsTranslate("visibility", "%1 %2 hidden."))
|
||||
.arg(Objects.types[this.targetType].displayType())
|
||||
.arg("<b style=\"font-size: 15px;\">" + this.targetName + "</b>")
|
||||
}
|
||||
}
|
||||
|
27
common/src/index.mjs
Normal file
27
common/src/index.mjs
Normal file
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import js from "./lib/polyfills/js.mjs"
|
||||
|
||||
import * as Modules from "./module/index.mjs"
|
||||
import * as ObjsAutoload from "./objs/autoload.mjs"
|
||||
|
||||
export * as MathLib from "./math/index.mjs"
|
||||
export * as HistoryLib from "./history/index.mjs"
|
||||
export * as Parsing from "./parsing/index.mjs"
|
||||
export * as Utils from "./utils.mjs"
|
539
common/src/lib/expr-eval/expression.mjs
Normal file
539
common/src/lib/expr-eval/expression.mjs
Normal file
|
@ -0,0 +1,539 @@
|
|||
/**
|
||||
* Based on ndef.parser, by Raphael Graf <r@undefined.ch>
|
||||
* http://www.undefined.ch/mparser/index.html
|
||||
*
|
||||
* Ported to JavaScript and modified by Matthew Crumley <email@matthewcrumley.com>
|
||||
* https://silentmatt.com/javascript-expression-evaluator/
|
||||
*
|
||||
* Ported to QMLJS with modifications done accordingly done by Ad5001 <mail@ad5001.eu> (https://ad5001.eu)
|
||||
*
|
||||
* Copyright (c) 2015 Matthew Crumley, 2021-2024 Ad5001
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
* You are free to use and modify this code in anyway you find useful. Please leave this comment in the code
|
||||
* to acknowledge its original source. If you feel like it, I enjoy hearing about projects that use my code,
|
||||
* but don't feel like you have to let me know or ask permission.
|
||||
*/
|
||||
|
||||
import {
|
||||
Instruction,
|
||||
IOP3, IOP2, IOP1,
|
||||
INUMBER, IARRAY,
|
||||
IVAR, IVARNAME,
|
||||
IEXPR, IEXPREVAL,
|
||||
IMEMBER, IFUNCALL,
|
||||
IENDSTATEMENT,
|
||||
unaryInstruction, binaryInstruction, ternaryInstruction
|
||||
} from "./instruction.mjs"
|
||||
|
||||
/**
|
||||
* Simplifies the given instructions
|
||||
* @param {Instruction[]} tokens
|
||||
* @param {Record.<string, function(any): any>} unaryOps
|
||||
* @param {Record.<string, function(any, any): any>} binaryOps
|
||||
* @param {Record.<string, function(any, any, any): any>} ternaryOps
|
||||
* @param {Record.<string, any>} values
|
||||
* @return {Instruction[]}
|
||||
*/
|
||||
function simplify(tokens, unaryOps, binaryOps, ternaryOps, values) {
|
||||
const nstack = []
|
||||
const newexpression = []
|
||||
let n1, n2, n3
|
||||
let f
|
||||
for(let i = 0; i < tokens.length; i++) {
|
||||
let item = tokens[i]
|
||||
const type = item.type
|
||||
if(type === INUMBER || type === IVARNAME) {
|
||||
if(Array.isArray(item.value)) {
|
||||
nstack.push.apply(nstack, simplify(item.value.map(function(x) {
|
||||
return new Instruction(INUMBER, x)
|
||||
}).concat(new Instruction(IARRAY, item.value.length)), unaryOps, binaryOps, ternaryOps, values))
|
||||
} else {
|
||||
nstack.push(item)
|
||||
}
|
||||
} else if(type === IVAR && values.hasOwnProperty(item.value)) {
|
||||
item = new Instruction(INUMBER, values[item.value])
|
||||
nstack.push(item)
|
||||
} else if(type === IOP2 && nstack.length > 1) {
|
||||
n2 = nstack.pop()
|
||||
n1 = nstack.pop()
|
||||
f = binaryOps[item.value]
|
||||
item = new Instruction(INUMBER, f(n1.value, n2.value))
|
||||
nstack.push(item)
|
||||
} else if(type === IOP3 && nstack.length > 2) {
|
||||
n3 = nstack.pop()
|
||||
n2 = nstack.pop()
|
||||
n1 = nstack.pop()
|
||||
if(item.value === "?") {
|
||||
nstack.push(n1.value ? n2.value : n3.value)
|
||||
} else {
|
||||
f = ternaryOps[item.value]
|
||||
item = new Instruction(INUMBER, f(n1.value, n2.value, n3.value))
|
||||
nstack.push(item)
|
||||
}
|
||||
} else if(type === IOP1 && nstack.length > 0) {
|
||||
n1 = nstack.pop()
|
||||
f = unaryOps[item.value]
|
||||
item = new Instruction(INUMBER, f(n1.value))
|
||||
nstack.push(item)
|
||||
} else if(type === IEXPR) {
|
||||
while(nstack.length > 0) {
|
||||
newexpression.push(nstack.shift())
|
||||
}
|
||||
newexpression.push(new Instruction(IEXPR, simplify(item.value, unaryOps, binaryOps, ternaryOps, values)))
|
||||
} else if(type === IMEMBER && nstack.length > 0) {
|
||||
n1 = nstack.pop()
|
||||
if(item.value in n1.value)
|
||||
nstack.push(new Instruction(INUMBER, n1.value[item.value]))
|
||||
else
|
||||
throw new Error(qsTranslate("error", "Cannot find property %1 of object %2.").arg(item.value).arg(n1))
|
||||
} else {
|
||||
while(nstack.length > 0) {
|
||||
newexpression.push(nstack.shift())
|
||||
}
|
||||
newexpression.push(item)
|
||||
}
|
||||
}
|
||||
while(nstack.length > 0) {
|
||||
newexpression.push(nstack.shift())
|
||||
}
|
||||
return newexpression
|
||||
}
|
||||
|
||||
/**
|
||||
* In the given instructions, replaces variable by expr.
|
||||
* @param {Instruction[]} tokens
|
||||
* @param {string} variable
|
||||
* @param {number} expr
|
||||
* @return {Instruction[]}
|
||||
*/
|
||||
function substitute(tokens, variable, expr) {
|
||||
const newexpression = []
|
||||
for(let i = 0; i < tokens.length; i++) {
|
||||
let item = tokens[i]
|
||||
const type = item.type
|
||||
if(type === IVAR && item.value === variable) {
|
||||
for(let j = 0; j < expr.tokens.length; j++) {
|
||||
const expritem = expr.tokens[j]
|
||||
let replitem
|
||||
if(expritem.type === IOP1) {
|
||||
replitem = unaryInstruction(expritem.value)
|
||||
} else if(expritem.type === IOP2) {
|
||||
replitem = binaryInstruction(expritem.value)
|
||||
} else if(expritem.type === IOP3) {
|
||||
replitem = ternaryInstruction(expritem.value)
|
||||
} else {
|
||||
replitem = new Instruction(expritem.type, expritem.value)
|
||||
}
|
||||
newexpression.push(replitem)
|
||||
}
|
||||
} else if(type === IEXPR) {
|
||||
newexpression.push(new Instruction(IEXPR, substitute(item.value, variable, expr)))
|
||||
} else {
|
||||
newexpression.push(item)
|
||||
}
|
||||
}
|
||||
return newexpression
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the given instructions for a given Expression with given values.
|
||||
* @param {Instruction[]} tokens
|
||||
* @param {ExprEvalExpression} expr
|
||||
* @param {Record.<string, number>} values
|
||||
* @return {number}
|
||||
*/
|
||||
function evaluate(tokens, expr, values) {
|
||||
const nstack = []
|
||||
let n1, n2, n3
|
||||
let f, args, argCount
|
||||
|
||||
if(isExpressionEvaluator(tokens)) {
|
||||
return resolveExpression(tokens, values)
|
||||
}
|
||||
|
||||
for(let i = 0; i < tokens.length; i++) {
|
||||
const item = tokens[i]
|
||||
const type = item.type
|
||||
if(type === INUMBER || type === IVARNAME) {
|
||||
nstack.push(item.value)
|
||||
} else if(type === IOP2) {
|
||||
n2 = nstack.pop()
|
||||
n1 = nstack.pop()
|
||||
if(item.value === "and") {
|
||||
nstack.push(n1 ? !!evaluate(n2, expr, values) : false)
|
||||
} else if(item.value === "or") {
|
||||
nstack.push(n1 ? true : !!evaluate(n2, expr, values))
|
||||
} else if(item.value === "=") {
|
||||
f = expr.binaryOps[item.value]
|
||||
nstack.push(f(n1, evaluate(n2, expr, values), values))
|
||||
} else {
|
||||
f = expr.binaryOps[item.value]
|
||||
nstack.push(f(resolveExpression(n1, values), resolveExpression(n2, values)))
|
||||
}
|
||||
} else if(type === IOP3) {
|
||||
n3 = nstack.pop()
|
||||
n2 = nstack.pop()
|
||||
n1 = nstack.pop()
|
||||
if(item.value === "?") {
|
||||
nstack.push(evaluate(n1 ? n2 : n3, expr, values))
|
||||
} else {
|
||||
f = expr.ternaryOps[item.value]
|
||||
nstack.push(f(resolveExpression(n1, values), resolveExpression(n2, values), resolveExpression(n3, values)))
|
||||
}
|
||||
} else if(type === IVAR) {
|
||||
// Check for variable value
|
||||
if(/^__proto__|prototype|constructor$/.test(item.value)) {
|
||||
throw new Error("WARNING: Prototype access detected and denied. If you downloaded this file from the internet, this file might be a virus.")
|
||||
} else if(item.value in expr.functions) {
|
||||
nstack.push(expr.functions[item.value])
|
||||
} else if(item.value in expr.unaryOps && expr.parser.isOperatorEnabled(item.value)) {
|
||||
nstack.push(expr.unaryOps[item.value])
|
||||
} else {
|
||||
const v = values[item.value]
|
||||
if(v !== undefined) {
|
||||
nstack.push(v)
|
||||
} else {
|
||||
throw new Error(qsTranslate("error", "Undefined variable %1.").arg(item.value))
|
||||
}
|
||||
}
|
||||
} else if(type === IOP1) {
|
||||
n1 = nstack.pop()
|
||||
f = expr.unaryOps[item.value]
|
||||
nstack.push(f(resolveExpression(n1, values)))
|
||||
} else if(type === IFUNCALL) {
|
||||
argCount = item.value
|
||||
args = []
|
||||
while(argCount-- > 0) {
|
||||
args.unshift(resolveExpression(nstack.pop(), values))
|
||||
}
|
||||
f = nstack.pop()
|
||||
if(f.apply && f.call) {
|
||||
nstack.push(f.apply(undefined, args))
|
||||
} else if(f.execute) {
|
||||
// Objects & expressions execution
|
||||
if(args.length >= 1)
|
||||
nstack.push(f.execute.apply(f, args))
|
||||
else
|
||||
throw new Error(qsTranslate("error", "In order to be executed, object %1 must have at least one argument.").arg(f))
|
||||
} else {
|
||||
throw new Error(qsTranslate("error", "%1 cannot be executed.").arg(f))
|
||||
}
|
||||
} else if(type === IEXPR) {
|
||||
nstack.push(createExpressionEvaluator(item, expr))
|
||||
} else if(type === IEXPREVAL) {
|
||||
nstack.push(item)
|
||||
} else if(type === IMEMBER) {
|
||||
n1 = nstack.pop()
|
||||
if(item.value in n1)
|
||||
if(n1[item.value].execute && n1[item.value].cached)
|
||||
nstack.push(n1[item.value].execute())
|
||||
else
|
||||
nstack.push(n1[item.value])
|
||||
else
|
||||
throw new Error(qsTranslate("error", "Cannot find property %1 of object %2.").arg(item.value).arg(n1))
|
||||
} else if(type === IENDSTATEMENT) {
|
||||
nstack.pop()
|
||||
} else if(type === IARRAY) {
|
||||
argCount = item.value
|
||||
args = []
|
||||
while(argCount-- > 0) {
|
||||
args.unshift(nstack.pop())
|
||||
}
|
||||
nstack.push(args)
|
||||
} else {
|
||||
throw new Error(qsTranslate("error", "Invalid expression."))
|
||||
}
|
||||
}
|
||||
if(nstack.length > 1) {
|
||||
throw new Error(qsTranslate("error", "Invalid expression (parity)."))
|
||||
}
|
||||
// Explicitly return zero to avoid test issues caused by -0
|
||||
return nstack[0] === 0 ? 0 : resolveExpression(nstack[0], values)
|
||||
}
|
||||
|
||||
function createExpressionEvaluator(token, expr) {
|
||||
if(isExpressionEvaluator(token)) return token
|
||||
return {
|
||||
type: IEXPREVAL,
|
||||
value: function(scope) {
|
||||
return evaluate(token.value, expr, scope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isExpressionEvaluator(n) {
|
||||
return n && n.type === IEXPREVAL
|
||||
}
|
||||
|
||||
function resolveExpression(n, values) {
|
||||
return isExpressionEvaluator(n) ? n.value(values) : n
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given instructions to a string
|
||||
* If toJS is active, can be evaluated with eval, otherwise it can be reparsed by the parser.
|
||||
* @param {Instruction[]} tokens
|
||||
* @param {boolean} toJS
|
||||
* @return {string}
|
||||
*/
|
||||
function expressionToString(tokens, toJS) {
|
||||
let nstack = []
|
||||
let n1, n2, n3
|
||||
let f, args, argCount
|
||||
for(let i = 0; i < tokens.length; i++) {
|
||||
const item = tokens[i]
|
||||
const type = item.type
|
||||
if(type === INUMBER) {
|
||||
if(typeof item.value === "number" && item.value < 0) {
|
||||
nstack.push("(" + item.value + ")")
|
||||
} else if(Array.isArray(item.value)) {
|
||||
nstack.push("[" + item.value.map(escapeValue).join(", ") + "]")
|
||||
} else {
|
||||
nstack.push(escapeValue(item.value))
|
||||
}
|
||||
} else if(type === IOP2) {
|
||||
n2 = nstack.pop()
|
||||
n1 = nstack.pop()
|
||||
f = item.value
|
||||
if(toJS) {
|
||||
if(f === "^") {
|
||||
nstack.push("Math.pow(" + n1 + ", " + n2 + ")")
|
||||
} else if(f === "and") {
|
||||
nstack.push("(!!" + n1 + " && !!" + n2 + ")")
|
||||
} else if(f === "or") {
|
||||
nstack.push("(!!" + n1 + " || !!" + n2 + ")")
|
||||
} else if(f === "||") {
|
||||
nstack.push("(function(a,b){ return Array.isArray(a) && Array.isArray(b) ? a.concat(b) : String(a) + String(b); }((" + n1 + "),(" + n2 + ")))")
|
||||
} else if(f === "==") {
|
||||
nstack.push("(" + n1 + " === " + n2 + ")")
|
||||
} else if(f === "!=") {
|
||||
nstack.push("(" + n1 + " !== " + n2 + ")")
|
||||
} else if(f === "[") {
|
||||
nstack.push(n1 + "[(" + n2 + ") | 0]")
|
||||
} else {
|
||||
nstack.push("(" + n1 + " " + f + " " + n2 + ")")
|
||||
}
|
||||
} else {
|
||||
if(f === "[") {
|
||||
nstack.push(n1 + "[" + n2 + "]")
|
||||
} else {
|
||||
nstack.push("(" + n1 + " " + f + " " + n2 + ")")
|
||||
}
|
||||
}
|
||||
} else if(type === IOP3) {
|
||||
n3 = nstack.pop()
|
||||
n2 = nstack.pop()
|
||||
n1 = nstack.pop()
|
||||
f = item.value
|
||||
if(f === "?") {
|
||||
nstack.push("(" + n1 + " ? " + n2 + " : " + n3 + ")")
|
||||
} else {
|
||||
throw new Error(qsTranslate("error", "Invalid expression."))
|
||||
}
|
||||
} else if(type === IVAR || type === IVARNAME) {
|
||||
nstack.push(item.value)
|
||||
} else if(type === IOP1) {
|
||||
n1 = nstack.pop()
|
||||
f = item.value
|
||||
if(f === "-" || f === "+") {
|
||||
nstack.push("(" + f + n1 + ")")
|
||||
} else if(toJS) {
|
||||
if(f === "not") {
|
||||
nstack.push("(" + "!" + n1 + ")")
|
||||
} else if(f === "!") {
|
||||
nstack.push("fac(" + n1 + ")")
|
||||
} else {
|
||||
nstack.push(f + "(" + n1 + ")")
|
||||
}
|
||||
} else if(f === "!") {
|
||||
nstack.push("(" + n1 + "!)")
|
||||
} else {
|
||||
nstack.push("(" + f + " " + n1 + ")")
|
||||
}
|
||||
} else if(type === IFUNCALL) {
|
||||
argCount = item.value
|
||||
args = []
|
||||
while(argCount-- > 0) {
|
||||
args.unshift(nstack.pop())
|
||||
}
|
||||
f = nstack.pop()
|
||||
nstack.push(f + "(" + args.join(", ") + ")")
|
||||
} else if(type === IMEMBER) {
|
||||
n1 = nstack.pop()
|
||||
nstack.push(n1 + "." + item.value)
|
||||
} else if(type === IARRAY) {
|
||||
argCount = item.value
|
||||
args = []
|
||||
while(argCount-- > 0) {
|
||||
args.unshift(nstack.pop())
|
||||
}
|
||||
nstack.push("[" + args.join(", ") + "]")
|
||||
} else if(type === IEXPR) {
|
||||
nstack.push("(" + expressionToString(item.value, toJS) + ")")
|
||||
} else if(type === IENDSTATEMENT) {
|
||||
|
||||
} else {
|
||||
throw new Error(qsTranslate("error", "Invalid expression."))
|
||||
}
|
||||
}
|
||||
if(nstack.length > 1) {
|
||||
if(toJS) {
|
||||
nstack = [nstack.join(",")]
|
||||
} else {
|
||||
nstack = [nstack.join(";")]
|
||||
}
|
||||
}
|
||||
return String(nstack[0])
|
||||
}
|
||||
|
||||
export function escapeValue(v) {
|
||||
if(typeof v === "string") {
|
||||
return JSON.stringify(v).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029")
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes all symbols from tokens into the symbols array.
|
||||
* @param {Instruction[]} tokens
|
||||
* @param {string[]} symbols
|
||||
* @param {{withMembers: (boolean|undefined)}}options
|
||||
*/
|
||||
function getSymbols(tokens, symbols, options) {
|
||||
options = options || {}
|
||||
const withMembers = !!options.withMembers
|
||||
let prevVar = null
|
||||
|
||||
for(let i = 0; i < tokens.length; i++) {
|
||||
const item = tokens[i]
|
||||
if(item.type === IVAR || item.type === IVARNAME) {
|
||||
if(!withMembers && !symbols.includes(item.value)) {
|
||||
symbols.push(item.value)
|
||||
} else if(prevVar !== null) {
|
||||
if(!symbols.includes(prevVar)) {
|
||||
symbols.push(prevVar)
|
||||
}
|
||||
prevVar = item.value
|
||||
} else {
|
||||
prevVar = item.value
|
||||
}
|
||||
} else if(item.type === IMEMBER && withMembers && prevVar !== null) {
|
||||
prevVar += "." + item.value
|
||||
} else if(item.type === IEXPR) {
|
||||
getSymbols(item.value, symbols, options)
|
||||
} else if(prevVar !== null) {
|
||||
if(!symbols.includes(prevVar)) {
|
||||
symbols.push(prevVar)
|
||||
}
|
||||
prevVar = null
|
||||
}
|
||||
}
|
||||
|
||||
if(prevVar !== null && !symbols.includes(prevVar)) {
|
||||
symbols.push(prevVar)
|
||||
}
|
||||
}
|
||||
|
||||
export class ExprEvalExpression {
|
||||
/**
|
||||
* @param {Instruction[]} tokens
|
||||
* @param {Parser} parser
|
||||
*/
|
||||
constructor(tokens, parser) {
|
||||
this.tokens = tokens
|
||||
this.parser = parser
|
||||
this.unaryOps = parser.unaryOps
|
||||
this.binaryOps = parser.binaryOps
|
||||
this.ternaryOps = parser.ternaryOps
|
||||
this.functions = parser.functions
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplifies the expression.
|
||||
* @param {Object<string, number|ExprEvalExpression>|undefined} values
|
||||
* @returns {ExprEvalExpression}
|
||||
*/
|
||||
simplify(values) {
|
||||
values = values || {}
|
||||
return new ExprEvalExpression(simplify(this.tokens, this.unaryOps, this.binaryOps, this.ternaryOps, values), this.parser)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new expression where the variable is substituted by the given expression.
|
||||
* @param {string} variable
|
||||
* @param {string|ExprEvalExpression} expr
|
||||
* @returns {ExprEvalExpression}
|
||||
*/
|
||||
substitute(variable, expr) {
|
||||
if(!(expr instanceof ExprEvalExpression)) {
|
||||
expr = this.parser.parse(String(expr))
|
||||
}
|
||||
|
||||
return new ExprEvalExpression(substitute(this.tokens, variable, expr), this.parser)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the value of the expression by giving all variables and their corresponding values.
|
||||
* @param {Object<string, number|DrawableObject>} values
|
||||
* @returns {number}
|
||||
*/
|
||||
evaluate(values) {
|
||||
values = Object.assign({}, values, this.parser.consts)
|
||||
return evaluate(this.tokens, this, values)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of symbols (string of characters) in the expressions.
|
||||
* Can be functions, constants, or variables.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
symbols(options) {
|
||||
options = options || {}
|
||||
const vars = []
|
||||
getSymbols(this.tokens, vars, options)
|
||||
return vars
|
||||
}
|
||||
|
||||
toString() {
|
||||
return expressionToString(this.tokens, false)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the list of symbols (string of characters) which are not defined
|
||||
* as constants or functions.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
variables(options = {}) {
|
||||
const vars = []
|
||||
getSymbols(this.tokens, vars, options)
|
||||
const functions = this.functions
|
||||
const consts = this.parser.consts
|
||||
return vars.filter((name) => {
|
||||
return !(name in functions) && !(name in consts)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts the expression to a JS function.
|
||||
* @param {string} param - Parsed variables for the function.
|
||||
* @param {Object.<string, (ExprEvalExpression|string)>} variables - Default variables to provide.
|
||||
* @returns {function(...any)}
|
||||
*/
|
||||
toJSFunction(param, variables) {
|
||||
const expr = this
|
||||
const f = new Function(param, "with(this.functions) with (this.ternaryOps) with (this.binaryOps) with (this.unaryOps) { return " + expressionToString(this.simplify(variables).tokens, true) + "; }") // eslint-disable-line no-new-func
|
||||
return function() {
|
||||
return f.apply(expr, arguments)
|
||||
}
|
||||
}
|
||||
}
|
82
common/src/lib/expr-eval/instruction.mjs
Normal file
82
common/src/lib/expr-eval/instruction.mjs
Normal file
|
@ -0,0 +1,82 @@
|
|||
/**
|
||||
* Based on ndef.parser, by Raphael Graf <r@undefined.ch>
|
||||
* http://www.undefined.ch/mparser/index.html
|
||||
*
|
||||
* Ported to JavaScript and modified by Matthew Crumley <email@matthewcrumley.com>
|
||||
* https://silentmatt.com/javascript-expression-evaluator/
|
||||
*
|
||||
* Ported to QMLJS with modifications done accordingly done by Ad5001 <mail@ad5001.eu> (https://ad5001.eu)
|
||||
*
|
||||
* Copyright (c) 2015 Matthew Crumley, 2021-2024 Ad5001
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
* You are free to use and modify this code in anyway you find useful. Please leave this comment in the code
|
||||
* to acknowledge its original source. If you feel like it, I enjoy hearing about projects that use my code,
|
||||
* but don't feel like you have to let me know or ask permission.
|
||||
*/
|
||||
|
||||
export const INUMBER = "INUMBER"
|
||||
export const IOP1 = "IOP1"
|
||||
export const IOP2 = "IOP2"
|
||||
export const IOP3 = "IOP3"
|
||||
export const IVAR = "IVAR"
|
||||
export const IVARNAME = "IVARNAME"
|
||||
export const IFUNCALL = "IFUNCALL"
|
||||
export const IEXPR = "IEXPR"
|
||||
export const IEXPREVAL = "IEXPREVAL"
|
||||
export const IMEMBER = "IMEMBER"
|
||||
export const IENDSTATEMENT = "IENDSTATEMENT"
|
||||
export const IARRAY = "IARRAY"
|
||||
|
||||
|
||||
export class Instruction {
|
||||
/**
|
||||
*
|
||||
* @param {string} type
|
||||
* @param {any} value
|
||||
*/
|
||||
constructor(type, value) {
|
||||
this.type = type
|
||||
this.value = (value !== undefined && value !== null) ? value : 0
|
||||
}
|
||||
|
||||
toString() {
|
||||
switch(this.type) {
|
||||
case INUMBER:
|
||||
case IOP1:
|
||||
case IOP2:
|
||||
case IOP3:
|
||||
case IVAR:
|
||||
case IVARNAME:
|
||||
case IENDSTATEMENT:
|
||||
return this.value
|
||||
case IFUNCALL:
|
||||
return "CALL " + this.value
|
||||
case IARRAY:
|
||||
return "ARRAY " + this.value
|
||||
case IMEMBER:
|
||||
return "." + this.value
|
||||
default:
|
||||
return "Invalid Instruction"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function unaryInstruction(value) {
|
||||
return new Instruction(IOP1, value)
|
||||
}
|
||||
|
||||
export function binaryInstruction(value) {
|
||||
return new Instruction(IOP2, value)
|
||||
}
|
||||
|
||||
export function ternaryInstruction(value) {
|
||||
return new Instruction(IOP3, value)
|
||||
}
|
172
common/src/lib/expr-eval/parser.mjs
Normal file
172
common/src/lib/expr-eval/parser.mjs
Normal file
|
@ -0,0 +1,172 @@
|
|||
/**
|
||||
* Based on ndef.parser, by Raphael Graf <r@undefined.ch>
|
||||
* http://www.undefined.ch/mparser/index.html
|
||||
*
|
||||
* Ported to JavaScript and modified by Matthew Crumley <email@matthewcrumley.com>
|
||||
* https://silentmatt.com/javascript-expression-evaluator/
|
||||
*
|
||||
* Ported to QMLJS with modifications done accordingly done by Ad5001 <mail@ad5001.eu> (https://ad5001.eu)
|
||||
*
|
||||
* Copyright (c) 2015 Matthew Crumley, 2021-2024 Ad5001
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
* You are free to use and modify this code in anyway you find useful. Please leave this comment in the code
|
||||
* to acknowledge its original source. If you feel like it, I enjoy hearing about projects that use my code,
|
||||
* but don't feel like you have to let me know or ask permission.
|
||||
*/
|
||||
|
||||
import * as Polyfill from "./polyfill.mjs"
|
||||
import { ParserState } from "./parserstate.mjs"
|
||||
import { TEOF, TokenStream } from "./tokens.mjs"
|
||||
import { ExprEvalExpression } from "./expression.mjs"
|
||||
|
||||
const optionNameMap = {
|
||||
"+": "add",
|
||||
"-": "subtract",
|
||||
"*": "multiply",
|
||||
"/": "divide",
|
||||
"%": "remainder",
|
||||
"^": "power",
|
||||
"!": "factorial",
|
||||
"<": "comparison",
|
||||
">": "comparison",
|
||||
"<=": "comparison",
|
||||
">=": "comparison",
|
||||
"==": "comparison",
|
||||
"!=": "comparison",
|
||||
"||": "concatenate",
|
||||
"and": "logical",
|
||||
"or": "logical",
|
||||
"not": "logical",
|
||||
"?": "conditional",
|
||||
":": "conditional",
|
||||
//'=': 'assignment', // Disable assignment
|
||||
"[": "array"
|
||||
//'()=': 'fndef' // Diable function definition
|
||||
}
|
||||
|
||||
export class Parser {
|
||||
constructor(options) {
|
||||
this.options = options || {}
|
||||
this.unaryOps = {
|
||||
sin: Math.sin,
|
||||
cos: Math.cos,
|
||||
tan: Math.tan,
|
||||
asin: Math.asin,
|
||||
acos: Math.acos,
|
||||
atan: Math.atan,
|
||||
sinh: Math.sinh || Polyfill.sinh,
|
||||
cosh: Math.cosh || Polyfill.cosh,
|
||||
tanh: Math.tanh || Polyfill.tanh,
|
||||
asinh: Math.asinh || Polyfill.asinh,
|
||||
acosh: Math.acosh || Polyfill.acosh,
|
||||
atanh: Math.atanh || Polyfill.atanh,
|
||||
sqrt: Math.sqrt,
|
||||
cbrt: Math.cbrt || Polyfill.cbrt,
|
||||
log: Math.log,
|
||||
log2: Math.log2 || Polyfill.log2,
|
||||
ln: Math.log,
|
||||
lg: Math.log10 || Polyfill.log10,
|
||||
log10: Math.log10 || Polyfill.log10,
|
||||
expm1: Math.expm1 || Polyfill.expm1,
|
||||
log1p: Math.log1p || Polyfill.log1p,
|
||||
abs: Math.abs,
|
||||
ceil: Math.ceil,
|
||||
floor: Math.floor,
|
||||
round: Math.round,
|
||||
trunc: Math.trunc || Polyfill.trunc,
|
||||
"-": Polyfill.neg,
|
||||
"+": Number,
|
||||
exp: Math.exp,
|
||||
not: Polyfill.not,
|
||||
length: Polyfill.stringOrArrayLength,
|
||||
"!": Polyfill.factorial,
|
||||
sign: Math.sign || Polyfill.sign
|
||||
}
|
||||
this.unaryOpsList = Object.keys(this.unaryOps)
|
||||
|
||||
this.binaryOps = {
|
||||
"+": Polyfill.add,
|
||||
"-": Polyfill.sub,
|
||||
"*": Polyfill.mul,
|
||||
"/": Polyfill.div,
|
||||
"%": Polyfill.mod,
|
||||
"^": Math.pow,
|
||||
"||": Polyfill.concat,
|
||||
"==": Polyfill.equal,
|
||||
"!=": Polyfill.notEqual,
|
||||
">": Polyfill.greaterThan,
|
||||
"<": Polyfill.lessThan,
|
||||
">=": Polyfill.greaterThanEqual,
|
||||
"<=": Polyfill.lessThanEqual,
|
||||
and: Polyfill.andOperator,
|
||||
or: Polyfill.orOperator,
|
||||
"in": Polyfill.inOperator,
|
||||
"=": Polyfill.setVar,
|
||||
"[": Polyfill.arrayIndex
|
||||
}
|
||||
|
||||
this.ternaryOps = {
|
||||
"?": Polyfill.condition
|
||||
}
|
||||
|
||||
this.functions = {
|
||||
random: Polyfill.random,
|
||||
fac: Polyfill.factorial,
|
||||
min: Polyfill.min,
|
||||
max: Polyfill.max,
|
||||
hypot: Math.hypot || Polyfill.hypot,
|
||||
pyt: Math.hypot || Polyfill.hypot, // backward compat
|
||||
pow: Math.pow,
|
||||
atan2: Math.atan2,
|
||||
"if": Polyfill.condition,
|
||||
gamma: Polyfill.gamma,
|
||||
"Γ": Polyfill.gamma,
|
||||
roundTo: Polyfill.roundTo,
|
||||
map: Polyfill.arrayMap,
|
||||
fold: Polyfill.arrayFold,
|
||||
filter: Polyfill.arrayFilter,
|
||||
indexOf: Polyfill.stringOrArrayIndexOf,
|
||||
join: Polyfill.arrayJoin
|
||||
}
|
||||
|
||||
// These constants will automatically be replaced the MOMENT they are parsed.
|
||||
// (Original consts from the parser)
|
||||
this.builtinConsts = {}
|
||||
// These consts will only be replaced when the expression is evaluated.
|
||||
this.consts = {}
|
||||
|
||||
}
|
||||
|
||||
parse(expr) {
|
||||
const instr = []
|
||||
const parserState = new ParserState(
|
||||
this,
|
||||
new TokenStream(this, expr),
|
||||
{ allowMemberAccess: this.options.allowMemberAccess }
|
||||
)
|
||||
|
||||
parserState.parseExpression(instr)
|
||||
parserState.expect(TEOF, QT_TRANSLATE_NOOP("error", "EOF"))
|
||||
|
||||
return new ExprEvalExpression(instr, this)
|
||||
}
|
||||
|
||||
evaluate(expr, variables) {
|
||||
return this.parse(expr).evaluate(variables)
|
||||
}
|
||||
|
||||
isOperatorEnabled(op) {
|
||||
const optionName = optionNameMap.hasOwnProperty(op) ? optionNameMap[op] : op
|
||||
const operators = this.options.operators || {}
|
||||
|
||||
return !(optionName in operators) || !!operators[optionName]
|
||||
}
|
||||
}
|
398
common/src/lib/expr-eval/parserstate.mjs
Normal file
398
common/src/lib/expr-eval/parserstate.mjs
Normal file
|
@ -0,0 +1,398 @@
|
|||
/**
|
||||
* Based on ndef.parser, by Raphael Graf <r@undefined.ch>
|
||||
* http://www.undefined.ch/mparser/index.html
|
||||
*
|
||||
* Ported to JavaScript and modified by Matthew Crumley <email@matthewcrumley.com>
|
||||
* https://silentmatt.com/javascript-expression-evaluator/
|
||||
*
|
||||
* Ported to QMLJS with modifications done accordingly done by Ad5001 <mail@ad5001.eu> (https://ad5001.eu)
|
||||
*
|
||||
* Copyright (c) 2015 Matthew Crumley, 2021-2024 Ad5001
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
* You are free to use and modify this code in anyway you find useful. Please leave this comment in the code
|
||||
* to acknowledge its original source. If you feel like it, I enjoy hearing about projects that use my code,
|
||||
* but don't feel like you have to let me know or ask permission.
|
||||
*/
|
||||
|
||||
import { TBRACKET, TCOMMA, TEOF, TNAME, TNUMBER, TOP, TPAREN, TSTRING } from "./tokens.mjs"
|
||||
import {
|
||||
Instruction,
|
||||
IARRAY, IEXPR, IFUNCALL, IMEMBER,
|
||||
INUMBER, IVAR,
|
||||
ternaryInstruction, binaryInstruction, unaryInstruction
|
||||
} from "./instruction.mjs"
|
||||
|
||||
const COMPARISON_OPERATORS = ["==", "!=", "<", "<=", ">=", ">", "in"]
|
||||
const ADD_SUB_OPERATORS = ["+", "-", "||"]
|
||||
const TERM_OPERATORS = ["*", "/", "%"]
|
||||
|
||||
export class ParserState {
|
||||
/**
|
||||
*
|
||||
* @param {Parser} parser
|
||||
* @param {TokenStream} tokenStream
|
||||
* @param {{[operators]: Object.<string, boolean>, [allowMemberAccess]: boolean}} options
|
||||
*/
|
||||
constructor(parser, tokenStream, options) {
|
||||
this.parser = parser
|
||||
this.tokens = tokenStream
|
||||
this.current = null
|
||||
this.nextToken = null
|
||||
this.next()
|
||||
this.savedCurrent = null
|
||||
this.savedNextToken = null
|
||||
this.allowMemberAccess = options.allowMemberAccess !== false
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the next token for parsing.
|
||||
* @return {Token}
|
||||
*/
|
||||
next() {
|
||||
this.current = this.nextToken
|
||||
this.nextToken = this.tokens.next()
|
||||
return this.nextToken
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given Token matches a condition (called if function, one of if array, and exact match otherwise)
|
||||
* @param {Token} token
|
||||
* @param {Array|function(Token): boolean|string|number|boolean} [value]
|
||||
* @return {boolean}
|
||||
*/
|
||||
tokenMatches(token, value) {
|
||||
if(typeof value === "undefined") {
|
||||
return true
|
||||
} else if(Array.isArray(value)) {
|
||||
return value.includes(token.value)
|
||||
} else if(typeof value === "function") {
|
||||
return value(token)
|
||||
} else {
|
||||
return token.value === value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the current state (current and next token) to be restored later.
|
||||
*/
|
||||
save() {
|
||||
this.savedCurrent = this.current
|
||||
this.savedNextToken = this.nextToken
|
||||
this.tokens.save()
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a previous state (current and next token) from last save.
|
||||
*/
|
||||
restore() {
|
||||
this.tokens.restore()
|
||||
this.current = this.savedCurrent
|
||||
this.nextToken = this.savedNextToken
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the next token matches the given type and value, and if so, consume the current token.
|
||||
* Returns true if the check matches.
|
||||
* @param {string} type
|
||||
* @param {any} [value]
|
||||
* @return {boolean}
|
||||
*/
|
||||
accept(type, value) {
|
||||
if(this.nextToken.type === type && this.tokenMatches(this.nextToken, value)) {
|
||||
this.next()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an error if the next token does not match the given type and value. Otherwise, consumes the current token.
|
||||
* @param {string} type
|
||||
* @param {any} [value]
|
||||
*/
|
||||
expect(type, value) {
|
||||
if(!this.accept(type, value)) {
|
||||
throw new Error(qsTranslate("error", "Parse error [position %1]: %2")
|
||||
.arg(this.tokens.pos)
|
||||
.arg(qsTranslate("error", "Expected %1").arg(value || type)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts enough Tokens to form an expression atom (generally the next part of the expression) into an instruction
|
||||
* and pushes it to the instruction list.
|
||||
* Throws an error if an unexpected token gets parsed.
|
||||
* @param {Instruction[]} instr
|
||||
*/
|
||||
parseAtom(instr) {
|
||||
const prefixOperators = this.tokens.unaryOpsList
|
||||
|
||||
if(this.accept(TNAME) || this.accept(TOP, prefixOperators)) {
|
||||
instr.push(new Instruction(IVAR, this.current.value))
|
||||
} else if(this.accept(TNUMBER)) {
|
||||
instr.push(new Instruction(INUMBER, this.current.value))
|
||||
} else if(this.accept(TSTRING)) {
|
||||
instr.push(new Instruction(INUMBER, this.current.value))
|
||||
} else if(this.accept(TPAREN, "(")) {
|
||||
this.parseExpression(instr)
|
||||
this.expect(TPAREN, ")")
|
||||
} else if(this.accept(TBRACKET, "[")) {
|
||||
if(this.accept(TBRACKET, "]")) {
|
||||
instr.push(new Instruction(IARRAY, 0))
|
||||
} else {
|
||||
const argCount = this.parseArrayList(instr)
|
||||
instr.push(new Instruction(IARRAY, argCount))
|
||||
}
|
||||
} else {
|
||||
throw new Error(qsTranslate("error", "Unexpected %1").arg(this.nextToken))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consumes the next tokens to compile a general expression which should return a value, and compiles
|
||||
* the instructions into the list.
|
||||
* @param {Instruction[]} instr
|
||||
*/
|
||||
parseExpression(instr) {
|
||||
const exprInstr = []
|
||||
this.parseConditionalExpression(exprInstr)
|
||||
instr.push(...exprInstr)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an array indice, and return the number of arguments found at the end.
|
||||
* @param {Instruction[]} instr
|
||||
* @return {number}
|
||||
*/
|
||||
parseArrayList(instr) {
|
||||
let argCount = 0
|
||||
|
||||
while(!this.accept(TBRACKET, "]")) {
|
||||
this.parseExpression(instr)
|
||||
++argCount
|
||||
while(this.accept(TCOMMA)) {
|
||||
this.parseExpression(instr)
|
||||
++argCount
|
||||
}
|
||||
}
|
||||
|
||||
return argCount
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a tertiary statement (<condition> ? <value if true> : <value if false>) and pushes it into the instruction
|
||||
* list.
|
||||
* @param {Instruction[]} instr
|
||||
*/
|
||||
parseConditionalExpression(instr) {
|
||||
this.parseOrExpression(instr)
|
||||
while(this.accept(TOP, "?")) {
|
||||
const trueBranch = []
|
||||
const falseBranch = []
|
||||
this.parseConditionalExpression(trueBranch)
|
||||
this.expect(TOP, ":")
|
||||
this.parseConditionalExpression(falseBranch)
|
||||
instr.push(new Instruction(IEXPR, trueBranch))
|
||||
instr.push(new Instruction(IEXPR, falseBranch))
|
||||
instr.push(ternaryInstruction("?"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a binary or statement (<condition 1> or <condition 2>) and pushes it into the instruction list.
|
||||
* @param {Instruction[]} instr
|
||||
*/
|
||||
parseOrExpression(instr) {
|
||||
this.parseAndExpression(instr)
|
||||
while(this.accept(TOP, "or")) {
|
||||
const falseBranch = []
|
||||
this.parseAndExpression(falseBranch)
|
||||
instr.push(new Instruction(IEXPR, falseBranch))
|
||||
instr.push(binaryInstruction("or"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a binary and statement (<condition 1> and <condition 2>) and pushes it into the instruction list.
|
||||
* @param {Instruction[]} instr
|
||||
*/
|
||||
parseAndExpression(instr) {
|
||||
this.parseComparison(instr)
|
||||
while(this.accept(TOP, "and")) {
|
||||
const trueBranch = []
|
||||
this.parseComparison(trueBranch)
|
||||
instr.push(new Instruction(IEXPR, trueBranch))
|
||||
instr.push(binaryInstruction("and"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a binary equality statement (<condition 1> == <condition 2> and so on) and pushes it into the instruction list.
|
||||
* @param {Instruction[]} instr
|
||||
*/
|
||||
parseComparison(instr) {
|
||||
this.parseAddSub(instr)
|
||||
while(this.accept(TOP, COMPARISON_OPERATORS)) {
|
||||
const op = this.current
|
||||
this.parseAddSub(instr)
|
||||
instr.push(binaryInstruction(op.value))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses add, minus and concat operations and pushes them into the instruction list.
|
||||
* @param {Instruction[]} instr
|
||||
*/
|
||||
parseAddSub(instr) {
|
||||
this.parseTerm(instr)
|
||||
while(this.accept(TOP, ADD_SUB_OPERATORS)) {
|
||||
const op = this.current
|
||||
this.parseTerm(instr)
|
||||
instr.push(binaryInstruction(op.value))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses times, divide and modulo operations and pushes them into the instruction list.
|
||||
* @param {Instruction[]} instr
|
||||
*/
|
||||
parseTerm(instr) {
|
||||
this.parseFactor(instr)
|
||||
while(this.accept(TOP, TERM_OPERATORS)) {
|
||||
const op = this.current
|
||||
this.parseFactor(instr)
|
||||
instr.push(binaryInstruction(op.value))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses prefix operations (+, -, but also functions like sin or cos which don't need parentheses)
|
||||
* @param {Instruction[]} instr
|
||||
*/
|
||||
parseFactor(instr) {
|
||||
const prefixOperators = this.tokens.unaryOpsList
|
||||
|
||||
this.save()
|
||||
if(this.accept(TOP, prefixOperators)) {
|
||||
if(this.current.value !== "-" && this.current.value !== "+") {
|
||||
if(this.nextToken.type === TPAREN && this.nextToken.value === "(") {
|
||||
this.restore()
|
||||
this.parseExponential(instr)
|
||||
return
|
||||
} else if(this.nextToken.type === TCOMMA || this.nextToken.type === TEOF || (this.nextToken.type === TPAREN && this.nextToken.value === ")")) {
|
||||
this.restore()
|
||||
this.parseAtom(instr)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const op = this.current
|
||||
this.parseFactor(instr)
|
||||
instr.push(unaryInstruction(op.value))
|
||||
} else {
|
||||
this.parseExponential(instr)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Instruction[]} instr
|
||||
*/
|
||||
parseExponential(instr) {
|
||||
this.parsePostfixExpression(instr)
|
||||
while(this.accept(TOP, "^")) {
|
||||
this.parseFactor(instr)
|
||||
instr.push(binaryInstruction("^"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parses factorial '!' (after the expression to apply it to).
|
||||
* @param {Instruction[]} instr
|
||||
*/
|
||||
parsePostfixExpression(instr) {
|
||||
this.parseFunctionCall(instr)
|
||||
while(this.accept(TOP, "!")) {
|
||||
instr.push(unaryInstruction("!"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a function (name + parentheses + arguments).
|
||||
* @param {Instruction[]} instr
|
||||
*/
|
||||
parseFunctionCall(instr) {
|
||||
const prefixOperators = this.tokens.unaryOpsList
|
||||
|
||||
if(this.accept(TOP, prefixOperators)) {
|
||||
const op = this.current
|
||||
this.parseAtom(instr)
|
||||
instr.push(unaryInstruction(op.value))
|
||||
} else {
|
||||
this.parseMemberExpression(instr)
|
||||
while(this.accept(TPAREN, "(")) {
|
||||
if(this.accept(TPAREN, ")")) {
|
||||
instr.push(new Instruction(IFUNCALL, 0))
|
||||
} else {
|
||||
const argCount = this.parseArgumentList(instr)
|
||||
instr.push(new Instruction(IFUNCALL, argCount))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a list of arguments, return their quantity.
|
||||
* @param {Instruction[]} instr
|
||||
* @return {number}
|
||||
*/
|
||||
parseArgumentList(instr) {
|
||||
let argCount = 0
|
||||
|
||||
while(!this.accept(TPAREN, ")")) {
|
||||
this.parseExpression(instr)
|
||||
++argCount
|
||||
while(this.accept(TCOMMA)) {
|
||||
this.parseExpression(instr)
|
||||
++argCount
|
||||
}
|
||||
}
|
||||
|
||||
return argCount
|
||||
}
|
||||
|
||||
parseMemberExpression(instr) {
|
||||
this.parseAtom(instr)
|
||||
while(this.accept(TOP, ".") || this.accept(TBRACKET, "[")) {
|
||||
const op = this.current
|
||||
|
||||
if(op.value === ".") {
|
||||
if(!this.allowMemberAccess) {
|
||||
throw new Error(qsTranslate("error", "Unexpected \".\": member access is not permitted"))
|
||||
}
|
||||
|
||||
this.expect(TNAME)
|
||||
instr.push(new Instruction(IMEMBER, this.current.value))
|
||||
} else if(op.value === "[") {
|
||||
if(!this.tokens.isOperatorEnabled("[")) {
|
||||
throw new Error(qsTranslate("error", "Unexpected \"[]\": arrays are disabled."))
|
||||
}
|
||||
|
||||
this.parseExpression(instr)
|
||||
this.expect(TBRACKET, "]")
|
||||
instr.push(binaryInstruction("["))
|
||||
} else {
|
||||
throw new Error(qsTranslate("error", "Unexpected symbol: %1.").arg(op.value))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
371
common/src/lib/expr-eval/polyfill.mjs
Normal file
371
common/src/lib/expr-eval/polyfill.mjs
Normal file
|
@ -0,0 +1,371 @@
|
|||
/**
|
||||
* Based on ndef.parser, by Raphael Graf <r@undefined.ch>
|
||||
* http://www.undefined.ch/mparser/index.html
|
||||
*
|
||||
* Ported to JavaScript and modified by Matthew Crumley <email@matthewcrumley.com>
|
||||
* https://silentmatt.com/javascript-expression-evaluator/
|
||||
*
|
||||
* Ported to QMLJS with modifications done accordingly done by Ad5001 <mail@ad5001.eu> (https://ad5001.eu)
|
||||
*
|
||||
* Copyright (c) 2015 Matthew Crumley, 2021-2024 Ad5001
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
* You are free to use and modify this code in anyway you find useful. Please leave this comment in the code
|
||||
* to acknowledge its original source. If you feel like it, I enjoy hearing about projects that use my code,
|
||||
* but don't feel like you have to let me know or ask permission.
|
||||
*/
|
||||
|
||||
export function add(a, b) {
|
||||
return Number(a) + Number(b)
|
||||
}
|
||||
|
||||
export function sub(a, b) {
|
||||
return a - b
|
||||
}
|
||||
|
||||
export function mul(a, b) {
|
||||
return a * b
|
||||
}
|
||||
|
||||
export function div(a, b) {
|
||||
return a / b
|
||||
}
|
||||
|
||||
export function mod(a, b) {
|
||||
return a % b
|
||||
}
|
||||
|
||||
export function concat(a, b) {
|
||||
if(Array.isArray(a) && Array.isArray(b)) {
|
||||
return a.concat(b)
|
||||
}
|
||||
return "" + a + b
|
||||
}
|
||||
|
||||
export function equal(a, b) {
|
||||
return a === b
|
||||
}
|
||||
|
||||
export function notEqual(a, b) {
|
||||
return a !== b
|
||||
}
|
||||
|
||||
export function greaterThan(a, b) {
|
||||
return a > b
|
||||
}
|
||||
|
||||
export function lessThan(a, b) {
|
||||
return a < b
|
||||
}
|
||||
|
||||
export function greaterThanEqual(a, b) {
|
||||
return a >= b
|
||||
}
|
||||
|
||||
export function lessThanEqual(a, b) {
|
||||
return a <= b
|
||||
}
|
||||
|
||||
export function andOperator(a, b) {
|
||||
return Boolean(a && b)
|
||||
}
|
||||
|
||||
export function orOperator(a, b) {
|
||||
return Boolean(a || b)
|
||||
}
|
||||
|
||||
export function inOperator(a, b) {
|
||||
return b.includes(a)
|
||||
}
|
||||
|
||||
export function sinh(a) {
|
||||
return ((Math.exp(a) - Math.exp(-a)) / 2)
|
||||
}
|
||||
|
||||
export function cosh(a) {
|
||||
return ((Math.exp(a) + Math.exp(-a)) / 2)
|
||||
}
|
||||
|
||||
export function tanh(a) {
|
||||
if(a === Infinity) return 1
|
||||
if(a === -Infinity) return -1
|
||||
return (Math.exp(a) - Math.exp(-a)) / (Math.exp(a) + Math.exp(-a))
|
||||
}
|
||||
|
||||
export function asinh(a) {
|
||||
if(a === -Infinity) return a
|
||||
return Math.log(a + Math.sqrt((a * a) + 1))
|
||||
}
|
||||
|
||||
export function acosh(a) {
|
||||
return Math.log(a + Math.sqrt((a * a) - 1))
|
||||
}
|
||||
|
||||
export function atanh(a) {
|
||||
return (Math.log((1 + a) / (1 - a)) / 2)
|
||||
}
|
||||
|
||||
export function log10(a) {
|
||||
return Math.log(a) * Math.LOG10E
|
||||
}
|
||||
|
||||
export function neg(a) {
|
||||
return -a
|
||||
}
|
||||
|
||||
export function not(a) {
|
||||
return !a
|
||||
}
|
||||
|
||||
export function trunc(a) {
|
||||
return a < 0 ? Math.ceil(a) : Math.floor(a)
|
||||
}
|
||||
|
||||
export function random(a) {
|
||||
return Math.random() * (a || 1)
|
||||
}
|
||||
|
||||
export function factorial(a) { // a!
|
||||
return gamma(a + 1)
|
||||
}
|
||||
|
||||
export function isInteger(value) {
|
||||
return isFinite(value) && (value === Math.round(value))
|
||||
}
|
||||
|
||||
const GAMMA_G = 4.7421875
|
||||
const GAMMA_P = [
|
||||
0.99999999999999709182,
|
||||
57.156235665862923517, -59.597960355475491248,
|
||||
14.136097974741747174, -0.49191381609762019978,
|
||||
0.33994649984811888699e-4,
|
||||
0.46523628927048575665e-4, -0.98374475304879564677e-4,
|
||||
0.15808870322491248884e-3, -0.21026444172410488319e-3,
|
||||
0.21743961811521264320e-3, -0.16431810653676389022e-3,
|
||||
0.84418223983852743293e-4, -0.26190838401581408670e-4,
|
||||
0.36899182659531622704e-5
|
||||
]
|
||||
|
||||
// Gamma function from math.js
|
||||
export function gamma(n) {
|
||||
let t, x
|
||||
|
||||
if(isInteger(n)) {
|
||||
if(n <= 0) {
|
||||
return isFinite(n) ? Infinity : NaN
|
||||
}
|
||||
|
||||
if(n > 171) {
|
||||
return Infinity // Will overflow
|
||||
}
|
||||
|
||||
let value = n - 2
|
||||
let res = n - 1
|
||||
while(value > 1) {
|
||||
res *= value
|
||||
value--
|
||||
}
|
||||
|
||||
if(res === 0) {
|
||||
res = 1 // 0! is per definition 1
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
if(n < 0.5) {
|
||||
return Math.PI / (Math.sin(Math.PI * n) * gamma(1 - n))
|
||||
}
|
||||
|
||||
if(n >= 171.35) {
|
||||
return Infinity // will overflow
|
||||
}
|
||||
|
||||
if(n > 85.0) { // Extended Stirling Approx
|
||||
const twoN = n * n
|
||||
const threeN = twoN * n
|
||||
const fourN = threeN * n
|
||||
const fiveN = fourN * n
|
||||
return Math.sqrt(2 * Math.PI / n) * Math.pow((n / Math.E), n) *
|
||||
(1 + (1 / (12 * n)) + (1 / (288 * twoN)) - (139 / (51840 * threeN)) -
|
||||
(571 / (2488320 * fourN)) + (163879 / (209018880 * fiveN)) +
|
||||
(5246819 / (75246796800 * fiveN * n)))
|
||||
}
|
||||
|
||||
--n
|
||||
x = GAMMA_P[0]
|
||||
for(let i = 1; i < GAMMA_P.length; ++i) {
|
||||
x += GAMMA_P[i] / (n + i)
|
||||
}
|
||||
|
||||
t = n + GAMMA_G + 0.5
|
||||
return Math.sqrt(2 * Math.PI) * Math.pow(t, n + 0.5) * Math.exp(-t) * x
|
||||
}
|
||||
|
||||
export function stringOrArrayLength(s) {
|
||||
if(Array.isArray(s)) {
|
||||
return s.length
|
||||
}
|
||||
return String(s).length
|
||||
}
|
||||
|
||||
export function hypot() {
|
||||
let sum = 0
|
||||
let larg = 0
|
||||
for(let i = 0; i < arguments.length; i++) {
|
||||
const arg = Math.abs(arguments[i])
|
||||
let div
|
||||
if(larg < arg) {
|
||||
div = larg / arg
|
||||
sum = (sum * div * div) + 1
|
||||
larg = arg
|
||||
} else if(arg > 0) {
|
||||
div = arg / larg
|
||||
sum += div * div
|
||||
} else {
|
||||
sum += arg
|
||||
}
|
||||
}
|
||||
return larg === Infinity ? Infinity : larg * Math.sqrt(sum)
|
||||
}
|
||||
|
||||
export function condition(cond, yep, nope) {
|
||||
return cond ? yep : nope
|
||||
}
|
||||
|
||||
/**
|
||||
* Decimal adjustment of a number.
|
||||
* From @escopecz.
|
||||
*
|
||||
* @param {number} value - The number.
|
||||
* @param {Integer} exp - The exponent (the 10 logarithm of the adjustment base).
|
||||
* @return {number} - The adjusted value.
|
||||
*/
|
||||
export function roundTo(value, exp) {
|
||||
// If the exp is undefined or zero...
|
||||
if(typeof exp === "undefined" || +exp === 0) {
|
||||
return Math.round(value)
|
||||
}
|
||||
value = +value
|
||||
exp = -(+exp)
|
||||
// If the value is not a number or the exp is not an integer...
|
||||
if(isNaN(value) || !(typeof exp === "number" && exp % 1 === 0)) {
|
||||
return NaN
|
||||
}
|
||||
// Shift
|
||||
value = value.toString().split("e")
|
||||
value = Math.round(+(value[0] + "e" + (value[1] ? (+value[1] - exp) : -exp)))
|
||||
// Shift back
|
||||
value = value.toString().split("e")
|
||||
return +(value[0] + "e" + (value[1] ? (+value[1] + exp) : exp))
|
||||
}
|
||||
|
||||
export function setVar(name, value, variables) {
|
||||
if(variables) variables[name] = value
|
||||
return value
|
||||
}
|
||||
|
||||
export function arrayIndex(array, index) {
|
||||
return array[index | 0]
|
||||
}
|
||||
|
||||
export function max(array) {
|
||||
if(arguments.length === 1 && Array.isArray(array)) {
|
||||
return Math.max.apply(Math, array)
|
||||
} else if(arguments.length >= 1) {
|
||||
return Math.max.apply(Math, arguments)
|
||||
} else {
|
||||
throw new EvalError(qsTranslate("error", "Function %1 must have at least one argument.").arg("max"))
|
||||
}
|
||||
}
|
||||
|
||||
export function min(array) {
|
||||
if(arguments.length === 1 && Array.isArray(array)) {
|
||||
return Math.min.apply(Math, array)
|
||||
} else if(arguments.length >= 1) {
|
||||
return Math.min.apply(Math, arguments)
|
||||
} else {
|
||||
throw new EvalError(qsTranslate("error", "Function %1 must have at least one argument.").arg("min"))
|
||||
}
|
||||
}
|
||||
|
||||
export function arrayMap(f, a) {
|
||||
if(typeof f !== "function") {
|
||||
throw new EvalError(qsTranslate("error", "First argument to map is not a function."))
|
||||
}
|
||||
if(!Array.isArray(a)) {
|
||||
throw new EvalError(qsTranslate("error", "Second argument to map is not an array."))
|
||||
}
|
||||
return a.map(function(x, i) {
|
||||
return f(x, i)
|
||||
})
|
||||
}
|
||||
|
||||
export function arrayFold(f, init, a) {
|
||||
if(typeof f !== "function") {
|
||||
throw new EvalError(qsTranslate("error", "First argument to fold is not a function."))
|
||||
}
|
||||
if(!Array.isArray(a)) {
|
||||
throw new EvalError(qsTranslate("error", "Second argument to fold is not an array."))
|
||||
}
|
||||
return a.reduce(function(acc, x, i) {
|
||||
return f(acc, x, i)
|
||||
}, init)
|
||||
}
|
||||
|
||||
export function arrayFilter(f, a) {
|
||||
if(typeof f !== "function") {
|
||||
throw new EvalError(qsTranslate("error", "First argument to filter is not a function."))
|
||||
}
|
||||
if(!Array.isArray(a)) {
|
||||
throw new EvalError(qsTranslate("error", "Second argument to filter is not an array."))
|
||||
}
|
||||
return a.filter(function(x, i) {
|
||||
return f(x, i)
|
||||
})
|
||||
}
|
||||
|
||||
export function stringOrArrayIndexOf(target, s) {
|
||||
if(!(Array.isArray(s) || typeof s === "string")) {
|
||||
throw new Error(qsTranslate("error", "Second argument to indexOf is not a string or array."))
|
||||
}
|
||||
|
||||
return s.indexOf(target)
|
||||
}
|
||||
|
||||
export function arrayJoin(sep, a) {
|
||||
if(!Array.isArray(a)) {
|
||||
throw new Error(qsTranslate("error", "Second argument to join is not an array."))
|
||||
}
|
||||
|
||||
return a.join(sep)
|
||||
}
|
||||
|
||||
export function sign(x) {
|
||||
return ((x > 0) - (x < 0)) || +x
|
||||
}
|
||||
|
||||
const ONE_THIRD = 1 / 3
|
||||
|
||||
export function cbrt(x) {
|
||||
return x < 0 ? -Math.pow(-x, ONE_THIRD) : Math.pow(x, ONE_THIRD)
|
||||
}
|
||||
|
||||
export function expm1(x) {
|
||||
return Math.exp(x) - 1
|
||||
}
|
||||
|
||||
export function log1p(x) {
|
||||
return Math.log(1 + x)
|
||||
}
|
||||
|
||||
export function log2(x) {
|
||||
return Math.log(x) / Math.LN2
|
||||
}
|
575
common/src/lib/expr-eval/tokens.mjs
Normal file
575
common/src/lib/expr-eval/tokens.mjs
Normal file
|
@ -0,0 +1,575 @@
|
|||
/**
|
||||
* Based on ndef.parser, by Raphael Graf <r@undefined.ch>
|
||||
* http://www.undefined.ch/mparser/index.html
|
||||
*
|
||||
* Ported to JavaScript and modified by Matthew Crumley <email@matthewcrumley.com>
|
||||
* https://silentmatt.com/javascript-expression-evaluator/
|
||||
*
|
||||
* Ported to QMLJS with modifications done accordingly done by Ad5001 <mail@ad5001.eu> (https://ad5001.eu)
|
||||
*
|
||||
* Copyright (c) 2015 Matthew Crumley, 2021-2024 Ad5001
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
* You are free to use and modify this code in anyway you find useful. Please leave this comment in the code
|
||||
* to acknowledge its original source. If you feel like it, I enjoy hearing about projects that use my code,
|
||||
* but don't feel like you have to let me know or ask permission.
|
||||
*/
|
||||
|
||||
export const TEOF = "TEOF"
|
||||
export const TOP = "TOP"
|
||||
export const TNUMBER = "TNUMBER"
|
||||
export const TSTRING = "TSTRING"
|
||||
export const TPAREN = "TPAREN"
|
||||
export const TBRACKET = "TBRACKET"
|
||||
export const TCOMMA = "TCOMMA"
|
||||
export const TNAME = "TNAME"
|
||||
|
||||
|
||||
// Additional variable characters.
|
||||
export const ADDITIONAL_VARCHARS = [
|
||||
"α", "β", "γ", "δ", "ε", "ζ", "η",
|
||||
"π", "θ", "κ", "λ", "μ", "ξ", "ρ",
|
||||
"ς", "σ", "τ", "φ", "χ", "ψ", "ω",
|
||||
"Γ", "Δ", "Θ", "Λ", "Ξ", "Π", "Σ",
|
||||
"Φ", "Ψ", "Ω", "ₐ", "ₑ", "ₒ", "ₓ",
|
||||
"ₕ", "ₖ", "ₗ", "ₘ", "ₙ", "ₚ", "ₛ",
|
||||
"ₜ", "¹", "²", "³", "⁴", "⁵", "⁶",
|
||||
"⁷", "⁸", "⁹", "⁰", "₁", "₂", "₃",
|
||||
"₄", "₅", "₆", "₇", "₈", "₉", "₀",
|
||||
"∞", "π"
|
||||
]
|
||||
|
||||
export class Token {
|
||||
/**
|
||||
*
|
||||
* @param {string} type - Type of the token (see above).
|
||||
* @param {any} value - Value of the token.
|
||||
* @param {number} index - Index in the string of the token.
|
||||
*/
|
||||
constructor(type, value, index) {
|
||||
this.type = type
|
||||
this.value = value
|
||||
this.index = index
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.type + ": " + this.value
|
||||
}
|
||||
}
|
||||
|
||||
const unicodeCodePointPattern = /^[0-9a-f]{4}$/i
|
||||
|
||||
export class TokenStream {
|
||||
/**
|
||||
*
|
||||
* @param {Parser} parser
|
||||
* @param {string} expression
|
||||
*/
|
||||
constructor(parser, expression) {
|
||||
this.pos = 0
|
||||
this.current = null
|
||||
this.unaryOps = parser.unaryOps
|
||||
this.unaryOpsList = parser.unaryOpsList
|
||||
this.binaryOps = parser.binaryOps
|
||||
this.ternaryOps = parser.ternaryOps
|
||||
this.builtinConsts = parser.builtinConsts
|
||||
this.expression = expression
|
||||
this.savedPosition = 0
|
||||
this.savedCurrent = null
|
||||
this.options = parser.options
|
||||
this.parser = parser
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} type - Type of the token (see above).
|
||||
* @param {any} value - Value of the token.
|
||||
* @param {number} [pos] - Index in the string of the token.
|
||||
*/
|
||||
newToken(type, value, pos) {
|
||||
return new Token(type, value, pos != null ? pos : this.pos)
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the current position and token into the object.
|
||||
*/
|
||||
save() {
|
||||
this.savedPosition = this.pos
|
||||
this.savedCurrent = this.current
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Restored the saved position and token into the current.
|
||||
*/
|
||||
restore() {
|
||||
this.pos = this.savedPosition
|
||||
this.current = this.savedCurrent
|
||||
}
|
||||
|
||||
/**
|
||||
* Consumes the character at the current position and advance it
|
||||
* until it makes a valid token, and returns it.
|
||||
* @returns {Token}
|
||||
*/
|
||||
next() {
|
||||
if(this.pos >= this.expression.length) {
|
||||
return this.newToken(TEOF, "EOF")
|
||||
}
|
||||
|
||||
if(this.isWhitespace()) {
|
||||
return this.next()
|
||||
} else if(this.isRadixInteger() ||
|
||||
this.isNumber() ||
|
||||
this.isOperator() ||
|
||||
this.isString() ||
|
||||
this.isParen() ||
|
||||
this.isBracket() ||
|
||||
this.isComma() ||
|
||||
this.isNamedOp() ||
|
||||
this.isConst() ||
|
||||
this.isName()) {
|
||||
return this.current
|
||||
} else {
|
||||
this.parseError(qsTranslate("error", "Unknown character \"%1\".").arg(this.expression.charAt(this.pos)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the character at the current position starts a string, and if so, consumes it as the current token
|
||||
* and returns true. Otherwise, returns false.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isString() {
|
||||
const startPos = this.pos
|
||||
const quote = this.expression.charAt(startPos)
|
||||
let r = false
|
||||
|
||||
if(quote === "'" || quote === "\"") {
|
||||
let index = this.expression.indexOf(quote, startPos + 1)
|
||||
while(index >= 0 && this.pos < this.expression.length) {
|
||||
this.pos = index + 1
|
||||
if(this.expression.charAt(index - 1) !== "\\") {
|
||||
const rawString = this.expression.substring(startPos + 1, index)
|
||||
this.current = this.newToken(TSTRING, this.unescape(rawString), startPos)
|
||||
r = true
|
||||
break
|
||||
}
|
||||
index = this.expression.indexOf(quote, index + 1)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the character at the current pos is a parenthesis, and if so consumes it into current
|
||||
* and returns true. Otherwise, returns false.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isParen() {
|
||||
const c = this.expression.charAt(this.pos)
|
||||
if(c === "(" || c === ")") {
|
||||
this.current = this.newToken(TPAREN, c)
|
||||
this.pos++
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the character at the current pos is a bracket, and if so consumes it into current
|
||||
* and returns true. Otherwise, returns false.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isBracket() {
|
||||
const c = this.expression.charAt(this.pos)
|
||||
if((c === "[" || c === "]") && this.isOperatorEnabled("[")) {
|
||||
this.current = this.newToken(TBRACKET, c)
|
||||
this.pos++
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the character at the current pos is a comma, and if so consumes it into current
|
||||
* and returns true. Otherwise, returns false.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isComma() {
|
||||
const c = this.expression.charAt(this.pos)
|
||||
if(c === ",") {
|
||||
this.current = this.newToken(TCOMMA, ",")
|
||||
this.pos++
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current character is an identifier and makes a const, and if so, consumes it as the current token
|
||||
* and returns true. Otherwise, returns false.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isConst() {
|
||||
const startPos = this.pos
|
||||
let i = startPos
|
||||
for(; i < this.expression.length; i++) {
|
||||
const c = this.expression.charAt(i)
|
||||
if(c.toUpperCase() === c.toLowerCase() && !ADDITIONAL_VARCHARS.includes(c)) {
|
||||
if(i === this.pos || (c !== "_" && c !== "." && (c < "0" || c > "9"))) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if(i > startPos) {
|
||||
const str = this.expression.substring(startPos, i)
|
||||
if(str in this.builtinConsts) {
|
||||
this.current = this.newToken(TNUMBER, this.builtinConsts[str])
|
||||
this.pos += str.length
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current character is an identifier and makes a function or an operator, and if so, consumes it as the current token
|
||||
* and returns true. Otherwise, returns false.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isNamedOp() {
|
||||
const startPos = this.pos
|
||||
let i = startPos
|
||||
for(; i < this.expression.length; i++) {
|
||||
const c = this.expression.charAt(i)
|
||||
if(c.toUpperCase() === c.toLowerCase()) {
|
||||
if(i === this.pos || (c !== "_" && (c < "0" || c > "9"))) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if(i > startPos) {
|
||||
const str = this.expression.substring(startPos, i)
|
||||
if(this.isOperatorEnabled(str) && (str in this.binaryOps || str in this.unaryOps || str in this.ternaryOps)) {
|
||||
this.current = this.newToken(TOP, str)
|
||||
this.pos += str.length
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current character is an identifier and makes a variable, and if so, consumes it as the current token
|
||||
* and returns true. Otherwise, returns false.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isName() {
|
||||
const startPos = this.pos
|
||||
let i = startPos
|
||||
let hasLetter = false
|
||||
for(; i < this.expression.length; i++) {
|
||||
const c = this.expression.charAt(i)
|
||||
if(c.toUpperCase() === c.toLowerCase() && !ADDITIONAL_VARCHARS.includes(c)) {
|
||||
if(i === this.pos && (c === "$" || c === "_")) {
|
||||
if(c === "_") {
|
||||
hasLetter = true
|
||||
}
|
||||
} else if(i === this.pos || !hasLetter || (c !== "_" && (c < "0" || c > "9"))) {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
hasLetter = true
|
||||
}
|
||||
}
|
||||
if(hasLetter) {
|
||||
const str = this.expression.substring(startPos, i)
|
||||
this.current = this.newToken(TNAME, str)
|
||||
this.pos += str.length
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the character at the current position is a whitespace, and if so, consumes all consecutive whitespaces
|
||||
* and returns true. Otherwise, returns false.
|
||||
* @returns {boolean}
|
||||
*
|
||||
*/
|
||||
isWhitespace() {
|
||||
let r = false
|
||||
let c = this.expression.charAt(this.pos)
|
||||
while(c === " " || c === "\t" || c === "\n" || c === "\r") {
|
||||
r = true
|
||||
this.pos++
|
||||
if(this.pos >= this.expression.length) {
|
||||
break
|
||||
}
|
||||
c = this.expression.charAt(this.pos)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current character is a zero, and checks whether it forms a radix number, and if so, consumes it as the current token
|
||||
* and returns true. Otherwise, returns false.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isRadixInteger() {
|
||||
let pos = this.pos
|
||||
|
||||
if(pos >= this.expression.length - 2 || this.expression.charAt(pos) !== "0") {
|
||||
return false
|
||||
}
|
||||
++pos
|
||||
|
||||
let radix
|
||||
let validDigit
|
||||
if(this.expression.charAt(pos) === "x") {
|
||||
radix = 16
|
||||
validDigit = /^[0-9a-f]$/i
|
||||
pos++
|
||||
} else if(this.expression.charAt(pos) === "b") {
|
||||
radix = 2
|
||||
validDigit = /^[01]$/i
|
||||
pos++
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
let valid = false
|
||||
const startPos = pos
|
||||
|
||||
while(pos < this.expression.length) {
|
||||
const c = this.expression.charAt(pos)
|
||||
if(validDigit.test(c)) {
|
||||
pos++
|
||||
valid = true
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if(valid) {
|
||||
this.current = this.newToken(TNUMBER, parseInt(this.expression.substring(startPos, pos), radix))
|
||||
this.pos = pos
|
||||
}
|
||||
return valid
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current character is a digit, and checks whether it forms a number, and if so, consumes it as the current token
|
||||
* and returns true. Otherwise, returns false.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isNumber() {
|
||||
const startPos = this.pos
|
||||
let valid = false
|
||||
let pos = startPos
|
||||
let resetPos = startPos
|
||||
let foundDot = false
|
||||
let foundDigits = false
|
||||
let c
|
||||
|
||||
// Check for digit with dot.
|
||||
while(pos < this.expression.length) {
|
||||
c = this.expression.charAt(pos)
|
||||
if((c >= "0" && c <= "9") || (!foundDot && c === ".")) {
|
||||
if(c === ".") {
|
||||
foundDot = true
|
||||
} else {
|
||||
foundDigits = true
|
||||
}
|
||||
pos++
|
||||
valid = foundDigits
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if(valid) {
|
||||
resetPos = pos
|
||||
}
|
||||
|
||||
// Check for e exponents.
|
||||
if(c === "e" || c === "E") {
|
||||
pos++
|
||||
let acceptSign = true
|
||||
let validExponent = false
|
||||
while(pos < this.expression.length) {
|
||||
c = this.expression.charAt(pos)
|
||||
if(acceptSign && (c === "+" || c === "-")) {
|
||||
acceptSign = false
|
||||
} else if(c >= "0" && c <= "9") {
|
||||
validExponent = true
|
||||
acceptSign = false
|
||||
} else {
|
||||
break
|
||||
}
|
||||
pos++
|
||||
}
|
||||
|
||||
if(!validExponent) {
|
||||
pos = resetPos
|
||||
}
|
||||
}
|
||||
|
||||
// Use parseFloat now that we've identified the number.
|
||||
if(valid) {
|
||||
this.current = this.newToken(TNUMBER, parseFloat(this.expression.substring(startPos, pos)))
|
||||
this.pos = pos
|
||||
} else {
|
||||
this.pos = resetPos
|
||||
}
|
||||
return valid
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current character is an operator, checks whether it's enabled and if so, consumes it as the current token
|
||||
* and returns true. Otherwise, returns false.
|
||||
* @return {boolean}
|
||||
*/
|
||||
isOperator() {
|
||||
const startPos = this.pos
|
||||
const c = this.expression.charAt(this.pos)
|
||||
|
||||
if(c === "+" || c === "-" || c === "*" || c === "/" || c === "%" || c === "^" || c === "?" || c === ":" || c === ".") {
|
||||
this.current = this.newToken(TOP, c)
|
||||
} else if(c === "∙" || c === "•") {
|
||||
this.current = this.newToken(TOP, "*")
|
||||
} else if(c === ">") {
|
||||
if(this.expression.charAt(this.pos + 1) === "=") {
|
||||
this.current = this.newToken(TOP, ">=")
|
||||
this.pos++
|
||||
} else {
|
||||
this.current = this.newToken(TOP, ">")
|
||||
}
|
||||
} else if(c === "<") {
|
||||
if(this.expression.charAt(this.pos + 1) === "=") {
|
||||
this.current = this.newToken(TOP, "<=")
|
||||
this.pos++
|
||||
} else {
|
||||
this.current = this.newToken(TOP, "<")
|
||||
}
|
||||
} else if(c === "|") {
|
||||
if(this.expression.charAt(this.pos + 1) === "|") {
|
||||
this.current = this.newToken(TOP, "||")
|
||||
this.pos++
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else if(c === "=") {
|
||||
if(this.expression.charAt(this.pos + 1) === "=") {
|
||||
this.current = this.newToken(TOP, "==")
|
||||
this.pos++
|
||||
} else {
|
||||
this.current = this.newToken(TOP, c)
|
||||
}
|
||||
} else if(c === "!") {
|
||||
if(this.expression.charAt(this.pos + 1) === "=") {
|
||||
this.current = this.newToken(TOP, "!=")
|
||||
this.pos++
|
||||
} else {
|
||||
this.current = this.newToken(TOP, c)
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
this.pos++
|
||||
|
||||
if(this.isOperatorEnabled(this.current.value)) {
|
||||
return true
|
||||
} else {
|
||||
this.pos = startPos
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces a backslash and a character by its unescaped value.
|
||||
* @param {string} v - string to un escape.
|
||||
*/
|
||||
unescape(v) {
|
||||
let index = v.indexOf("\\")
|
||||
if(index < 0) {
|
||||
return v
|
||||
}
|
||||
|
||||
let buffer = v.substring(0, index)
|
||||
while(index >= 0) {
|
||||
const c = v.charAt(++index)
|
||||
switch(c) {
|
||||
case "'":
|
||||
buffer += "'"
|
||||
break
|
||||
case "\"":
|
||||
buffer += "\""
|
||||
break
|
||||
case "\\":
|
||||
buffer += "\\"
|
||||
break
|
||||
case "/":
|
||||
buffer += "/"
|
||||
break
|
||||
case "b":
|
||||
buffer += "\b"
|
||||
break
|
||||
case "f":
|
||||
buffer += "\f"
|
||||
break
|
||||
case "n":
|
||||
buffer += "\n"
|
||||
break
|
||||
case "r":
|
||||
buffer += "\r"
|
||||
break
|
||||
case "t":
|
||||
buffer += "\t"
|
||||
break
|
||||
case "u":
|
||||
// interpret the following 4 characters as the hex of the unicode code point
|
||||
const codePoint = v.substring(index + 1, index + 5)
|
||||
if(!unicodeCodePointPattern.test(codePoint)) {
|
||||
this.parseError(qsTranslate("error", "Illegal escape sequence: %1.").arg("\\u" + codePoint))
|
||||
}
|
||||
buffer += String.fromCharCode(parseInt(codePoint, 16))
|
||||
index += 4
|
||||
break
|
||||
default:
|
||||
throw this.parseError(qsTranslate("error", "Illegal escape sequence: %1.").arg("\\" + c))
|
||||
}
|
||||
++index
|
||||
const backslash = v.indexOf("\\", index)
|
||||
buffer += v.substring(index, backslash < 0 ? v.length : backslash)
|
||||
index = backslash
|
||||
}
|
||||
|
||||
return buffer
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorthand for the parser's method to check if an operator is enabled.
|
||||
* @param {string} op
|
||||
* @return {boolean}
|
||||
*/
|
||||
isOperatorEnabled(op) {
|
||||
return this.parser.isOperatorEnabled(op)
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws a translated error.
|
||||
* @param {string} msg
|
||||
*/
|
||||
parseError(msg) {
|
||||
throw new Error(qsTranslate("error", "Parse error [position %1]: %2").arg(this.pos).arg(msg))
|
||||
}
|
||||
}
|
126
common/src/lib/polyfills/js.mjs
Normal file
126
common/src/lib/polyfills/js.mjs
Normal file
|
@ -0,0 +1,126 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// JS polyfills to add because they're not implemented in the QML Scripting engine.
|
||||
// CoreJS does not work well with it (as well as doubles the compiled size), so this is preferable.
|
||||
function notPolyfilled(name) {
|
||||
return function() { throw new Error(`${name} not polyfilled`) }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} depth
|
||||
* @this {Array}
|
||||
* @returns {Array}
|
||||
*/
|
||||
function arrayFlat(depth = 1) {
|
||||
const newArray = []
|
||||
for(const element of this) {
|
||||
if(element instanceof Array)
|
||||
newArray.push(...(depth > 1 ? element.flat(depth - 1) : element))
|
||||
else
|
||||
newArray.push(element)
|
||||
}
|
||||
return newArray
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function(any, number, Array): any} callbackFn
|
||||
* @param {object} thisArg
|
||||
* @this {Array}
|
||||
* @returns {Array}
|
||||
*/
|
||||
function arrayFlatMap(callbackFn, thisArg) {
|
||||
const newArray = []
|
||||
for(let i = 0; i < this.length; i++) {
|
||||
const value = callbackFn.call(thisArg ?? this, this[i], i, this)
|
||||
if(value instanceof Array)
|
||||
newArray.push(...value)
|
||||
else
|
||||
newArray.push(value)
|
||||
}
|
||||
return newArray
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces all instances of from by to.
|
||||
* @param {string} from
|
||||
* @param {string} to
|
||||
* @this {string}
|
||||
* @return {String}
|
||||
*/
|
||||
function stringReplaceAll(from, to) {
|
||||
let str = this
|
||||
while(str.includes(from))
|
||||
str = str.replace(from, to)
|
||||
return str
|
||||
}
|
||||
|
||||
|
||||
const polyfills = {
|
||||
2017: [
|
||||
[Object, "entries", notPolyfilled("Object.entries")],
|
||||
[Object, "values", notPolyfilled("Object.values")],
|
||||
[Object, "getOwnPropertyDescriptors", notPolyfilled("Object.getOwnPropertyDescriptors")],
|
||||
[String.prototype, "padStart", notPolyfilled("String.prototype.padStart")],
|
||||
[String.prototype, "padEnd", notPolyfilled("String.prototype.padEnd")]
|
||||
],
|
||||
2018: [
|
||||
[Promise.prototype, "finally", notPolyfilled("Object.entries")]
|
||||
],
|
||||
2019: [
|
||||
[String.prototype, "trimStart", notPolyfilled("String.prototype.trimStart")],
|
||||
[String.prototype, "trimEnd", notPolyfilled("String.prototype.trimEnd")],
|
||||
[Object, "fromEntries", notPolyfilled("Object.fromEntries")],
|
||||
[Array.prototype, "flat", arrayFlat],
|
||||
[Array.prototype, "flatMap", arrayFlatMap]
|
||||
],
|
||||
2020: [
|
||||
[String.prototype, "matchAll", notPolyfilled("String.prototype.matchAll")],
|
||||
[Promise, "allSettled", notPolyfilled("Promise.allSettled")]
|
||||
],
|
||||
2021: [
|
||||
[Promise, "any", notPolyfilled("Promise.any")],
|
||||
[String.prototype, "replaceAll", stringReplaceAll]
|
||||
],
|
||||
2022: [
|
||||
[Array.prototype, "at", notPolyfilled("Array.prototype.at")],
|
||||
[String.prototype, "at", notPolyfilled("String.prototype.at")],
|
||||
[Object, "hasOwn", notPolyfilled("Object.hasOwn")]
|
||||
],
|
||||
2023: [
|
||||
[Array.prototype, "findLast", notPolyfilled("Array.prototype.findLast")],
|
||||
[Array.prototype, "toReversed", notPolyfilled("Array.prototype.toReversed")],
|
||||
[Array.prototype, "toSorted", notPolyfilled("Array.prototype.toSorted")],
|
||||
[Array.prototype, "toSpliced", notPolyfilled("Array.prototype.toSpliced")],
|
||||
[Array.prototype, "with", notPolyfilled("Array.prototype.with")]
|
||||
],
|
||||
2024: [
|
||||
[Object, "groupBy", notPolyfilled("Object.groupBy")],
|
||||
[Map, "groupBy", notPolyfilled("Map.groupBy")]
|
||||
]
|
||||
}
|
||||
|
||||
// Fulfill polyfill.
|
||||
for(const [year, entries] of Object.entries(polyfills)) {
|
||||
const defined = entries.filter(x => x[0][x[1]] !== undefined)
|
||||
console.info(`ES${year} support: ${defined.length === entries.length} (${defined.length}/${entries.length})`)
|
||||
// Apply polyfills
|
||||
for(const [context, functionName, polyfill] of entries.filter(x => x[0][x[1]] === undefined)) {
|
||||
context[functionName] = polyfill
|
||||
}
|
||||
}
|
46
common/src/lib/polyfills/qt.mjs
Normal file
46
common/src/lib/polyfills/qt.mjs
Normal file
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// Type polyfills for IDEs.
|
||||
// Never directly imported.
|
||||
// Might need to be reimplemented in other implemententations.
|
||||
|
||||
Modules = Modules || {}
|
||||
/** @type {function(string, string): string} */
|
||||
qsTranslate = qsTranslate || function(category, string) { throw new Error('qsTranslate not implemented.'); }
|
||||
/** @type {function(string): string} */
|
||||
qsTr = qsTr || function(string) { throw new Error('qsTr not implemented.'); }
|
||||
/** @type {function(string, string): string} */
|
||||
QT_TRANSLATE_NOOP = QT_TRANSLATE_NOOP || function(category, string) { throw new Error('QT_TRANSLATE_NOOP not implemented.'); }
|
||||
/** @type {function(string): string} */
|
||||
QT_TR_NOOP = QT_TR_NOOP || function(string) { throw new Error('QT_TR_NOOP not implemented.'); }
|
||||
/** @type {function(string|boolean|int): string} */
|
||||
String.prototype.arg = String.prototype.arg || function(parameter) { throw new Error('arg not implemented.'); }
|
||||
|
||||
const Qt = {
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} width
|
||||
* @param {number} height
|
||||
* @returns {{x, width, y, height}}
|
||||
*/
|
||||
rect: function(x, y, width, height) {
|
||||
return {x: x, y: y, width: width, height: height};
|
||||
}
|
||||
}
|
635
common/src/math/domain.mjs
Normal file
635
common/src/math/domain.mjs
Normal file
|
@ -0,0 +1,635 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Expression, executeExpression } from "./expression.mjs"
|
||||
|
||||
/**
|
||||
* Main abstract domain class
|
||||
* It doesn't represent any kind of domain and is meant to be extended.
|
||||
*/
|
||||
export class Domain {
|
||||
constructor() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether x is included in the domain.
|
||||
* @param {number} x - The x value.
|
||||
* @return {boolean} true if included, false otherwise.
|
||||
*/
|
||||
includes(x) {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the domain.
|
||||
* @return {string} String representation of the domain.
|
||||
*/
|
||||
toString() {
|
||||
return "???"
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new domain that is the union between this domain and another.
|
||||
* @param {Domain} domain - Domain to unionise with this.
|
||||
* @return {Domain} newly created domain.
|
||||
*/
|
||||
union(domain) {
|
||||
return domain
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new domain that is the intersection between this domain and another.
|
||||
* @param {Domain} domain - Domain to get the interscection with this.
|
||||
* @return {Domain} newly created domain.
|
||||
*/
|
||||
intersection(domain) {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a domain from a string.
|
||||
* @return {Domain} Found domain, string otherwise.
|
||||
*/
|
||||
static import(frm) {
|
||||
switch(frm.trim().toUpperCase()) {
|
||||
case "R":
|
||||
case "ℝ":
|
||||
return Domain.R
|
||||
case "RE":
|
||||
case "R*":
|
||||
case "ℝ*":
|
||||
return Domain.RE
|
||||
case "RP":
|
||||
case "R+":
|
||||
case "ℝ⁺":
|
||||
case "ℝ+":
|
||||
return Domain.RP
|
||||
case "RM":
|
||||
case "R-":
|
||||
case "ℝ⁻":
|
||||
case "ℝ-":
|
||||
return Domain.RM
|
||||
case "RPE":
|
||||
case "REP":
|
||||
case "R+*":
|
||||
case "R*+":
|
||||
case "ℝ*⁺":
|
||||
case "ℝ⁺*":
|
||||
case "ℝ*+":
|
||||
case "ℝ+*":
|
||||
return Domain.RPE
|
||||
case "RME":
|
||||
case "REM":
|
||||
case "R-*":
|
||||
case "R*-":
|
||||
case "ℝ⁻*":
|
||||
case "ℝ*⁻":
|
||||
case "ℝ-*":
|
||||
case "ℝ*-":
|
||||
return Domain.RME
|
||||
case "ℕ":
|
||||
case "N":
|
||||
case "ZP":
|
||||
case "Z+":
|
||||
case "ℤ⁺":
|
||||
case "ℤ+":
|
||||
return Domain.N
|
||||
case "NLOG":
|
||||
case "ℕˡᵒᵍ":
|
||||
case "ℕLOG":
|
||||
return Domain.NLog
|
||||
case "NE":
|
||||
case "NP":
|
||||
case "N*":
|
||||
case "N+":
|
||||
case "ℕ*":
|
||||
case "ℕ⁺":
|
||||
case "ℕ+":
|
||||
case "ZPE":
|
||||
case "ZEP":
|
||||
case "Z+*":
|
||||
case "Z*+":
|
||||
case "ℤ⁺*":
|
||||
case "ℤ*⁺":
|
||||
case "ℤ+*":
|
||||
case "ℤ*+":
|
||||
return Domain.NE
|
||||
case "Z":
|
||||
case "ℤ":
|
||||
return Domain.Z
|
||||
case "ZM":
|
||||
case "Z-":
|
||||
case "ℤ⁻":
|
||||
case "ℤ-":
|
||||
return Domain.ZM
|
||||
case "ZME":
|
||||
case "ZEM":
|
||||
case "Z-*":
|
||||
case "Z*-":
|
||||
case "ℤ⁻*":
|
||||
case "ℤ*⁻":
|
||||
case "ℤ-*":
|
||||
case "ℤ*-":
|
||||
return Domain.ZME
|
||||
case "ZE":
|
||||
case "Z*":
|
||||
case "ℤ*":
|
||||
return Domain.ZE
|
||||
default:
|
||||
return Domain.EmptySet
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an empty set.
|
||||
*/
|
||||
export class EmptySet extends Domain {
|
||||
constructor() {
|
||||
super()
|
||||
this.displayName = "∅"
|
||||
this.latexMarkup = "\\emptyset"
|
||||
}
|
||||
|
||||
includes(x) {
|
||||
return false
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.displayName
|
||||
}
|
||||
|
||||
union(domain) {
|
||||
return domain
|
||||
}
|
||||
|
||||
intersection(domain) {
|
||||
return this
|
||||
}
|
||||
|
||||
static import(frm) {
|
||||
return new EmptySet()
|
||||
}
|
||||
}
|
||||
|
||||
Domain.EmptySet = new EmptySet() // To prevent use prior to declaration.
|
||||
|
||||
/**
|
||||
* Domain classes for ranges (e.g ]0;3[, [1;2[ ...)
|
||||
*/
|
||||
export class Range extends Domain {
|
||||
constructor(begin, end, openBegin, openEnd) {
|
||||
super()
|
||||
if(typeof begin == "number" || typeof begin == "string") begin = new Expression(begin.toString())
|
||||
this.begin = begin
|
||||
if(typeof end == "number" || typeof end == "string") end = new Expression(end.toString())
|
||||
this.end = end
|
||||
this.openBegin = openBegin
|
||||
this.openEnd = openEnd
|
||||
this.displayName = (openBegin ? "]" : "[") + begin.toString() + ";" + end.toString() + (openEnd ? "[" : "]")
|
||||
this.latexMarkup = `\\mathopen${openBegin ? "]" : "["}${this.begin.latexMarkup};${this.end.latexMarkup}\\mathclose${openEnd ? "[" : "]"}`
|
||||
}
|
||||
|
||||
includes(x) {
|
||||
if(x instanceof Expression) x = x.execute()
|
||||
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()))
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.displayName
|
||||
}
|
||||
|
||||
union(domain) {
|
||||
if(domain instanceof EmptySet) return this
|
||||
if(domain instanceof DomainSet) return domain.union(this)
|
||||
if(domain instanceof UnionDomain) return domain.union(this)
|
||||
if(domain instanceof IntersectionDomain) return new UnionDomain(this, domain)
|
||||
if(domain instanceof MinusDomain) return new UnionDomain(this, domain)
|
||||
if(domain instanceof Range) return new UnionDomain(this, domain)
|
||||
}
|
||||
|
||||
intersection(domain) {
|
||||
if(domain instanceof EmptySet) return domain
|
||||
if(domain instanceof DomainSet) return domain.intersection(this)
|
||||
if(domain instanceof UnionDomain) return new IntersectionDomain(this, domain)
|
||||
if(domain instanceof IntersectionDomain) return domain.intersection(this)
|
||||
if(domain instanceof MinusDomain) return new IntersectionDomain(this, domain)
|
||||
if(domain instanceof Range) return new IntersectionDomain(this, domain)
|
||||
}
|
||||
|
||||
static import(frm) {
|
||||
let openBegin = frm.trim().charAt(0) === "]"
|
||||
let openEnd = frm.trim().charAt(frm.length - 1) === "["
|
||||
let [begin, end] = frm.substring(1, frm.length - 1).split(";")
|
||||
return new Range(begin.trim(), end.trim(), openBegin, openEnd)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Domain classes for special domains (N, Z, ...)
|
||||
*/
|
||||
export class SpecialDomain extends Domain {
|
||||
/**
|
||||
* @constructs SpecialDomain
|
||||
* @param {string} displayName
|
||||
* @param {function} isValid - function returning true when number is in domain false when it isn't.
|
||||
* @param {function} next - function provides the next positive value in the domain after the one given.
|
||||
* @param {function} previous - function provides the previous positive value in the domain before the one given.
|
||||
* @param {boolean} moveSupported - Only true if next and previous functions are valid.
|
||||
*/
|
||||
constructor(displayName, isValid, next = () => true, previous = () => true,
|
||||
moveSupported = true) {
|
||||
super()
|
||||
this.displayName = displayName
|
||||
this.isValid = isValid
|
||||
this.nextValue = next
|
||||
this.prevValue = previous
|
||||
this.moveSupported = moveSupported
|
||||
}
|
||||
|
||||
includes(x) {
|
||||
if(x instanceof Expression) x = x.execute()
|
||||
if(typeof x == "string") x = executeExpression(x)
|
||||
return this.isValid(x)
|
||||
}
|
||||
|
||||
next(x) {
|
||||
if(x instanceof Expression) x = x.execute()
|
||||
if(typeof x == "string") x = executeExpression(x)
|
||||
return this.nextValue(x)
|
||||
}
|
||||
|
||||
previous(x) {
|
||||
if(x instanceof Expression) x = x.execute()
|
||||
if(typeof x == "string") x = executeExpression(x)
|
||||
return this.prevValue(x)
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.displayName
|
||||
}
|
||||
|
||||
union(domain) {
|
||||
if(domain instanceof EmptySet) return this
|
||||
if(domain instanceof DomainSet) return domain.union(this)
|
||||
if(domain instanceof UnionDomain) return new UnionDomain(this, domain)
|
||||
if(domain instanceof IntersectionDomain) return new UnionDomain(this, domain)
|
||||
if(domain instanceof MinusDomain) return new UnionDomain(this, domain)
|
||||
if(domain instanceof Range) return new UnionDomain(this, domain)
|
||||
}
|
||||
|
||||
intersection(domain) {
|
||||
if(domain instanceof EmptySet) return domain
|
||||
if(domain instanceof DomainSet) return domain.intersection(this)
|
||||
if(domain instanceof UnionDomain) return new IntersectionDomain(this, domain)
|
||||
if(domain instanceof IntersectionDomain) return new IntersectionDomain(this, domain)
|
||||
if(domain instanceof MinusDomain) return new IntersectionDomain(this, domain)
|
||||
if(domain instanceof Range) return new IntersectionDomain(this, domain)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Domain classes for sets (e.g {0;3}, {0;1;2;pi} ...)
|
||||
*/
|
||||
export class DomainSet extends SpecialDomain {
|
||||
constructor(values) {
|
||||
super("", () => true, x => x, true)
|
||||
let newVals = {}
|
||||
this.executedValues = []
|
||||
for(let value of values) {
|
||||
let expr = new Expression(value.toString())
|
||||
let ex = expr.execute()
|
||||
newVals[ex] = expr
|
||||
this.executedValues.push(ex)
|
||||
}
|
||||
this.executedValues.sort((a, b) => a - b)
|
||||
this.values = this.executedValues.map(val => newVals[val])
|
||||
this.displayName = "{" + this.values.join(";") + "}"
|
||||
this.latexMarkup = `\\{${this.values.join(";")}\\}`
|
||||
}
|
||||
|
||||
includes(x) {
|
||||
if(x instanceof Expression) x = x.execute()
|
||||
if(typeof x == "string") x = executeExpression(x)
|
||||
for(let value of this.values)
|
||||
if(x === value.execute()) return true
|
||||
return false
|
||||
}
|
||||
|
||||
next(x) {
|
||||
if(x instanceof Expression) x = x.execute()
|
||||
if(typeof x == "string") x = executeExpression(x)
|
||||
if(x < this.executedValues[0]) return this.executedValues[0]
|
||||
for(let i = 1; i < this.values.length; i++) {
|
||||
let prevValue = this.executedValues[i - 1]
|
||||
let value = this.executedValues[i]
|
||||
if(x >= prevValue && x < value) return value
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
previous(x) {
|
||||
if(x instanceof Expression) x = x.execute()
|
||||
if(typeof x == "string") x = executeExpression(x)
|
||||
if(x > this.executedValues[this.executedValues.length - 1])
|
||||
return this.executedValues[this.executedValues.length - 1]
|
||||
for(let i = 1; i < this.values.length; i++) {
|
||||
let prevValue = this.executedValues[i - 1]
|
||||
let value = this.executedValues[i]
|
||||
if(x > prevValue && x <= value) return prevValue
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.displayName
|
||||
}
|
||||
|
||||
union(domain) {
|
||||
if(domain instanceof EmptySet) return this
|
||||
if(domain instanceof DomainSet) {
|
||||
let newValues = []
|
||||
let values = this.values.concat(domain.values).filter(function(val) {
|
||||
newValues.push(val.execute())
|
||||
return newValues.indexOf(val.execute()) === newValues.length - 1
|
||||
})
|
||||
return new DomainSet(values)
|
||||
}
|
||||
let notIncludedValues = []
|
||||
for(let i = 0; i < this.values.length; i++) {
|
||||
let value = this.executedValues[i]
|
||||
if(domain instanceof Range) {
|
||||
if(domain.begin.execute() === value && domain.openBegin) {
|
||||
domain.openBegin = false
|
||||
}
|
||||
if(domain.end.execute() === value && domain.openEnd) {
|
||||
domain.openEnd = false
|
||||
}
|
||||
}
|
||||
if(!domain.includes(value))
|
||||
notIncludedValues.push(this.values[i].toEditableString())
|
||||
}
|
||||
if(notIncludedValues.length === 0) return domain
|
||||
return new UnionDomain(domain, new DomainSet(notIncludedValues))
|
||||
}
|
||||
|
||||
intersection(domain) {
|
||||
if(domain instanceof EmptySet) return domain
|
||||
if(domain instanceof DomainSet) {
|
||||
let domValues = domain.values.map(expr => expr.execute())
|
||||
this.values = this.values.filter(function(val) {
|
||||
return domValues.indexOf(val.execute()) >= 0
|
||||
})
|
||||
return this
|
||||
}
|
||||
let includedValues = []
|
||||
for(let i in this.values) {
|
||||
let value = this.executedValues[i]
|
||||
if(domain instanceof Range) {
|
||||
if(domain.begin.execute() === value && !domain.openBegin) {
|
||||
domain.openBegin = false
|
||||
}
|
||||
if(domain.end.execute() === value && !domain.openEnd) {
|
||||
domain.openEnd = false
|
||||
}
|
||||
}
|
||||
if(domain.includes(value))
|
||||
includedValues.push(this.values[i].toEditableString())
|
||||
}
|
||||
if(includedValues.length === 0) return new EmptySet()
|
||||
if(includedValues.length === this.values.length) return this
|
||||
return new IntersectionDomain(domain, new DomainSet(includedValues))
|
||||
}
|
||||
|
||||
static import(frm) {
|
||||
return new DomainSet(frm.substring(1, frm.length - 1).split(";"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Domain representing the union between two domains.
|
||||
*/
|
||||
export class UnionDomain extends Domain {
|
||||
constructor(dom1, dom2) {
|
||||
super()
|
||||
this.dom1 = dom1
|
||||
this.dom2 = dom2
|
||||
this.displayName = this.dom1.toString() + " ∪ " + this.dom2.toString()
|
||||
this.latexMarkup = `${dom1.latexMarkup}\\cup${dom2.latexMarkup}`
|
||||
}
|
||||
|
||||
includes(x) {
|
||||
return this.dom1.includes(x) || this.dom2.includes(x)
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.displayName
|
||||
}
|
||||
|
||||
union(domain) {
|
||||
if(domain instanceof EmptySet) return this
|
||||
if(domain instanceof DomainSet) return domain.union(this)
|
||||
if(domain instanceof Range) return domain.union(this)
|
||||
if(domain instanceof UnionDomain) return new UnionDomain(this, domain)
|
||||
if(domain instanceof IntersectionDomain) return new UnionDomain(this, domain)
|
||||
if(domain instanceof MinusDomain) return new MinusDomain(this, domain)
|
||||
}
|
||||
|
||||
intersection(domain) {
|
||||
if(domain instanceof EmptySet) return domain
|
||||
if(domain instanceof DomainSet) return domain.intersection(this)
|
||||
if(domain instanceof UnionDomain) return new IntersectionDomain(this, domain)
|
||||
if(domain instanceof IntersectionDomain) return this.dom1.intersection(domain.dom1).intersection(this.dom2).intersection(domain.dom2)
|
||||
if(domain instanceof MinusDomain) return new IntersectionDomain(this, domain)
|
||||
}
|
||||
|
||||
static import(frm) {
|
||||
let domains = frm.trim().split("∪")
|
||||
if(domains.length === 1) domains = frm.trim().split("U") // Fallback
|
||||
let dom2 = parseDomain(domains.pop())
|
||||
let dom1 = parseDomain(domains.join("∪"))
|
||||
return dom1.union(dom2)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Domain representing the intersection between two domains.
|
||||
*/
|
||||
export class IntersectionDomain extends Domain {
|
||||
constructor(dom1, dom2) {
|
||||
super()
|
||||
this.dom1 = dom1
|
||||
this.dom2 = dom2
|
||||
this.displayName = dom1.toString() + " ∩ " + dom2.toString()
|
||||
this.latexMarkup = `${dom1.latexMarkup}\\cap${dom2.latexMarkup}`
|
||||
}
|
||||
|
||||
includes(x) {
|
||||
return this.dom1.includes(x) && this.dom2.includes(x)
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.displayName
|
||||
}
|
||||
|
||||
union(domain) {
|
||||
if(domain instanceof EmptySet) return this
|
||||
if(domain instanceof DomainSet) return domain.union(this)
|
||||
if(domain instanceof Range) return domain.union(this)
|
||||
if(domain instanceof UnionDomain) return this.dom1.union(domain.dom1).union(this.dom2).union(domain.dom2)
|
||||
if(domain instanceof IntersectionDomain) return new UnionDomain(this, domain)
|
||||
if(domain instanceof MinusDomain) return new MinusDomain(this, domain)
|
||||
}
|
||||
|
||||
intersection(domain) {
|
||||
if(domain instanceof EmptySet) return domain
|
||||
if(domain instanceof DomainSet) return domain.intersection(this)
|
||||
if(domain instanceof UnionDomain) return new IntersectionDomain(this, domain)
|
||||
if(domain instanceof IntersectionDomain) return new IntersectionDomain(this, domain)
|
||||
if(domain instanceof MinusDomain) return new IntersectionDomain(this, domain)
|
||||
}
|
||||
|
||||
static import(frm) {
|
||||
let domains = frm.trim().split("∩")
|
||||
let dom1 = parseDomain(domains.pop())
|
||||
let dom2 = parseDomain(domains.join("∩"))
|
||||
return dom1.intersection(dom2)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Domain representing the minus between two domains.
|
||||
*/
|
||||
export class MinusDomain extends Domain {
|
||||
constructor(dom1, dom2) {
|
||||
super()
|
||||
this.dom1 = dom1
|
||||
this.dom2 = dom2
|
||||
this.displayName = dom1.toString() + "∖" + dom2.toString()
|
||||
this.latexMarkup = `${dom1.latexMarkup}\\setminus${dom2.latexMarkup}`
|
||||
}
|
||||
|
||||
includes(x) {
|
||||
return this.dom1.includes(x) && !this.dom2.includes(x)
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.displayName
|
||||
}
|
||||
|
||||
static import(frm) {
|
||||
let domains = frm.trim().split("∖")
|
||||
if(domains.length === 1) domains = frm.trim().split("\\") // Fallback
|
||||
let dom1 = parseDomain(domains.shift())
|
||||
let dom2 = parseDomain(domains.join("∪"))
|
||||
return new MinusDomain(dom1, dom2)
|
||||
}
|
||||
}
|
||||
|
||||
Domain.RE = new MinusDomain("R", "{0}")
|
||||
Domain.RE.displayName = "ℝ*"
|
||||
Domain.RE.latexMarkup = "\\mathbb{R}^{*}"
|
||||
|
||||
Domain.R = new Range(-Infinity, Infinity, true, true)
|
||||
Domain.R.displayName = "ℝ"
|
||||
Domain.R.latexMarkup = "\\mathbb{R}"
|
||||
Domain.RP = new Range(0, Infinity, true, false)
|
||||
Domain.RP.displayName = "ℝ⁺"
|
||||
Domain.RP.latexMarkup = "\\mathbb{R}^{+}"
|
||||
Domain.RM = new Range(-Infinity, 0, true, false)
|
||||
Domain.RM.displayName = "ℝ⁻"
|
||||
Domain.RM.latexMarkup = "\\mathbb{R}^{-}"
|
||||
Domain.RPE = new Range(0, Infinity, true, true)
|
||||
Domain.RPE.displayName = "ℝ⁺*"
|
||||
Domain.RPE.latexMarkup = "\\mathbb{R}^{+*}"
|
||||
Domain.RME = new Range(-Infinity, 0, true, true)
|
||||
Domain.RME.displayName = "ℝ⁻*"
|
||||
Domain.RME.latexMarkup = "\\mathbb{R}^{+*}"
|
||||
Domain.N = new SpecialDomain("ℕ", x => x % 1 === 0 && x >= 0,
|
||||
x => Math.max(Math.floor(x) + 1, 0),
|
||||
x => Math.max(Math.ceil(x) - 1, 0))
|
||||
Domain.N.latexMarkup = "\\mathbb{N}"
|
||||
Domain.NE = new SpecialDomain("ℕ*", x => x % 1 === 0 && x > 0,
|
||||
x => Math.max(Math.floor(x) + 1, 1),
|
||||
x => Math.max(Math.ceil(x) - 1, 1))
|
||||
Domain.NE.latexMarkup = "\\mathbb{N}^{*}"
|
||||
Domain.Z = new SpecialDomain("ℤ", x => x % 1 === 0, x => Math.floor(x) + 1, x => Math.ceil(x) - 1)
|
||||
Domain.Z.latexMarkup = "\\mathbb{Z}"
|
||||
Domain.ZE = new SpecialDomain("ℤ*", x => x % 1 === 0 && x !== 0,
|
||||
x => Math.floor(x) + 1 === 0 ? Math.floor(x) + 2 : Math.floor(x) + 1,
|
||||
x => Math.ceil(x) - 1 === 0 ? Math.ceil(x) - 2 : Math.ceil(x) - 1)
|
||||
Domain.ZE.latexMarkup = "\\mathbb{Z}^{*}"
|
||||
Domain.ZM = new SpecialDomain("ℤ⁻", x => x % 1 === 0 && x <= 0,
|
||||
x => Math.min(Math.floor(x) + 1, 0),
|
||||
x => Math.min(Math.ceil(x) - 1, 0))
|
||||
Domain.ZM.latexMarkup = "\\mathbb{Z}^{-}"
|
||||
Domain.ZME = new SpecialDomain("ℤ⁻*", x => x % 1 === 0 && x < 0,
|
||||
x => Math.min(Math.floor(x) + 1, -1),
|
||||
x => Math.min(Math.ceil(x) - 1, -1))
|
||||
Domain.ZME.latexMarkup = "\\mathbb{Z}^{-*}"
|
||||
Domain.NLog = new SpecialDomain("ℕˡᵒᵍ",
|
||||
x => x / Math.pow(10, Math.ceil(Math.log10(x))) % 1 === 0 && x > 0,
|
||||
function(x) {
|
||||
let x10pow = Math.pow(10, Math.ceil(Math.log10(x)))
|
||||
return Math.max(1, (Math.floor(x / x10pow) + 1) * x10pow)
|
||||
},
|
||||
function(x) {
|
||||
let x10pow = Math.pow(10, Math.ceil(Math.log10(x)))
|
||||
return Math.max(1, (Math.ceil(x / x10pow) - 1) * x10pow)
|
||||
})
|
||||
Domain.NLog.latexMarkup = "\\mathbb{N}^{log}"
|
||||
|
||||
let refedDomains = []
|
||||
|
||||
/**
|
||||
* Parses a domain, that can use parentheses.
|
||||
* e.g (N ∪ [-1;0[) ∩ (Z \ {0;3})
|
||||
* @param {string} domain - string of the domain to be parsed.
|
||||
* @returns {Domain} Parsed domain.
|
||||
*/
|
||||
export function parseDomain(domain) {
|
||||
if(!domain.includes(")") && !domain.includes("(")) return parseDomainSimple(domain)
|
||||
let domStr
|
||||
while((domStr = /\(([^)(]+)\)/.exec(domain)) !== null) {
|
||||
let dom = parseDomainSimple(domStr[1].trim())
|
||||
domain = domain.replace(domStr[0], "D" + refedDomains.length)
|
||||
refedDomains.push(dom)
|
||||
}
|
||||
return parseDomainSimple(domain)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a domain, without parentheses.
|
||||
* e.g N ∪ [-1;0[, Z \ {0;3}, N+*...
|
||||
* @param {string} domain - string of the domain to be parsed.
|
||||
* @returns {Domain} Parsed domain.
|
||||
*/
|
||||
export function parseDomainSimple(domain) {
|
||||
domain = domain.trim()
|
||||
if(domain.includes("U") || domain.includes("∪")) return UnionDomain.import(domain)
|
||||
if(domain.includes("∩")) return IntersectionDomain.import(domain)
|
||||
if(domain.includes("∖") || domain.includes("\\")) return MinusDomain.import(domain)
|
||||
if(domain.charAt(0) === "{" && domain.charAt(domain.length - 1) === "}") return DomainSet.import(domain)
|
||||
if(domain.includes("]") || domain.includes("[")) return Range.import(domain)
|
||||
if(["R", "ℝ", "N", "ℕ", "Z", "ℤ"].some(str => domain.toUpperCase().includes(str)))
|
||||
return Domain.import(domain)
|
||||
if(domain[0] === "D") return refedDomains[parseInt(domain.substring(1))]
|
||||
return new EmptySet()
|
||||
}
|
126
common/src/math/expression.mjs
Normal file
126
common/src/math/expression.mjs
Normal file
|
@ -0,0 +1,126 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
import * as Utils from "../utils.mjs"
|
||||
import Latex from "../module/latex.mjs"
|
||||
import ExprParser from "../module/expreval.mjs"
|
||||
import Objects from "../module/objects.mjs"
|
||||
|
||||
/**
|
||||
* Represents any kind of x-based or non variable based expression.
|
||||
*/
|
||||
export class Expression {
|
||||
constructor(expr) {
|
||||
if(typeof expr === "string") {
|
||||
this.expr = Utils.exponentsToExpression(expr)
|
||||
this.calc = ExprParser.parse(this.expr).simplify()
|
||||
} else {
|
||||
// Passed an expression here directly.
|
||||
this.calc = expr.simplify()
|
||||
this.expr = expr.toString()
|
||||
}
|
||||
this.cached = this.isConstant()
|
||||
this.cachedValue = null
|
||||
if(this.cached && this.allRequirementsFulfilled())
|
||||
this.cachedValue = this.calc.evaluate(Objects.currentObjectsByName)
|
||||
this.latexMarkup = Latex.expression(this.calc.tokens)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all the variables used in calc
|
||||
* @return {string[]}
|
||||
*/
|
||||
variables() {
|
||||
return this.calc.variables()
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current expression is constant (does not depend on a variable, be it x or n).
|
||||
* @return {boolean}
|
||||
*/
|
||||
isConstant() {
|
||||
let vars = this.calc.variables()
|
||||
return !vars.includes("x") && !vars.includes("n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of object names this expression is dependant on.
|
||||
* @return {string[]}
|
||||
*/
|
||||
requiredObjects() {
|
||||
return this.calc.variables().filter(objName => objName !== "x" && objName !== "n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if all the objects required for this expression are defined.
|
||||
* @return {boolean}
|
||||
*/
|
||||
allRequirementsFulfilled() {
|
||||
return this.requiredObjects().every(objName => objName in Objects.currentObjectsByName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of names whose corresponding objects this expression is dependant on and are missing.
|
||||
* @return {boolean}
|
||||
*/
|
||||
undefinedVariables() {
|
||||
return this.requiredObjects().filter(objName => !(objName in Objects.currentObjectsByName))
|
||||
}
|
||||
|
||||
recache() {
|
||||
if(this.cached)
|
||||
this.cachedValue = this.calc.evaluate(Objects.currentObjectsByName)
|
||||
}
|
||||
|
||||
execute(x = 1) {
|
||||
if(this.cached) {
|
||||
if(this.cachedValue == null)
|
||||
this.cachedValue = this.calc.evaluate(Objects.currentObjectsByName)
|
||||
return this.cachedValue
|
||||
}
|
||||
ExprParser.currentVars = Object.assign({ "x": x }, Objects.currentObjectsByName)
|
||||
return this.calc.evaluate(ExprParser.currentVars)
|
||||
}
|
||||
|
||||
simplify(x) {
|
||||
let expr = this.calc.substitute("x", x).simplify()
|
||||
if(expr.evaluate() === 0) expr = "0"
|
||||
return new Expression(expr)
|
||||
}
|
||||
|
||||
toEditableString() {
|
||||
return this.calc.toString()
|
||||
}
|
||||
|
||||
toString(forceSign = false) {
|
||||
let str = Utils.makeExpressionReadable(this.calc.toString())
|
||||
if(str !== undefined && str.match(/^\d*\.\d+$/)) {
|
||||
if(str.split(".")[1].split("0").length > 7) {
|
||||
// Likely rounding error
|
||||
str = parseFloat(str.substring(0, str.length - 1)).toString()
|
||||
}
|
||||
}
|
||||
if(str[0] !== "-" && forceSign) str = "+" + str
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
export function executeExpression(expr) {
|
||||
return (new Expression(expr.toString())).execute()
|
||||
}
|
40
common/src/math/index.mjs
Normal file
40
common/src/math/index.mjs
Normal file
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
import * as Expr from "./expression.mjs"
|
||||
import * as Seq from "./sequence.mjs"
|
||||
import * as Dom from "./domain.mjs"
|
||||
|
||||
|
||||
export const Expression = Expr.Expression
|
||||
export const executeExpression = Expr.executeExpression
|
||||
export const Sequence = Seq.Sequence
|
||||
|
||||
// Domains
|
||||
export const Domain = Dom.Domain
|
||||
export const EmptySet = Dom.EmptySet
|
||||
export const Range = Dom.Range
|
||||
export const SpecialDomain = Dom.SpecialDomain
|
||||
export const DomainSet = Dom.DomainSet
|
||||
export const UnionDomain = Dom.UnionDomain
|
||||
export const IntersectionDomain = Dom.IntersectionDomain
|
||||
export const MinusDomain = Dom.MinusDomain
|
||||
|
||||
export const parseDomain = Dom.parseDomain
|
||||
export const parseDomainSimple = Dom.parseDomainSimple
|
98
common/src/math/sequence.mjs
Normal file
98
common/src/math/sequence.mjs
Normal file
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as Expr from "./expression.mjs"
|
||||
import * as Utils from "../utils.mjs"
|
||||
import Latex from "../module/latex.mjs"
|
||||
import Objects from "../module/objects.mjs"
|
||||
import ExprParser from "../module/expreval.mjs"
|
||||
|
||||
/**
|
||||
* Represents mathematical object for sequences.
|
||||
*/
|
||||
export class Sequence extends Expr.Expression {
|
||||
constructor(name, baseValues = {}, valuePlus = 1, expr = "") {
|
||||
// u[n+valuePlus] = expr
|
||||
super(expr)
|
||||
this.name = name
|
||||
this.baseValues = baseValues
|
||||
this.calcValues = Object.assign({}, baseValues)
|
||||
this.latexValues = Object.assign({}, baseValues)
|
||||
for(let n in this.calcValues)
|
||||
if(['string', 'number'].includes(typeof this.calcValues[n])) {
|
||||
let parsed = ExprParser.parse(this.calcValues[n].toString()).simplify()
|
||||
this.latexValues[n] = Latex.expression(parsed.tokens)
|
||||
this.calcValues[n] = parsed.evaluate()
|
||||
}
|
||||
this.valuePlus = parseInt(valuePlus)
|
||||
}
|
||||
|
||||
isConstant() {
|
||||
return this.expr.indexOf("n") === -1
|
||||
}
|
||||
|
||||
execute(n = 1) {
|
||||
if(n in this.calcValues)
|
||||
return this.calcValues[n]
|
||||
this.cache(n)
|
||||
return this.calcValues[n]
|
||||
}
|
||||
|
||||
simplify(n = 1) {
|
||||
if(!(n in this.calcValues))
|
||||
this.cache(n)
|
||||
return this.calcValues[n].toString()
|
||||
}
|
||||
|
||||
cache(n = 1) {
|
||||
let str = Utils.simplifyExpression(this.calc.substitute('n', n-this.valuePlus).toString())
|
||||
let expr = ExprParser.parse(str).simplify()
|
||||
// Cache values required for this one.
|
||||
if(!this.calcValues[n-this.valuePlus] && n-this.valuePlus > 0)
|
||||
this.cache(n-this.valuePlus)
|
||||
// Setting current variables
|
||||
ExprParser.currentVars = Object.assign(
|
||||
{'n': n-this.valuePlus}, // Just in case, add n (for custom functions)
|
||||
Objects.currentObjectsByName,
|
||||
{[this.name]: this.calcValues}
|
||||
)
|
||||
this.calcValues[n] = expr.evaluate(ExprParser.currentVars)
|
||||
}
|
||||
|
||||
toString(forceSign=false) {
|
||||
let str = Utils.makeExpressionReadable(this.calc.toString())
|
||||
if(str[0] !== '-' && forceSign) str = '+' + str
|
||||
let subtxt = this.valuePlus === 0 ? 'ₙ' : Utils.textsub('n+' + this.valuePlus)
|
||||
let 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
|
||||
}
|
||||
|
||||
toLatexString(forceSign=false) {
|
||||
let str = this.latexMarkup
|
||||
if(str[0] !== '-' && forceSign) str = '+' + str
|
||||
let subtxt = '_{n' + (this.valuePlus === 0 ? '' : '+' + this.valuePlus) + '}'
|
||||
let ret = `\\begin{array}{l}${Latex.variable(this.name)}${subtxt} = ${str}${this.latexValues.length === 0 ? '' : "\n"}\\\\`
|
||||
ret += Object.keys(this.latexValues).map(
|
||||
n => `${this.name}_{${n}} = ${this.latexValues[n]}`
|
||||
).join('; ') + "\\end{array}"
|
||||
return ret
|
||||
}
|
||||
}
|
594
common/src/module/canvas.mjs
Normal file
594
common/src/module/canvas.mjs
Normal file
|
@ -0,0 +1,594 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Module } from "./common.mjs"
|
||||
import { CanvasInterface, DialogInterface } from "./interface.mjs"
|
||||
import { textsup } from "../utils.mjs"
|
||||
import { Expression } from "../math/index.mjs"
|
||||
import Latex from "./latex.mjs"
|
||||
import Objects from "./objects.mjs"
|
||||
import History from "./history.mjs"
|
||||
|
||||
class CanvasAPI extends Module {
|
||||
constructor() {
|
||||
super("Canvas", {
|
||||
canvas: CanvasInterface,
|
||||
drawingErrorDialog: DialogInterface
|
||||
})
|
||||
|
||||
/** @type {CanvasInterface} */
|
||||
this._canvas = null
|
||||
|
||||
/** @type {CanvasRenderingContext2D} */
|
||||
this._ctx = null
|
||||
|
||||
/**
|
||||
* @type {{show(string, string, string)}}
|
||||
* @private
|
||||
*/
|
||||
this._drawingErrorDialog = null
|
||||
/**
|
||||
*
|
||||
* @type {Object.<string, {expression: Expression, value: number, maxDraw: number}>}
|
||||
*/
|
||||
this.axesSteps = {
|
||||
x: {
|
||||
expression: null,
|
||||
value: -1,
|
||||
maxDraw: -1
|
||||
},
|
||||
y: {
|
||||
expression: null,
|
||||
value: -1,
|
||||
maxDraw: -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the module.
|
||||
* @param {CanvasInterface} canvas
|
||||
* @param {{show(string, string, string)}} drawingErrorDialog
|
||||
*/
|
||||
initialize({ canvas, drawingErrorDialog }) {
|
||||
super.initialize({ canvas, drawingErrorDialog })
|
||||
this._canvas = canvas
|
||||
this._drawingErrorDialog = drawingErrorDialog
|
||||
}
|
||||
|
||||
get width() {
|
||||
if(!this.initialized) throw new Error("Attempting width before initialize!")
|
||||
return this._canvas.width
|
||||
}
|
||||
|
||||
get height() {
|
||||
if(!this.initialized) throw new Error("Attempting height before initialize!")
|
||||
return this._canvas.height
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimum x of the diagram, provided from settings.
|
||||
* @returns {number}
|
||||
*/
|
||||
get xmin() {
|
||||
if(!this.initialized) throw new Error("Attempting xmin before initialize!")
|
||||
return this._canvas.xmin
|
||||
}
|
||||
|
||||
/**
|
||||
* Zoom on the x-axis of the diagram, provided from settings.
|
||||
* @returns {number}
|
||||
*/
|
||||
get xzoom() {
|
||||
if(!this.initialized) throw new Error("Attempting xzoom before initialize!")
|
||||
return this._canvas.xzoom
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum y of the diagram, provided from settings.
|
||||
* @returns {number}
|
||||
*/
|
||||
get ymax() {
|
||||
if(!this.initialized) throw new Error("Attempting ymax before initialize!")
|
||||
return this._canvas.ymax
|
||||
}
|
||||
|
||||
/**
|
||||
* Zoom on the y-axis of the diagram, provided from settings.
|
||||
* @returns {number}
|
||||
*/
|
||||
get yzoom() {
|
||||
if(!this.initialized) throw new Error("Attempting yzoom before initialize!")
|
||||
return this._canvas.yzoom
|
||||
}
|
||||
|
||||
/**
|
||||
* Label used on the x-axis, provided from settings.
|
||||
* @returns {string}
|
||||
*/
|
||||
get xlabel() {
|
||||
if(!this.initialized) throw new Error("Attempting xlabel before initialize!")
|
||||
return this._canvas.xlabel
|
||||
}
|
||||
|
||||
/**
|
||||
* Label used on the y-axis, provided from settings.
|
||||
* @returns {string}
|
||||
*/
|
||||
get ylabel() {
|
||||
if(!this.initialized) throw new Error("Attempting ylabel before initialize!")
|
||||
return this._canvas.ylabel
|
||||
}
|
||||
|
||||
/**
|
||||
* Width of lines that will be drawn into the canvas, provided from settings.
|
||||
* @returns {number}
|
||||
*/
|
||||
get linewidth() {
|
||||
if(!this.initialized) throw new Error("Attempting linewidth before initialize!")
|
||||
return this._canvas.linewidth
|
||||
}
|
||||
|
||||
/**
|
||||
* Font size of the text that will be drawn into the canvas, provided from settings.
|
||||
* @returns {number}
|
||||
*/
|
||||
get textsize() {
|
||||
if(!this.initialized) throw new Error("Attempting textsize before initialize!")
|
||||
return this._canvas.textsize
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the canvas should be in logarithmic mode, false otherwise.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get logscalex() {
|
||||
if(!this.initialized) throw new Error("Attempting logscalex before initialize!")
|
||||
return this._canvas.logscalex
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the x graduation should be shown, false otherwise.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get showxgrad() {
|
||||
if(!this.initialized) throw new Error("Attempting showxgrad before initialize!")
|
||||
return this._canvas.showxgrad
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the y graduation should be shown, false otherwise.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get showygrad() {
|
||||
if(!this.initialized) throw new Error("Attempting showygrad before initialize!")
|
||||
return this._canvas.showygrad
|
||||
}
|
||||
|
||||
/**
|
||||
* Max power of the logarithmic scaled on the x axis in logarithmic mode.
|
||||
* @returns {number}
|
||||
*/
|
||||
get maxgradx() {
|
||||
if(!this.initialized) throw new Error("Attempting maxgradx before initialize!")
|
||||
return Math.min(
|
||||
309, // 10e309 = Infinity (beyond this land be dragons)
|
||||
Math.max(
|
||||
Math.ceil(Math.abs(Math.log10(this.xmin))),
|
||||
Math.ceil(Math.abs(Math.log10(this.px2x(this.width))))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
//
|
||||
// Methods to draw the canvas
|
||||
//
|
||||
|
||||
requestPaint() {
|
||||
if(!this.initialized) throw new Error("Attempting requestPaint before initialize!")
|
||||
this._canvas.requestPaint()
|
||||
}
|
||||
|
||||
/**
|
||||
* Redraws the entire canvas
|
||||
*/
|
||||
redraw() {
|
||||
if(!this.initialized) throw new Error("Attempting redraw before initialize!")
|
||||
this._ctx = this._canvas.getContext("2d")
|
||||
this._computeAxes()
|
||||
this._reset()
|
||||
this._drawGrid()
|
||||
this._drawAxes()
|
||||
this._drawLabels()
|
||||
this._ctx.lineWidth = this.linewidth
|
||||
for(let objType in Objects.currentObjects) {
|
||||
for(let obj of Objects.currentObjects[objType]) {
|
||||
this._ctx.strokeStyle = obj.color
|
||||
this._ctx.fillStyle = obj.color
|
||||
if(obj.visible)
|
||||
try {
|
||||
obj.draw(this)
|
||||
} catch(e) {
|
||||
// Drawing throws an error. Generally, it's due to a new modification (or the opening of a file)
|
||||
console.error(e)
|
||||
console.log(e.stack)
|
||||
this._drawingErrorDialog.show(objType, obj.name, e.message)
|
||||
History.undo()
|
||||
}
|
||||
}
|
||||
}
|
||||
this._ctx.lineWidth = 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates information for drawing gradations for axes.
|
||||
* @private
|
||||
*/
|
||||
_computeAxes() {
|
||||
let exprY = new Expression(`x*(${this._canvas.yaxisstep})`)
|
||||
let y1 = exprY.execute(1)
|
||||
let exprX = new Expression(`x*(${this._canvas.xaxisstep})`)
|
||||
let x1 = exprX.execute(1)
|
||||
this.axesSteps = {
|
||||
x: {
|
||||
expression: exprX,
|
||||
value: x1,
|
||||
maxDraw: Math.ceil(Math.max(Math.abs(this.xmin), Math.abs(this.px2x(this.width))) / x1)
|
||||
},
|
||||
y: {
|
||||
expression: exprY,
|
||||
value: y1,
|
||||
maxDraw: Math.ceil(Math.max(Math.abs(this.ymax), Math.abs(this.px2y(this.height))) / y1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the canvas to a blank one with default setting.
|
||||
* @private
|
||||
*/
|
||||
_reset() {
|
||||
// Reset
|
||||
this._ctx.fillStyle = "#FFFFFF"
|
||||
this._ctx.strokeStyle = "#000000"
|
||||
this._ctx.font = `${this.textsize}px sans-serif`
|
||||
this._ctx.fillRect(0, 0, this.width, this.height)
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the grid.
|
||||
* @private
|
||||
*/
|
||||
_drawGrid() {
|
||||
this._ctx.strokeStyle = "#C0C0C0"
|
||||
if(this.logscalex) {
|
||||
for(let xpow = -this.maxgradx; xpow <= this.maxgradx; xpow++) {
|
||||
for(let xmulti = 1; xmulti < 10; xmulti++) {
|
||||
this.drawXLine(Math.pow(10, xpow) * xmulti)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for(let x = 0; x < this.axesSteps.x.maxDraw; x += 1) {
|
||||
this.drawXLine(x * this.axesSteps.x.value)
|
||||
this.drawXLine(-x * this.axesSteps.x.value)
|
||||
}
|
||||
}
|
||||
for(let y = 0; y < this.axesSteps.y.maxDraw; y += 1) {
|
||||
this.drawYLine(y * this.axesSteps.y.value)
|
||||
this.drawYLine(-y * this.axesSteps.y.value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the graph axes.
|
||||
* @private
|
||||
*/
|
||||
_drawAxes() {
|
||||
this._ctx.strokeStyle = "#000000"
|
||||
let axisypos = this.logscalex ? 1 : 0
|
||||
this.drawXLine(axisypos)
|
||||
this.drawYLine(0)
|
||||
let axisypx = this.x2px(axisypos) // X coordinate of Y axis
|
||||
let axisxpx = this.y2px(0) // Y coordinate of X axis
|
||||
// Drawing arrows
|
||||
this.drawLine(axisypx, 0, axisypx - 10, 10)
|
||||
this.drawLine(axisypx, 0, axisypx + 10, 10)
|
||||
this.drawLine(this.width, axisxpx, this.width - 10, axisxpx - 10)
|
||||
this.drawLine(this.width, axisxpx, this.width - 10, axisxpx + 10)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the canvas to a blank one with default setting.
|
||||
* @private
|
||||
*/
|
||||
_drawLabels() {
|
||||
let axisypx = this.x2px(this.logscalex ? 1 : 0) // X coordinate of Y axis
|
||||
let axisxpx = this.y2px(0) // Y coordinate of X axis
|
||||
// Labels
|
||||
this._ctx.fillStyle = "#000000"
|
||||
this._ctx.font = `${this.textsize}px sans-serif`
|
||||
this._ctx.fillText(this.ylabel, axisypx + 10, 24)
|
||||
let textWidth = this._ctx.measureText(this.xlabel).width
|
||||
this._ctx.fillText(this.xlabel, this.width - 14 - textWidth, axisxpx - 5)
|
||||
// Axis graduation labels
|
||||
this._ctx.font = `${this.textsize - 4}px sans-serif`
|
||||
|
||||
let txtMinus = this._ctx.measureText("-").width
|
||||
if(this.showxgrad) {
|
||||
if(this.logscalex) {
|
||||
for(let xpow = -this.maxgradx; xpow <= this.maxgradx; xpow += 1) {
|
||||
textWidth = this._ctx.measureText("10" + textsup(xpow)).width
|
||||
if(xpow !== 0)
|
||||
this.drawVisibleText("10" + textsup(xpow), this.x2px(Math.pow(10, xpow)) - textWidth / 2, axisxpx + 16 + (6 * (xpow === 1)))
|
||||
}
|
||||
} else {
|
||||
for(let x = 1; x < this.axesSteps.x.maxDraw; x += 1) {
|
||||
let drawX = x * this.axesSteps.x.value
|
||||
let txtX = this.axesSteps.x.expression.simplify(x).toString().replace(/^\((.+)\)$/, "$1")
|
||||
let textHeight = this.measureText(txtX).height
|
||||
this.drawVisibleText(txtX, this.x2px(drawX) - 4, axisxpx + this.textsize / 2 + textHeight)
|
||||
this.drawVisibleText("-" + txtX, this.x2px(-drawX) - 4, axisxpx + this.textsize / 2 + textHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
if(this.showygrad) {
|
||||
for(let y = 0; y < this.axesSteps.y.maxDraw; y += 1) {
|
||||
let drawY = y * this.axesSteps.y.value
|
||||
let txtY = this.axesSteps.y.expression.simplify(y).toString().replace(/^\((.+)\)$/, "$1")
|
||||
textWidth = this._ctx.measureText(txtY).width
|
||||
this.drawVisibleText(txtY, axisypx - 6 - textWidth, this.y2px(drawY) + 4 + (10 * (y === 0)))
|
||||
if(y !== 0)
|
||||
this.drawVisibleText("-" + txtY, axisypx - 6 - textWidth - txtMinus, this.y2px(-drawY) + 4)
|
||||
}
|
||||
}
|
||||
this._ctx.fillStyle = "#FFFFFF"
|
||||
}
|
||||
|
||||
//
|
||||
// Public functions
|
||||
//
|
||||
|
||||
/**
|
||||
* Draws an horizontal line at x plot coordinate.
|
||||
* @param {number} x
|
||||
*/
|
||||
drawXLine(x) {
|
||||
if(this.isVisible(x, this.ymax)) {
|
||||
this.drawLine(this.x2px(x), 0, this.x2px(x), this.height)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws an vertical line at y plot coordinate
|
||||
* @param {number} y
|
||||
* @private
|
||||
*/
|
||||
drawYLine(y) {
|
||||
if(this.isVisible(this.xmin, y)) {
|
||||
this.drawLine(0, this.y2px(y), this.width, this.y2px(y))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes multiline text onto the canvas.
|
||||
* NOTE: The x and y properties here are relative to the canvas, not the plot.
|
||||
* @param {string} text
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
*/
|
||||
drawVisibleText(text, x, y) {
|
||||
if(x > 0 && x < this.width && y > 0 && y < this.height) {
|
||||
text.toString().split("\n").forEach((txt, i) => {
|
||||
this._ctx.fillText(txt, x, y + (this.textsize * i))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws an image onto the canvas.
|
||||
* NOTE: The x, y width and height properties here are relative to the canvas, not the plot.
|
||||
* @param {CanvasImageSource} image
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} width
|
||||
* @param {number} height
|
||||
*/
|
||||
drawVisibleImage(image, x, y, width, height) {
|
||||
this._canvas.markDirty(Qt.rect(x, y, width, height))
|
||||
this._ctx.drawImage(image, x, y, width, height)
|
||||
}
|
||||
|
||||
/**
|
||||
* Measures the width and height of a multiline text that would be drawn onto the canvas.
|
||||
* @param {string} text
|
||||
* @returns {{width: number, height: number}}
|
||||
*/
|
||||
measureText(text) {
|
||||
let theight = 0
|
||||
let twidth = 0
|
||||
let defaultHeight = this.textsize * 1.2 // Approximate but good enough!
|
||||
for(let txt of text.split("\n")) {
|
||||
theight += defaultHeight
|
||||
if(this._ctx.measureText(txt).width > twidth) twidth = this._ctx.measureText(txt).width
|
||||
}
|
||||
return { "width": twidth, "height": theight }
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an x coordinate to its relative position on the canvas.
|
||||
* It supports both logarithmic and non-logarithmic scale depending on the currently selected mode.
|
||||
* @param {number} x
|
||||
* @returns {number}
|
||||
*/
|
||||
x2px(x) {
|
||||
if(this.logscalex) {
|
||||
const logxmin = Math.log(this.xmin)
|
||||
return (Math.log(x) - logxmin) * this.xzoom
|
||||
} else
|
||||
return (x - this.xmin) * this.xzoom
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an y coordinate to it's relative position on the canvas.
|
||||
* The y-axis not supporting logarithmic scale, it only supports linear conversion.
|
||||
* @param {number} y
|
||||
* @returns {number}
|
||||
*/
|
||||
y2px(y) {
|
||||
return (this.ymax - y) * this.yzoom
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an x px position on the canvas to it's corresponding coordinate on the plot.
|
||||
* It supports both logarithmic and non-logarithmic scale depending on the currently selected mode.
|
||||
* @param {number} px
|
||||
* @returns {number}
|
||||
*/
|
||||
px2x(px) {
|
||||
if(this.logscalex) {
|
||||
return Math.exp(px / this.xzoom + Math.log(this.xmin))
|
||||
} else
|
||||
return (px / this.xzoom + this.xmin)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an x px position on the canvas to it's corresponding coordinate on the plot.
|
||||
* It supports both logarithmic and non logarithmic scale depending on the currently selected mode.
|
||||
* @param {number} px
|
||||
* @returns {number}
|
||||
*/
|
||||
px2y(px) {
|
||||
return -(px / this.yzoom - this.ymax)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a plot point (x, y) is visible or not on the canvas.
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isVisible(x, y) {
|
||||
return (this.x2px(x) >= 0 && this.x2px(x) <= this.width) && (this.y2px(y) >= 0 && this.y2px(y) <= this.height)
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a line from plot point (x1, y1) to plot point (x2, y2).
|
||||
* @param {number} x1
|
||||
* @param {number} y1
|
||||
* @param {number} x2
|
||||
* @param {number} y2
|
||||
*/
|
||||
drawLine(x1, y1, x2, y2) {
|
||||
this._ctx.beginPath()
|
||||
this._ctx.moveTo(x1, y1)
|
||||
this._ctx.lineTo(x2, y2)
|
||||
this._ctx.stroke()
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a dashed line from plot point (x1, y1) to plot point (x2, y2).
|
||||
* @param {number} x1
|
||||
* @param {number} y1
|
||||
* @param {number} x2
|
||||
* @param {number} y2
|
||||
* @param {number} dashPxSize
|
||||
*/
|
||||
drawDashedLine(x1, y1, x2, y2, dashPxSize = 6) {
|
||||
this._ctx.setLineDash([dashPxSize / 2, dashPxSize])
|
||||
this.drawLine(x1, y1, x2, y2)
|
||||
this._ctx.setLineDash([])
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders latex markup ltxText to an image and loads it. Returns a dictionary with three values: source, width and height.
|
||||
* @param {string} ltxText
|
||||
* @param {string} color
|
||||
* @param {function(LatexRenderResult|{width: number, height: number, source: string})} callback
|
||||
*/
|
||||
renderLatexImage(ltxText, color, callback) {
|
||||
const onRendered = (imgData) => {
|
||||
if(!this._canvas.isImageLoaded(imgData.source) && !this._canvas.isImageLoading(imgData.source)) {
|
||||
// Wait until the image is loaded to callback.
|
||||
this._canvas.loadImage(imgData.source)
|
||||
this._canvas.imageLoaders[imgData.source] = [callback, imgData]
|
||||
} else {
|
||||
// Callback directly
|
||||
callback(imgData)
|
||||
}
|
||||
}
|
||||
const prerendered = Latex.findPrerendered(ltxText, this.textsize, color)
|
||||
if(prerendered !== null)
|
||||
onRendered(prerendered)
|
||||
else
|
||||
Latex.requestAsyncRender(ltxText, this.textsize, color).then(onRendered)
|
||||
}
|
||||
|
||||
//
|
||||
// Context methods
|
||||
//
|
||||
|
||||
get font() {
|
||||
return this._ctx.font
|
||||
}
|
||||
|
||||
set font(value) {
|
||||
return this._ctx.font = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws an act on the canvas centered on a point.
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} radius
|
||||
* @param {number} startAngle
|
||||
* @param {number} endAngle
|
||||
* @param {boolean} counterclockwise
|
||||
*/
|
||||
arc(x, y, radius, startAngle, endAngle, counterclockwise = false) {
|
||||
this._ctx.beginPath()
|
||||
this._ctx.arc(x, y, radius, startAngle, endAngle, counterclockwise)
|
||||
this._ctx.stroke()
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a filled circle centered on a point.
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} radius
|
||||
*/
|
||||
disc(x, y, radius) {
|
||||
this._ctx.beginPath()
|
||||
this._ctx.arc(x, y, radius, 0, 2 * Math.PI)
|
||||
this._ctx.fill()
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a filled rectangle onto the canvas.
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} w
|
||||
* @param {number} h
|
||||
*/
|
||||
fillRect(x, y, w, h) {
|
||||
this._ctx.fillRect(x, y, w, h)
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {CanvasAPI} */
|
||||
Modules.Canvas = Modules.Canvas || new CanvasAPI()
|
||||
export default Modules.Canvas
|
60
common/src/module/common.mjs
Normal file
60
common/src/module/common.mjs
Normal file
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Interface } from "./interface.mjs"
|
||||
|
||||
// Define Modules interface before they are imported.
|
||||
globalThis.Modules = globalThis.Modules || {}
|
||||
|
||||
/**
|
||||
* Base class for global APIs in runtime.
|
||||
*/
|
||||
export class Module {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} name - Name of the API
|
||||
* @param {Object.<string, (Interface|string|number|boolean)>} initializationParameters - List of parameters for the initialize function.
|
||||
*/
|
||||
constructor(name, initializationParameters = {}) {
|
||||
console.log(`Loading module ${name}...`)
|
||||
this.__name = name
|
||||
this.__initializationParameters = initializationParameters
|
||||
this.initialized = false
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if all requirements are defined.
|
||||
* @param {Object.<string, any>} options
|
||||
*/
|
||||
initialize(options) {
|
||||
if(this.initialized)
|
||||
throw new Error(`Cannot reinitialize module ${this.__name}.`)
|
||||
console.log(`Initializing ${this.__name}...`)
|
||||
for(const [name, value] of Object.entries(this.__initializationParameters)) {
|
||||
if(!options.hasOwnProperty(name))
|
||||
throw new Error(`Option '${name}' of initialize of module ${this.__name} does not exist.`)
|
||||
if(typeof value === "function" && value.prototype instanceof Interface)
|
||||
Interface.check_implementation(value, options[name])
|
||||
else if(typeof value !== typeof options[name])
|
||||
throw new Error(`Option '${name}' of initialize of module ${this.__name} is not a '${value}' (${typeof options[name]}).`)
|
||||
}
|
||||
this.initialized = true
|
||||
}
|
||||
}
|
114
common/src/module/expreval.mjs
Normal file
114
common/src/module/expreval.mjs
Normal file
|
@ -0,0 +1,114 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Module } from "./common.mjs"
|
||||
import { Parser } from "../lib/expr-eval/parser.mjs"
|
||||
|
||||
const evalVariables = {
|
||||
// Variables not provided by expr-eval.js, needs to be provided manually
|
||||
"pi": Math.PI,
|
||||
"PI": Math.PI,
|
||||
"π": Math.PI,
|
||||
"inf": Infinity,
|
||||
"infinity": Infinity,
|
||||
"Infinity": Infinity,
|
||||
"∞": Infinity,
|
||||
"e": Math.E,
|
||||
"E": Math.E,
|
||||
"true": true,
|
||||
"false": false
|
||||
}
|
||||
|
||||
class ExprParserAPI extends Module {
|
||||
constructor() {
|
||||
super("ExprParser")
|
||||
this.currentVars = {}
|
||||
this._parser = new Parser()
|
||||
|
||||
this._parser.consts = Object.assign({}, this._parser.consts, evalVariables)
|
||||
|
||||
this._parser.functions.integral = this.integral.bind(this)
|
||||
this._parser.functions.derivative = this.derivative.bind(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses arguments for a function, returns the corresponding JS function if it exists.
|
||||
* Throws either usage error otherwise.
|
||||
* @param {array} args - Arguments of the function, either [ ExecutableObject ] or [ string, variable ].
|
||||
* @param {string} usage1 - Usage for executable object.
|
||||
* @param {string} usage2 - Usage for string function.
|
||||
* @return {function} JS function to call.
|
||||
*/
|
||||
parseArgumentsForFunction(args, usage1, usage2) {
|
||||
let f, variable
|
||||
if(args.length === 1) {
|
||||
// Parse object
|
||||
f = args[0]
|
||||
if(typeof f !== "object" || !f.execute)
|
||||
throw EvalError(qsTranslate("usage", "Usage:\n%1").arg(usage1))
|
||||
let target = f
|
||||
f = (x) => target.execute(x)
|
||||
} else if(args.length === 2) {
|
||||
// Parse variable
|
||||
[f, variable] = args
|
||||
if(typeof f !== "string" || typeof variable !== "string")
|
||||
throw EvalError(qsTranslate("usage", "Usage:\n%1").arg(usage2))
|
||||
f = this._parser.parse(f).toJSFunction(variable, this.currentVars)
|
||||
} else
|
||||
throw EvalError(qsTranslate("usage", "Usage:\n%1\n%2").arg(usage1).arg(usage2))
|
||||
return f
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} expression - Expression to parse
|
||||
* @returns {ExprEvalExpression}
|
||||
*/
|
||||
parse(expression) {
|
||||
return this._parser.parse(expression)
|
||||
}
|
||||
|
||||
integral(a, b, ...args) {
|
||||
let usage1 = qsTranslate("usage", "integral(<from: number>, <to: number>, <f: ExecutableObject>)")
|
||||
let usage2 = qsTranslate("usage", "integral(<from: number>, <to: number>, <f: string>, <variable: string>)")
|
||||
let f = this.parseArgumentsForFunction(args, usage1, usage2)
|
||||
if(a == null || b == null)
|
||||
throw EvalError(qsTranslate("usage", "Usage:\n%1\n%2").arg(usage1).arg(usage2))
|
||||
|
||||
// https://en.wikipedia.org/wiki/Simpson%27s_rule
|
||||
// Simpler, faster than tokenizing the expression
|
||||
return (b - a) / 6 * (f(a) + 4 * f((a + b) / 2) + f(b))
|
||||
}
|
||||
|
||||
derivative(...args) {
|
||||
let usage1 = qsTranslate("usage", "derivative(<f: ExecutableObject>, <x: number>)")
|
||||
let usage2 = qsTranslate("usage", "derivative(<f: string>, <variable: string>, <x: number>)")
|
||||
let x = args.pop()
|
||||
let f = this.parseArgumentsForFunction(args, usage1, usage2)
|
||||
if(x == null)
|
||||
throw EvalError(qsTranslate("usage", "Usage:\n%1\n%2").arg(usage1).arg(usage2))
|
||||
|
||||
let derivative_precision = x / 10
|
||||
return (f(x + derivative_precision / 2) - f(x - derivative_precision / 2)) / derivative_precision
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {ExprParserAPI} */
|
||||
Modules.ExprParser = Modules.ExprParser || new ExprParserAPI()
|
||||
|
||||
export default Modules.ExprParser
|
||||
|
80
common/src/module/history.mjs
Normal file
80
common/src/module/history.mjs
Normal file
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Module } from "./common.mjs"
|
||||
import { HistoryInterface, NUMBER, STRING } from "./interface.mjs"
|
||||
|
||||
|
||||
class HistoryAPI extends Module {
|
||||
constructor() {
|
||||
super("History", {
|
||||
historyObj: HistoryInterface,
|
||||
themeTextColor: STRING,
|
||||
imageDepth: NUMBER,
|
||||
fontSize: NUMBER
|
||||
})
|
||||
// History QML object
|
||||
this.history = null
|
||||
this.themeTextColor = "#FF0000"
|
||||
this.imageDepth = 2
|
||||
this.fontSize = 28
|
||||
}
|
||||
|
||||
initialize({ historyObj, themeTextColor, imageDepth, fontSize }) {
|
||||
super.initialize({ historyObj, themeTextColor, imageDepth, fontSize })
|
||||
this.history = historyObj
|
||||
this.themeTextColor = themeTextColor
|
||||
this.imageDepth = imageDepth
|
||||
this.fontSize = fontSize
|
||||
}
|
||||
|
||||
undo() {
|
||||
if(!this.initialized) throw new Error("Attempting undo before initialize!")
|
||||
this.history.undo()
|
||||
}
|
||||
|
||||
redo() {
|
||||
if(!this.initialized) throw new Error("Attempting redo before initialize!")
|
||||
this.history.redo()
|
||||
}
|
||||
|
||||
clear() {
|
||||
if(!this.initialized) throw new Error("Attempting clear before initialize!")
|
||||
this.history.clear()
|
||||
}
|
||||
|
||||
addToHistory(action) {
|
||||
if(!this.initialized) throw new Error("Attempting addToHistory before initialize!")
|
||||
this.history.addToHistory(action)
|
||||
}
|
||||
|
||||
unserialize(...data) {
|
||||
if(!this.initialized) throw new Error("Attempting unserialize before initialize!")
|
||||
this.history.unserialize(...data)
|
||||
}
|
||||
|
||||
serialize() {
|
||||
if(!this.initialized) throw new Error("Attempting serialize before initialize!")
|
||||
return this.history.serialize()
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {HistoryAPI} */
|
||||
Modules.History = Modules.History || new HistoryAPI()
|
||||
|
||||
export default Modules.History
|
35
common/src/module/index.mjs
Normal file
35
common/src/module/index.mjs
Normal file
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Objects from "./objects.mjs"
|
||||
import ExprParser from "./expreval.mjs"
|
||||
import Latex from "./latex.mjs"
|
||||
import History from "./history.mjs"
|
||||
import Canvas from "./canvas.mjs"
|
||||
import IO from "./io.mjs"
|
||||
import Preferences from "./preferences.mjs"
|
||||
|
||||
export default {
|
||||
Objects,
|
||||
ExprParser,
|
||||
Latex,
|
||||
History,
|
||||
Canvas,
|
||||
IO,
|
||||
Preferences
|
||||
}
|
187
common/src/module/interface.mjs
Normal file
187
common/src/module/interface.mjs
Normal file
|
@ -0,0 +1,187 @@
|
|||
/*!
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
*
|
||||
* @author Ad5001 <mail@ad5001.eu>
|
||||
* @license GPL-3.0-or-later
|
||||
* @copyright (C) 2021-2024 Ad5001
|
||||
* @preserve
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export const NUMBER = 0
|
||||
export const STRING = "string"
|
||||
export const BOOLEAN = true
|
||||
export const OBJECT = {}
|
||||
export const FUNCTION = () => {
|
||||
throw new Error("Cannot call function of an interface.")
|
||||
}
|
||||
|
||||
|
||||
export class Interface {
|
||||
/**
|
||||
* Checks if the class to check implements the given interface.
|
||||
* Throws an error if the implementation does not conform to the interface.
|
||||
* @param {typeof Interface} interface_
|
||||
* @param {object} classToCheck
|
||||
* @return {boolean}
|
||||
*/
|
||||
static check_implementation(interface_, classToCheck) {
|
||||
const properties = new interface_()
|
||||
const interfaceName = interface_.name
|
||||
const toCheckName = classToCheck.constructor.name
|
||||
for(const [property, value] of Object.entries(properties))
|
||||
if(property !== "implement") {
|
||||
if(classToCheck[property] === undefined)
|
||||
// Check if the property exist
|
||||
throw new Error(`Property '${property}' (${typeof value}) is present in interface ${interfaceName}, but not in implementation ${toCheckName}.`)
|
||||
else if((typeof value) !== (typeof classToCheck[property]))
|
||||
// Compare the types
|
||||
throw new Error(`Property '${property}' of ${interfaceName} implementation ${toCheckName} is a '${typeof classToCheck[property]}' and not a '${typeof value}'.`)
|
||||
else if((typeof value) === "object")
|
||||
// Test type of object.
|
||||
if(value instanceof Interface)
|
||||
Interface.check_implementation(value, classToCheck[property])
|
||||
else if(value.prototype && !(classToCheck[property] instanceof value))
|
||||
throw new Error(`Property '${property}' of ${interfaceName} implementation ${toCheckName} is not '${value.constructor.name}'.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class SettingsInterface extends Interface {
|
||||
width = NUMBER
|
||||
height = NUMBER
|
||||
xmin = NUMBER
|
||||
ymax = NUMBER
|
||||
xzoom = NUMBER
|
||||
yzoom = NUMBER
|
||||
xaxisstep = STRING
|
||||
yaxisstep = STRING
|
||||
xlabel = STRING
|
||||
ylabel = STRING
|
||||
linewidth = NUMBER
|
||||
textsize = NUMBER
|
||||
logscalex = BOOLEAN
|
||||
showxgrad = BOOLEAN
|
||||
showygrad = BOOLEAN
|
||||
}
|
||||
|
||||
export class CanvasInterface extends SettingsInterface {
|
||||
imageLoaders = OBJECT
|
||||
/** @type {function(string): CanvasRenderingContext2D} */
|
||||
getContext = FUNCTION
|
||||
/** @type {function(rect)} */
|
||||
markDirty = FUNCTION
|
||||
/** @type {function(string)} */
|
||||
loadImage = FUNCTION
|
||||
/** @type {function(string)} */
|
||||
isImageLoading = FUNCTION
|
||||
/** @type {function(string)} */
|
||||
isImageLoaded = FUNCTION
|
||||
/** @type {function()} */
|
||||
requestPaint = FUNCTION
|
||||
}
|
||||
|
||||
export class RootInterface extends Interface {
|
||||
width = NUMBER
|
||||
height = NUMBER
|
||||
updateObjectsLists = FUNCTION
|
||||
}
|
||||
|
||||
export class DialogInterface extends Interface {
|
||||
show = FUNCTION
|
||||
}
|
||||
|
||||
export class HistoryInterface extends Interface {
|
||||
undo = FUNCTION
|
||||
redo = FUNCTION
|
||||
clear = FUNCTION
|
||||
addToHistory = FUNCTION
|
||||
unserialize = FUNCTION
|
||||
serialize = FUNCTION
|
||||
}
|
||||
|
||||
export class LatexInterface extends Interface {
|
||||
/**
|
||||
* @param {string} markup - LaTeX markup to render
|
||||
* @param {number} fontSize - Font size (in pt) to render
|
||||
* @param {string} color - Color of the text to render
|
||||
* @returns {string} - Comma separated data of the image (source, width, height)
|
||||
*/
|
||||
render = FUNCTION
|
||||
/**
|
||||
* @param {string} markup - LaTeX markup to render
|
||||
* @param {number} fontSize - Font size (in pt) to render
|
||||
* @param {string} color - Color of the text to render
|
||||
* @returns {string} - Comma separated data of the image (source, width, height)
|
||||
*/
|
||||
findPrerendered = FUNCTION
|
||||
/**
|
||||
* Checks if the Latex installation is valid
|
||||
* @returns {boolean}
|
||||
*/
|
||||
checkLatexInstallation = FUNCTION
|
||||
}
|
||||
|
||||
export class HelperInterface extends Interface {
|
||||
/**
|
||||
* Gets a setting from the config
|
||||
* @param {string} settingName - Setting (and its dot-separated namespace) to get (e.g. "default_graph.xmin")
|
||||
* @returns {boolean} Value of the setting
|
||||
*/
|
||||
getSettingBool = FUNCTION
|
||||
/**
|
||||
* Gets a setting from the config
|
||||
* @param {string} settingName - Setting (and its dot-separated namespace) to get (e.g. "default_graph.xmin")
|
||||
* @returns {number} Value of the setting
|
||||
*/
|
||||
getSettingInt = FUNCTION
|
||||
/**
|
||||
* Gets a setting from the config
|
||||
* @param {string} settingName - Setting (and its dot-separated namespace) to get (e.g. "default_graph.xmin")
|
||||
* @returns {string} Value of the setting
|
||||
*/
|
||||
getSetting = FUNCTION
|
||||
/**
|
||||
* Sets a setting in the config
|
||||
* @param {string} settingName - Setting (and its dot-separated namespace) to set (e.g. "default_graph.xmin")
|
||||
* @param {boolean} value
|
||||
*/
|
||||
setSettingBool = FUNCTION
|
||||
/**
|
||||
* Sets a setting in the config
|
||||
* @param {string} settingName - Setting (and its dot-separated namespace) to set (e.g. "default_graph.xmin")
|
||||
* @param {number} value
|
||||
*/
|
||||
setSettingInt = FUNCTION
|
||||
/**
|
||||
* Sets a setting in the config
|
||||
* @param {string} settingName - Setting (and its dot-separated namespace) to set (e.g. "default_graph.xmin")
|
||||
* @param {string} value
|
||||
*/
|
||||
setSetting = FUNCTION
|
||||
/**
|
||||
* Sends data to be written
|
||||
* @param {string} file
|
||||
* @param {string} dataToWrite - just JSON encoded, requires the "LPFv1" mime to be added before writing
|
||||
*/
|
||||
write = FUNCTION
|
||||
/**
|
||||
* Requests data to be read from a file
|
||||
* @param {string} file
|
||||
* @returns {string} the loaded data - just JSON encoded, requires the "LPFv1" mime to be stripped
|
||||
*/
|
||||
load = FUNCTION
|
||||
}
|
181
common/src/module/io.mjs
Normal file
181
common/src/module/io.mjs
Normal file
|
@ -0,0 +1,181 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Module } from "./common.mjs"
|
||||
import Objects from "./objects.mjs"
|
||||
import History from "./history.mjs"
|
||||
import Canvas from "./canvas.mjs"
|
||||
import { DialogInterface, RootInterface, SettingsInterface } from "./interface.mjs"
|
||||
|
||||
|
||||
class IOAPI extends Module {
|
||||
|
||||
constructor() {
|
||||
super("IO", {
|
||||
alert: DialogInterface,
|
||||
root: RootInterface,
|
||||
settings: SettingsInterface
|
||||
})
|
||||
/**
|
||||
* Path of the currently opened file. Empty if no file is opened.
|
||||
* @type {string}
|
||||
*/
|
||||
this.saveFileName = ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes module with QML elements.
|
||||
* @param {RootInterface} root
|
||||
* @param {SettingsInterface} settings
|
||||
* @param {{show: function(string)}} alert
|
||||
*/
|
||||
initialize({ root, settings, alert }) {
|
||||
super.initialize({ root, settings, alert })
|
||||
this.rootElement = root
|
||||
this.settings = settings
|
||||
this.alert = alert
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the diagram to a certain \c filename.
|
||||
* @param {string} filename
|
||||
*/
|
||||
saveDiagram(filename) {
|
||||
if(!this.initialized) throw new Error("Attempting saveDiagram before initialize!")
|
||||
// Add extension if necessary
|
||||
if(["lpf"].indexOf(filename.split(".")[filename.split(".").length - 1]) === -1)
|
||||
filename += ".lpf"
|
||||
this.saveFilename = filename
|
||||
let objs = {}
|
||||
for(let objType in Objects.currentObjects) {
|
||||
objs[objType] = []
|
||||
for(let obj of Objects.currentObjects[objType]) {
|
||||
objs[objType].push(obj.export())
|
||||
}
|
||||
}
|
||||
let settings = {
|
||||
"xzoom": this.settings.xzoom,
|
||||
"yzoom": this.settings.yzoom,
|
||||
"xmin": this.settings.xmin,
|
||||
"ymax": this.settings.ymax,
|
||||
"xaxisstep": this.settings.xaxisstep,
|
||||
"yaxisstep": this.settings.yaxisstep,
|
||||
"xaxislabel": this.settings.xlabel,
|
||||
"yaxislabel": this.settings.ylabel,
|
||||
"logscalex": this.settings.logscalex,
|
||||
"linewidth": this.settings.linewidth,
|
||||
"showxgrad": this.settings.showxgrad,
|
||||
"showygrad": this.settings.showygrad,
|
||||
"textsize": this.settings.textsize,
|
||||
"history": History.serialize(),
|
||||
"width": this.rootElement.width,
|
||||
"height": this.rootElement.height,
|
||||
"objects": objs,
|
||||
"type": "logplotv1"
|
||||
}
|
||||
Helper.write(filename, JSON.stringify(settings))
|
||||
this.alert.show(qsTranslate("io", "Saved plot to '%1'.").arg(filename.split("/").pop()))
|
||||
History.history.saved = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the diagram from a certain \c filename.
|
||||
* @param {string} filename
|
||||
*/
|
||||
async loadDiagram(filename) {
|
||||
if(!this.initialized) throw new Error("Attempting loadDiagram before initialize!")
|
||||
if(!History.initialized) throw new Error("Attempting loadDiagram before history is initialized!")
|
||||
let basename = filename.split("/").pop()
|
||||
this.alert.show(qsTranslate("io", "Loading file '%1'.").arg(basename))
|
||||
let data = JSON.parse(Helper.load(filename))
|
||||
let error = ""
|
||||
if(data.hasOwnProperty("type") && data["type"] === "logplotv1") {
|
||||
History.clear()
|
||||
// Importing settings
|
||||
this.settings.saveFilename = filename
|
||||
this.settings.xzoom = parseFloat(data["xzoom"]) || 100
|
||||
this.settings.yzoom = parseFloat(data["yzoom"]) || 10
|
||||
this.settings.xmin = parseFloat(data["xmin"]) || 5 / 10
|
||||
this.settings.ymax = parseFloat(data["ymax"]) || 24
|
||||
this.settings.xaxisstep = data["xaxisstep"] || "4"
|
||||
this.settings.yaxisstep = data["yaxisstep"] || "4"
|
||||
this.settings.xlabel = data["xaxislabel"] || ""
|
||||
this.settings.ylabel = data["yaxislabel"] || ""
|
||||
this.settings.logscalex = data["logscalex"] === true
|
||||
if("showxgrad" in data)
|
||||
this.settings.showxgrad = data["showxgrad"]
|
||||
if("showygrad" in data)
|
||||
this.settings.textsize = data["showygrad"]
|
||||
if("linewidth" in data)
|
||||
this.settings.linewidth = data["linewidth"]
|
||||
if("textsize" in data)
|
||||
this.settings.textsize = data["textsize"]
|
||||
this.rootElement.height = parseFloat(data["height"]) || 500
|
||||
this.rootElement.width = parseFloat(data["width"]) || 1000
|
||||
|
||||
// Importing objects
|
||||
Objects.currentObjects = {}
|
||||
for(let key of Object.keys(Objects.currentObjectsByName)) {
|
||||
delete Objects.currentObjectsByName[key]
|
||||
// Required to keep the same reference for the copy of the object used in expression variable detection.
|
||||
// Another way would be to change the reference as well, but I feel like the code would be less clean.
|
||||
}
|
||||
for(let objType in data["objects"]) {
|
||||
if(Object.keys(Objects.types).includes(objType)) {
|
||||
Objects.currentObjects[objType] = []
|
||||
for(let objData of data["objects"][objType]) {
|
||||
/** @type {DrawableObject} */
|
||||
let obj = Objects.types[objType].import(...objData)
|
||||
Objects.currentObjects[objType].push(obj)
|
||||
Objects.currentObjectsByName[obj.name] = obj
|
||||
}
|
||||
} else {
|
||||
error += qsTranslate("io", "Unknown object type: %1.").arg(objType) + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
// Updating object dependencies.
|
||||
for(let objName in Objects.currentObjectsByName)
|
||||
Objects.currentObjectsByName[objName].update()
|
||||
|
||||
// Importing history
|
||||
if("history" in data)
|
||||
History.unserialize(...data["history"])
|
||||
|
||||
// Refreshing sidebar
|
||||
this.rootElement.updateObjectsLists()
|
||||
} else {
|
||||
error = qsTranslate("io", "Invalid file provided.")
|
||||
}
|
||||
if(error !== "") {
|
||||
console.log(error)
|
||||
this.alert.show(qsTranslate("io", "Could not load file: ") + error)
|
||||
// TODO: Error handling
|
||||
return
|
||||
}
|
||||
Canvas.redraw()
|
||||
this.alert.show(qsTranslate("io", "Loaded file '%1'.").arg(basename))
|
||||
History.history.saved = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** @type {IOAPI} */
|
||||
Modules.IO = Modules.IO || new IOAPI()
|
||||
|
||||
export default Modules.IO
|
334
common/src/module/latex.mjs
Normal file
334
common/src/module/latex.mjs
Normal file
|
@ -0,0 +1,334 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Module } from "./common.mjs"
|
||||
import * as Instruction from "../lib/expr-eval/instruction.mjs"
|
||||
import { escapeValue } from "../lib/expr-eval/expression.mjs"
|
||||
import { HelperInterface, LatexInterface } from "./interface.mjs"
|
||||
|
||||
const unicodechars = [
|
||||
"α", "β", "γ", "δ", "ε", "ζ", "η",
|
||||
"π", "θ", "κ", "λ", "μ", "ξ", "ρ",
|
||||
"ς", "σ", "τ", "φ", "χ", "ψ", "ω",
|
||||
"Γ", "Δ", "Θ", "Λ", "Ξ", "Π", "Σ",
|
||||
"Φ", "Ψ", "Ω", "ₐ", "ₑ", "ₒ", "ₓ",
|
||||
"ₕ", "ₖ", "ₗ", "ₘ", "ₙ", "ₚ", "ₛ",
|
||||
"ₜ", "¹", "²", "³", "⁴", "⁵", "⁶",
|
||||
"⁷", "⁸", "⁹", "⁰", "₁", "₂", "₃",
|
||||
"₄", "₅", "₆", "₇", "₈", "₉", "₀",
|
||||
"pi", "∞"]
|
||||
const equivalchars = [
|
||||
"\\alpha", "\\beta", "\\gamma", "\\delta", "\\epsilon", "\\zeta", "\\eta",
|
||||
"\\pi", "\\theta", "\\kappa", "\\lambda", "\\mu", "\\xi", "\\rho",
|
||||
"\\sigma", "\\sigma", "\\tau", "\\phi", "\\chi", "\\psi", "\\omega",
|
||||
"\\Gamma", "\\Delta", "\\Theta", "\\Lambda", "\\Xi", "\\Pi", "\\Sigma",
|
||||
"\\Phy", "\\Psi", "\\Omega", "{}_{a}", "{}_{e}", "{}_{o}", "{}_{x}",
|
||||
"{}_{h}", "{}_{k}", "{}_{l}", "{}_{m}", "{}_{n}", "{}_{p}", "{}_{s}",
|
||||
"{}_{t}", "{}^{1}", "{}^{2}", "{}^{3}", "{}^{4}", "{}^{5}", "{}^{6}",
|
||||
"{}^{7}", "{}^{8}", "{}^{9}", "{}^{0}", "{}_{1}", "{}_{2}", "{}_{3}",
|
||||
"{}_{4}", "{}_{5}", "{}_{6}", "{}_{7}", "{}_{8}", "{}_{9}", "{}_{0}",
|
||||
"\\pi", "\\infty"]
|
||||
|
||||
/**
|
||||
* Class containing the result of a LaTeX render.
|
||||
*
|
||||
* @property {string} source - Exported PNG file
|
||||
* @property {number} width
|
||||
* @property {number} height
|
||||
*/
|
||||
class LatexRenderResult {
|
||||
constructor(source, width, height) {
|
||||
this.source = source
|
||||
this.width = parseFloat(width)
|
||||
this.height = parseFloat(height)
|
||||
}
|
||||
}
|
||||
|
||||
class LatexAPI extends Module {
|
||||
constructor() {
|
||||
super("Latex", {
|
||||
latex: LatexInterface,
|
||||
helper: HelperInterface
|
||||
})
|
||||
/**
|
||||
* true if latex has been enabled by the user, false otherwise.
|
||||
*/
|
||||
this.enabled = false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {LatexInterface} latex
|
||||
* @param {HelperInterface} helper
|
||||
*/
|
||||
initialize({ latex, helper }) {
|
||||
super.initialize({ latex, helper })
|
||||
this.latex = latex
|
||||
this.helper = helper
|
||||
this.enabled = helper.getSettingBool("enable_latex")
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given markup (with given font size and color) has already been
|
||||
* rendered, and if so, returns its data. Otherwise, returns null.
|
||||
*
|
||||
* @param {string} markup - LaTeX markup to render.
|
||||
* @param {number} fontSize - Font size (in pt) to render.
|
||||
* @param {string} color - Color of the text to render.
|
||||
* @returns {LatexRenderResult|null}
|
||||
*/
|
||||
findPrerendered(markup, fontSize, color) {
|
||||
if(!this.initialized) throw new Error("Attempting findPrerendered before initialize!")
|
||||
const data = this.latex.findPrerendered(markup, fontSize, color)
|
||||
let ret = null
|
||||
if(data !== "")
|
||||
ret = new LatexRenderResult(...data.split(","))
|
||||
return ret
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares and renders a latex string into a png file asynchronously.
|
||||
*
|
||||
* @param {string} markup - LaTeX markup to render.
|
||||
* @param {number} fontSize - Font size (in pt) to render.
|
||||
* @param {string} color - Color of the text to render.
|
||||
* @returns {Promise<LatexRenderResult>}
|
||||
*/
|
||||
async requestAsyncRender(markup, fontSize, color) {
|
||||
if(!this.initialized) throw new Error("Attempting requestAsyncRender before initialize!")
|
||||
let args = this.latex.render(markup, fontSize, color).split(",")
|
||||
return new LatexRenderResult(...args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts element within parenthesis.
|
||||
*
|
||||
* @param {string|number} elem - element to put within parenthesis.
|
||||
* @returns {string}
|
||||
*/
|
||||
par(elem) {
|
||||
return `(${elem})`
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the element contains at least one of the elements of
|
||||
* the string array contents, but not at the first position of the string,
|
||||
* and returns the parenthesis version if so.
|
||||
*
|
||||
* @param {string|number} elem - element to put within parenthesis.
|
||||
* @param {Array} contents - Array of elements to put within parenthesis.
|
||||
* @returns {string}
|
||||
*/
|
||||
parif(elem, contents) {
|
||||
elem = elem.toString()
|
||||
if(elem[0] !== "(" && elem[elem.length - 1] !== ")" && contents.some(x => elem.indexOf(x) > 0))
|
||||
return this.par(elem)
|
||||
if(elem[0] === "(" && elem[elem.length - 1] === ")")
|
||||
return elem.removeEnclosure()
|
||||
return elem
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a latex expression for a function.
|
||||
*
|
||||
* @param {string} f - Function to convert
|
||||
* @param {(number,string)[]} args - Arguments of the function
|
||||
* @returns {string}
|
||||
*/
|
||||
functionToLatex(f, args) {
|
||||
switch(f) {
|
||||
case "derivative":
|
||||
if(args.length === 3)
|
||||
return `\\frac{d${args[0].removeEnclosure().replaceAll(args[1].removeEnclosure(), "x")}}{dx}`
|
||||
else
|
||||
return `\\frac{d${args[0]}}{dx}(x)`
|
||||
case "integral":
|
||||
if(args.length === 4)
|
||||
return `\\int\\limits_{${args[0]}}^{${args[1]}}${args[2].removeEnclosure()} d${args[3].removeEnclosure()}`
|
||||
else
|
||||
return `\\int\\limits_{${args[0]}}^{${args[1]}}${args[2]}(t) dt`
|
||||
case "sqrt":
|
||||
return `\\sqrt\\left(${args.join(", ")}\\right)`
|
||||
case "abs":
|
||||
return `\\left|${args.join(", ")}\\right|`
|
||||
case "floor":
|
||||
return `\\left\\lfloor${args.join(", ")}\\right\\rfloor`
|
||||
case "ceil":
|
||||
return `\\left\\lceil${args.join(", ")}\\right\\rceil`
|
||||
default:
|
||||
return `\\mathrm{${f}}\\left(${args.join(", ")}\\right)`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a latex variable from a variable.
|
||||
*
|
||||
* @param {string} vari - variable text to convert
|
||||
* @param {boolean} wrapIn$ - checks whether the escaped chars should be escaped
|
||||
* @returns {string}
|
||||
*/
|
||||
variable(vari, wrapIn$ = false) {
|
||||
if(wrapIn$)
|
||||
for(let i = 0; i < unicodechars.length; i++) {
|
||||
if(vari.includes(unicodechars[i]))
|
||||
vari = vari.replaceAll(unicodechars[i], "$" + equivalchars[i] + "$")
|
||||
}
|
||||
else
|
||||
for(let i = 0; i < unicodechars.length; i++) {
|
||||
if(vari.includes(unicodechars[i]))
|
||||
vari = vari.replaceAll(unicodechars[i], equivalchars[i])
|
||||
}
|
||||
return vari
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts expr-eval instructions to a latex string.
|
||||
*
|
||||
* @param {Instruction[]} instructions - expr-eval tokens list
|
||||
* @returns {string}
|
||||
*/
|
||||
expression(instructions) {
|
||||
let nstack = []
|
||||
let n1, n2, n3
|
||||
let f, args, argCount
|
||||
for(let item of instructions) {
|
||||
let type = item.type
|
||||
|
||||
switch(type) {
|
||||
case Instruction.INUMBER:
|
||||
if(item.value === Infinity) {
|
||||
nstack.push("\\infty")
|
||||
} else if(typeof item.value === "number" && item.value < 0) {
|
||||
nstack.push(this.par(item.value))
|
||||
} else if(Array.isArray(item.value)) {
|
||||
nstack.push("[" + item.value.map(escapeValue).join(", ") + "]")
|
||||
} else {
|
||||
nstack.push(escapeValue(item.value))
|
||||
}
|
||||
break
|
||||
case Instruction.IOP2:
|
||||
n2 = nstack.pop()
|
||||
n1 = nstack.pop()
|
||||
f = item.value
|
||||
switch(f) {
|
||||
case "-":
|
||||
case "+":
|
||||
nstack.push(n1 + f + n2)
|
||||
break
|
||||
case "||":
|
||||
case "or":
|
||||
case "&&":
|
||||
case "and":
|
||||
case "==":
|
||||
case "!=":
|
||||
nstack.push(this.par(n1) + f + this.par(n2))
|
||||
break
|
||||
case "*":
|
||||
if(n2 === "\\pi" || n2 === "e" || n2 === "x" || n2 === "n")
|
||||
nstack.push(this.parif(n1, ["+", "-"]) + n2)
|
||||
else
|
||||
nstack.push(this.parif(n1, ["+", "-"]) + " \\times " + this.parif(n2, ["+", "-"]))
|
||||
break
|
||||
case "/":
|
||||
nstack.push("\\frac{" + n1 + "}{" + n2 + "}")
|
||||
break
|
||||
case "^":
|
||||
nstack.push(this.parif(n1, ["+", "-", "*", "/", "!"]) + "^{" + n2 + "}")
|
||||
break
|
||||
case "%":
|
||||
nstack.push(this.parif(n1, ["+", "-", "*", "/", "!", "^"]) + " \\mathrm{mod} " + this.parif(n2, ["+", "-", "*", "/", "!", "^"]))
|
||||
break
|
||||
case "[":
|
||||
nstack.push(n1 + "[" + n2 + "]")
|
||||
break
|
||||
default:
|
||||
throw new EvalError("Unknown operator " + item.value + ".")
|
||||
}
|
||||
break
|
||||
case Instruction.IOP3: // Thirdiary operator
|
||||
n3 = nstack.pop()
|
||||
n2 = nstack.pop()
|
||||
n1 = nstack.pop()
|
||||
f = item.value
|
||||
if(f === "?") {
|
||||
nstack.push("(" + n1 + " ? " + n2 + " : " + n3 + ")")
|
||||
} else {
|
||||
throw new EvalError("Unknown operator " + item.value + ".")
|
||||
}
|
||||
break
|
||||
case Instruction.IVAR:
|
||||
case Instruction.IVARNAME:
|
||||
nstack.push(this.variable(item.value.toString()))
|
||||
break
|
||||
case Instruction.IOP1: // Unary operator
|
||||
n1 = nstack.pop()
|
||||
f = item.value
|
||||
switch(f) {
|
||||
case "-":
|
||||
case "+":
|
||||
nstack.push(this.par(f + n1))
|
||||
break
|
||||
case "!":
|
||||
nstack.push(this.parif(n1, ["+", "-", "*", "/", "^"]) + "!")
|
||||
break
|
||||
default:
|
||||
nstack.push(f + this.parif(n1, ["+", "-", "*", "/", "^"]))
|
||||
break
|
||||
}
|
||||
break
|
||||
case Instruction.IFUNCALL:
|
||||
argCount = item.value
|
||||
args = []
|
||||
while(argCount-- > 0) {
|
||||
args.unshift(nstack.pop())
|
||||
}
|
||||
f = nstack.pop()
|
||||
// Handling various functions
|
||||
nstack.push(this.functionToLatex(f, args))
|
||||
break
|
||||
case Instruction.IMEMBER:
|
||||
n1 = nstack.pop()
|
||||
nstack.push(n1 + "." + item.value)
|
||||
break
|
||||
case Instruction.IARRAY:
|
||||
argCount = item.value
|
||||
args = []
|
||||
while(argCount-- > 0) {
|
||||
args.unshift(nstack.pop())
|
||||
}
|
||||
nstack.push("[" + args.join(", ") + "]")
|
||||
break
|
||||
case Instruction.IEXPR:
|
||||
nstack.push("(" + this.expression(item.value) + ")")
|
||||
break
|
||||
case Instruction.IENDSTATEMENT:
|
||||
break
|
||||
default:
|
||||
throw new EvalError("invalid Expression")
|
||||
}
|
||||
}
|
||||
if(nstack.length > 1) {
|
||||
nstack = [nstack.join(";")]
|
||||
}
|
||||
return String(nstack[0])
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {LatexAPI} */
|
||||
Modules.Latex = Modules.Latex || new LatexAPI()
|
||||
/** @type {LatexAPI} */
|
||||
export default Modules.Latex
|
130
common/src/module/objects.mjs
Normal file
130
common/src/module/objects.mjs
Normal file
|
@ -0,0 +1,130 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Module } from "./common.mjs"
|
||||
import { textsub } from "../utils.mjs"
|
||||
|
||||
class ObjectsAPI extends Module {
|
||||
|
||||
constructor() {
|
||||
super("Objects")
|
||||
|
||||
this.types = {}
|
||||
/**
|
||||
* List of objects for each type of object.
|
||||
* @type {Object.<string,DrawableObject[]>}
|
||||
*/
|
||||
this.currentObjects = {}
|
||||
/**
|
||||
* List of objects matched by their name.
|
||||
* @type {Object.<string,DrawableObject>}
|
||||
*/
|
||||
this.currentObjectsByName = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new name for an object, based on the allowedLetters.
|
||||
* If variables with each of the allowedLetters is created, a subscript
|
||||
* number is added to the name.
|
||||
* @param {string} allowedLetters
|
||||
* @param {string} prefix - Prefix to the name.
|
||||
* @return {string} New unused name for a new object.
|
||||
*/
|
||||
getNewName(allowedLetters, prefix = "") {
|
||||
// Allows to get a new name, based on the allowed letters,
|
||||
// as well as adding a sub number when needs be.
|
||||
let newid = 0
|
||||
let ret
|
||||
do {
|
||||
let letter = allowedLetters[newid % allowedLetters.length]
|
||||
let num = Math.floor((newid - (newid % allowedLetters.length)) / allowedLetters.length)
|
||||
ret = prefix + letter + (num > 0 ? textsub(num - 1) : "")
|
||||
newid += 1
|
||||
} while(ret in this.currentObjectsByName)
|
||||
return ret
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames an object from its old name to the new one.
|
||||
* @param {string} oldName - Current name of the object.
|
||||
* @param {string} newName - Name to rename the object to.
|
||||
*/
|
||||
renameObject(oldName, newName) {
|
||||
let obj = this.currentObjectsByName[oldName]
|
||||
delete this.currentObjectsByName[oldName]
|
||||
this.currentObjectsByName[newName] = obj
|
||||
obj.name = newName
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an object by its given name.
|
||||
* @param {string} objName - Current name of the object.
|
||||
*/
|
||||
deleteObject(objName) {
|
||||
let obj = this.currentObjectsByName[objName]
|
||||
if(obj !== undefined) {
|
||||
this.currentObjects[obj.type].splice(this.currentObjects[obj.type].indexOf(obj), 1)
|
||||
obj.delete()
|
||||
delete this.currentObjectsByName[objName]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of all names of a certain object type.
|
||||
* @param {string} objType - Type of the object to query. Can be ExecutableObject for all ExecutableObjects.
|
||||
* @returns {string[]} List of names of the objects.
|
||||
*/
|
||||
getObjectsName(objType) {
|
||||
if(objType === "ExecutableObject")
|
||||
return this.getExecutableTypes().flatMap(
|
||||
elemType => this.currentObjects[elemType].map(obj => obj.name)
|
||||
)
|
||||
if(this.currentObjects[objType] === undefined) return []
|
||||
return this.currentObjects[objType].map(obj => obj.name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all object types which are executable objects.
|
||||
* @return {string[]} List of all object types which are executable objects.
|
||||
*/
|
||||
getExecutableTypes() {
|
||||
return Object.keys(this.currentObjects).filter(objType => this.types[objType].executable())
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and register an object in the database.
|
||||
* @param {string} objType - Type of the object to create.
|
||||
* @param {string[]} args - List of arguments for the objects (can be empty).
|
||||
* @return {DrawableObject<objType>} Newly created object.
|
||||
*/
|
||||
createNewRegisteredObject(objType, args = []) {
|
||||
if(Object.keys(this.types).indexOf(objType) === -1) return null // Object type does not exist.
|
||||
const newobj = new this.types[objType](...args)
|
||||
if(Object.keys(this.currentObjects).indexOf(objType) === -1) {
|
||||
this.currentObjects[objType] = []
|
||||
}
|
||||
this.currentObjects[objType].push(newobj)
|
||||
this.currentObjectsByName[newobj.name] = newobj
|
||||
return newobj
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {ObjectsAPI} */
|
||||
Modules.Objects = Modules.Objects || new ObjectsAPI()
|
||||
|
||||
export default Modules.Objects
|
37
common/src/module/preferences.mjs
Normal file
37
common/src/module/preferences.mjs
Normal file
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { Module } from "./common.mjs"
|
||||
import General from "../preferences/general.mjs"
|
||||
import Editor from "../preferences/expression.mjs"
|
||||
import DefaultGraph from "../preferences/default.mjs"
|
||||
|
||||
class PreferencesAPI extends Module {
|
||||
constructor() {
|
||||
super("Preferences")
|
||||
|
||||
this.categories = {
|
||||
[QT_TRANSLATE_NOOP("settingCategory", "general")]: General,
|
||||
[QT_TRANSLATE_NOOP("settingCategory", "editor")]: Editor,
|
||||
[QT_TRANSLATE_NOOP("settingCategory", "default")]: DefaultGraph
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {CanvasAPI} */
|
||||
Modules.Preferences = Modules.Preferences || new PreferencesAPI()
|
||||
export default Modules.Preferences
|
57
common/src/objs/autoload.mjs
Normal file
57
common/src/objs/autoload.mjs
Normal file
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Objects from "../module/objects.mjs"
|
||||
import { DrawableObject } from "./common.mjs"
|
||||
import Point from "./point.mjs"
|
||||
import Text from "./text.mjs"
|
||||
import Function from "./function.mjs"
|
||||
import BodeMagnitude from "./bodemagnitude.mjs"
|
||||
import BodePhase from "./bodephase.mjs"
|
||||
import BodeMagnitudeSum from "./bodemagnitudesum.mjs"
|
||||
import BodePhaseSum from "./bodephasesum.mjs"
|
||||
import XCursor from "./xcursor.mjs"
|
||||
import Sequence from "./sequence.mjs"
|
||||
import DistributionFunction from "./distribution.mjs"
|
||||
|
||||
/**
|
||||
* Registers the object obj in the object list.
|
||||
* @param {DrawableObject} obj - Object to be registered.
|
||||
*/
|
||||
function registerObject(obj) {
|
||||
// Registers an object to be used in LogarithmPlotter.
|
||||
if(obj.prototype instanceof DrawableObject) {
|
||||
if(!Objects.types[obj.type()])
|
||||
Objects.types[obj.type()] = obj
|
||||
} else {
|
||||
console.error("Could not register object " + (obj?.type() ?? obj.constructor.name) + ", as it isn't a DrawableObject.")
|
||||
}
|
||||
}
|
||||
|
||||
if(Object.keys(Objects.types).length === 0) {
|
||||
registerObject(Point)
|
||||
registerObject(Text)
|
||||
registerObject(Function)
|
||||
registerObject(BodeMagnitude)
|
||||
registerObject(BodePhase)
|
||||
registerObject(BodeMagnitudeSum)
|
||||
registerObject(BodePhaseSum)
|
||||
registerObject(XCursor)
|
||||
registerObject(Sequence)
|
||||
registerObject(DistributionFunction)
|
||||
}
|
162
common/src/objs/bodemagnitude.mjs
Normal file
162
common/src/objs/bodemagnitude.mjs
Normal file
|
@ -0,0 +1,162 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { parseDomain, executeExpression, Expression, EmptySet, Domain } from "../math/index.mjs"
|
||||
import { CreateNewObject } from "../history/index.mjs"
|
||||
import * as P from "../parameters.mjs"
|
||||
import Objects from "../module/objects.mjs"
|
||||
import Latex from "../module/latex.mjs"
|
||||
import History from "../module/history.mjs"
|
||||
|
||||
import { ExecutableObject } from "./common.mjs"
|
||||
import Function from "./function.mjs"
|
||||
|
||||
export default class BodeMagnitude extends ExecutableObject {
|
||||
static type() {
|
||||
return "Gain Bode"
|
||||
}
|
||||
|
||||
static displayType() {
|
||||
return qsTranslate("bodemagnitude", "Bode Magnitude")
|
||||
}
|
||||
|
||||
static displayTypeMultiple() {
|
||||
return qsTranslate("bodemagnitude", "Bode Magnitudes")
|
||||
}
|
||||
|
||||
static properties() {
|
||||
return {
|
||||
[QT_TRANSLATE_NOOP("prop", "om_0")]: new P.ObjectType("Point"),
|
||||
[QT_TRANSLATE_NOOP("prop", "pass")]: P.Enum.BodePass,
|
||||
[QT_TRANSLATE_NOOP("prop", "gain")]: new P.Expression(),
|
||||
[QT_TRANSLATE_NOOP("prop", "labelPosition")]: P.Enum.Position,
|
||||
[QT_TRANSLATE_NOOP("prop", "labelX")]: "number",
|
||||
[QT_TRANSLATE_NOOP("prop", "omGraduation")]: "boolean"
|
||||
}
|
||||
}
|
||||
|
||||
constructor(name = null, visible = true, color = null, labelContent = "name + value",
|
||||
om_0 = "", pass = "high", gain = "20", labelPosition = "above", labelX = 1, omGraduation = false) {
|
||||
if(name == null) name = Objects.getNewName("G")
|
||||
if(name === "G") name = "G₀" // G is reserved for sum of BODE magnitudes (Somme gains Bode).
|
||||
super(name, visible, color, labelContent)
|
||||
if(typeof om_0 == "string") {
|
||||
// Point name or create one
|
||||
om_0 = Objects.currentObjectsByName[om_0]
|
||||
if(om_0 == null) {
|
||||
// Create new point
|
||||
om_0 = Objects.createNewRegisteredObject("Point", [Objects.getNewName("ω"), true, this.color, "name"])
|
||||
History.addToHistory(new CreateNewObject(om_0.name, "Point", om_0.export()))
|
||||
om_0.update()
|
||||
labelPosition = "below"
|
||||
}
|
||||
om_0.requiredBy.push(this)
|
||||
}
|
||||
/** @type {Point} */
|
||||
this.om_0 = om_0
|
||||
this.pass = pass
|
||||
if(typeof gain == "number" || typeof gain == "string") gain = new Expression(gain.toString())
|
||||
this.gain = gain
|
||||
this.labelPosition = labelPosition
|
||||
this.labelX = labelX
|
||||
this.omGraduation = omGraduation
|
||||
}
|
||||
|
||||
getReadableString() {
|
||||
let pass = this.pass === "low" ? qsTranslate("bodemagnitude", "low-pass") : qsTranslate("bodemagnitude", "high-pass")
|
||||
return `${this.name}: ${pass}; ${this.om_0.name} = ${this.om_0.x}\n ${" ".repeat(this.name.length)}${this.gain.toString(true)} dB/dec`
|
||||
}
|
||||
|
||||
getLatexString() {
|
||||
let pass = this.pass === "low" ? qsTranslate("bodemagnitude", "low-pass") : qsTranslate("bodemagnitude", "high-pass")
|
||||
return `\\mathrm{${Latex.variable(this.name)}:}\\begin{array}{l}
|
||||
\\textsf{${pass}};${Latex.variable(this.om_0.name)} = ${this.om_0.x.latexMarkup} \\\\
|
||||
${this.gain.latexMarkup}\\textsf{ dB/dec}
|
||||
\\end{array}`
|
||||
}
|
||||
|
||||
execute(x = 1) {
|
||||
if(typeof x == "string") x = executeExpression(x)
|
||||
if((this.pass === "high" && x < this.om_0.x) || (this.pass === "low" && x > this.om_0.x)) {
|
||||
let dbfn = new Expression(`${this.gain.execute()}*(ln(x)-ln(${this.om_0.x}))/ln(10)+${this.om_0.y}`)
|
||||
return dbfn.execute(x)
|
||||
} else {
|
||||
return this.om_0.y.execute()
|
||||
}
|
||||
}
|
||||
|
||||
simplify(x = 1) {
|
||||
let xval = x
|
||||
if(typeof x == "string") xval = executeExpression(x)
|
||||
if((this.pass === "high" && xval < this.om_0.x.execute()) || (this.pass === "low" && xval > this.om_0.x.execute())) {
|
||||
let dbfn = new Expression(`${this.gain.execute()}*(ln(x)-ln(${this.om_0.x}))/ln(10)+${this.om_0.y}`)
|
||||
return dbfn.simplify(x)
|
||||
} else {
|
||||
return this.om_0.y.toString()
|
||||
}
|
||||
}
|
||||
|
||||
canExecute(x = 1) {
|
||||
return true
|
||||
}
|
||||
|
||||
draw(canvas) {
|
||||
let base = [canvas.x2px(this.om_0.x.execute()), canvas.y2px(this.om_0.y.execute())]
|
||||
let dbfn = new Expression(`${this.gain.execute()}*(log10(x)-log10(${this.om_0.x}))+${this.om_0.y}`)
|
||||
let inDrawDom
|
||||
|
||||
if(this.pass === "high") {
|
||||
// High pass, linear line from beginning, then constant to the end.
|
||||
canvas.drawLine(base[0], base[1], canvas.width, base[1])
|
||||
inDrawDom = parseDomain(`]-inf;${this.om_0.x}[`)
|
||||
} else {
|
||||
// Low pass, constant from the beginning, linear line to the end.
|
||||
canvas.drawLine(base[0], base[1], 0, base[1])
|
||||
inDrawDom = parseDomain(`]${this.om_0.x};+inf[`)
|
||||
}
|
||||
Function.drawFunction(canvas, dbfn, inDrawDom, Domain.R)
|
||||
// Dashed line representing break in function
|
||||
let xpos = canvas.x2px(this.om_0.x.execute())
|
||||
let dashPxSize = 10
|
||||
for(let i = 0; i < canvas.height && this.omGraduation; i += dashPxSize * 2)
|
||||
canvas.drawLine(xpos, i, xpos, i + dashPxSize)
|
||||
|
||||
// Label
|
||||
this.drawLabel(canvas, this.labelPosition, canvas.x2px(this.labelX), canvas.y2px(this.execute(this.labelX)))
|
||||
}
|
||||
|
||||
update() {
|
||||
super.update()
|
||||
/** @type {BodeMagnitudeSum[]} */
|
||||
let sumObjs = Objects.currentObjects["Somme gains Bode"]
|
||||
if(sumObjs !== undefined && sumObjs.length > 0) {
|
||||
sumObjs[0].recalculateCache()
|
||||
} else {
|
||||
Objects.createNewRegisteredObject("Somme gains Bode")
|
||||
}
|
||||
}
|
||||
|
||||
delete() {
|
||||
super.delete()
|
||||
/** @type {BodeMagnitudeSum[]} */
|
||||
let sumObjs = Objects.currentObjects["Somme gains Bode"]
|
||||
if(sumObjs !== undefined && sumObjs.length > 0) {
|
||||
sumObjs[0].recalculateCache()
|
||||
}
|
||||
}
|
||||
}
|
158
common/src/objs/bodemagnitudesum.mjs
Normal file
158
common/src/objs/bodemagnitudesum.mjs
Normal file
|
@ -0,0 +1,158 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Range, Expression, Domain } from "../math/index.mjs"
|
||||
import * as P from "../parameters.mjs"
|
||||
import Objects from "../module/objects.mjs"
|
||||
import Latex from "../module/latex.mjs"
|
||||
|
||||
import { ExecutableObject } from "./common.mjs"
|
||||
import Function from "./function.mjs"
|
||||
|
||||
|
||||
export default class BodeMagnitudeSum extends ExecutableObject {
|
||||
static type() {
|
||||
return "Somme gains Bode"
|
||||
}
|
||||
|
||||
static displayType() {
|
||||
return qsTranslate("bodemagnitudesum", "Bode Magnitudes Sum")
|
||||
}
|
||||
|
||||
static displayTypeMultiple() {
|
||||
return qsTranslate("bodemagnitudesum", "Bode Magnitudes Sum")
|
||||
}
|
||||
|
||||
static createable() {
|
||||
return false
|
||||
}
|
||||
|
||||
static properties() {
|
||||
return {
|
||||
[QT_TRANSLATE_NOOP("prop", "labelPosition")]: P.Enum.Position,
|
||||
[QT_TRANSLATE_NOOP("prop", "labelX")]: "number"
|
||||
}
|
||||
}
|
||||
|
||||
constructor(name = null, visible = true, color = null, labelContent = "name + value",
|
||||
labelPosition = "above", labelX = 1) {
|
||||
if(name == null) name = "G"
|
||||
super(name, visible, color, labelContent)
|
||||
this.labelPosition = labelPosition
|
||||
this.labelX = labelX
|
||||
this.recalculateCache()
|
||||
}
|
||||
|
||||
getReadableString() {
|
||||
return `${this.name} = ${Objects.getObjectsName("Gain Bode").join(" + ")}`
|
||||
}
|
||||
|
||||
getLatexString() {
|
||||
return `${Latex.variable(this.name)} = ${Objects.getObjectsName("Gain Bode").map(name => Latex.variable(name)).join(" + ")}`
|
||||
}
|
||||
|
||||
execute(x = 0) {
|
||||
for(let [limitedDrawFunction, inDrawDom] of this.cachedParts) {
|
||||
if(inDrawDom.includes(x)) {
|
||||
return limitedDrawFunction.execute(x)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
canExecute(x = 1) {
|
||||
return true // Should always be true
|
||||
}
|
||||
|
||||
simplify(x = 1) {
|
||||
for(let [limitedDrawFunction, inDrawDom] of this.cachedParts) {
|
||||
if(inDrawDom.includes(x)) {
|
||||
return limitedDrawFunction.simplify(x)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
recalculateCache() {
|
||||
this.cachedParts = []
|
||||
// Calculating this is fairly resource expansive so it's cached.
|
||||
let magnitudeObjects = Objects.currentObjects["Gain Bode"]
|
||||
if(magnitudeObjects === undefined || magnitudeObjects.length < 1) {
|
||||
Objects.deleteObject(this.name)
|
||||
} else {
|
||||
console.log("Recalculating cache gain")
|
||||
// Minimum to draw (can be expended if needed, just not infinite or it'll cause issues.
|
||||
const MIN_DRAW = 1e-20
|
||||
// Format: [[x value of where the filter transitions, magnitude, high-pass (bool)]]
|
||||
const magnitudes = []
|
||||
const XVALUE = 0
|
||||
const MAGNITUDE = 1
|
||||
const PASS = 2
|
||||
magnitudes.push([Number.MAX_VALUE, 0, true]) // Draw the ending section
|
||||
// Collect data from current magnitude (or gain in French) objects.
|
||||
let baseY = 0
|
||||
for(/** @type {BodeMagnitude} */ let magnitudeObj of magnitudeObjects) { // Sorting by their om_0 position.
|
||||
const om0x = magnitudeObj.om_0.x.execute()
|
||||
magnitudes.push([om0x, magnitudeObj.gain.execute(), magnitudeObj.pass === "high"])
|
||||
baseY += magnitudeObj.execute(MIN_DRAW)
|
||||
}
|
||||
// Sorting the data by their x transitions value
|
||||
magnitudes.sort((a, b) => a[XVALUE] - b[XVALUE])
|
||||
// Adding the total gains.
|
||||
let magnitudesBeforeTransition = []
|
||||
let magnitudesAfterTransition = []
|
||||
let totalMagnitudeAtStart = 0 // Magnitude at the lowest x value (sum of all high-pass magnitudes)
|
||||
for(let [om0x, magnitude, highpass] of magnitudes) {
|
||||
if(highpass) {
|
||||
magnitudesBeforeTransition.push(magnitude)
|
||||
magnitudesAfterTransition.push(0)
|
||||
totalMagnitudeAtStart += magnitude
|
||||
} else {
|
||||
magnitudesBeforeTransition.push(0)
|
||||
magnitudesAfterTransition.push(magnitude)
|
||||
}
|
||||
}
|
||||
// Calculating parts
|
||||
let previousTransitionX = MIN_DRAW
|
||||
let currentMagnitude = totalMagnitudeAtStart
|
||||
for(let transitionID = 0; transitionID < magnitudes.length; transitionID++) {
|
||||
const transitionX = magnitudes[transitionID][XVALUE]
|
||||
// Create draw function that will be used during drawing.
|
||||
const limitedDrawFunction = new Expression(`${currentMagnitude}*(ln(x)-ln(${previousTransitionX}))/ln(10)+${baseY}`)
|
||||
const drawDomain = new Range(previousTransitionX, transitionX, true, false)
|
||||
this.cachedParts.push([limitedDrawFunction, drawDomain])
|
||||
// Prepare default values for next function.
|
||||
previousTransitionX = transitionX
|
||||
baseY = limitedDrawFunction.execute(transitionX)
|
||||
currentMagnitude += magnitudesAfterTransition[transitionID] - magnitudesBeforeTransition[transitionID]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draw(canvas) {
|
||||
if(this.cachedParts.length > 0) {
|
||||
for(let [limitedDrawFunction, drawDomain] of this.cachedParts) {
|
||||
Function.drawFunction(canvas, limitedDrawFunction, drawDomain, Domain.R)
|
||||
// Check if necessary to draw label
|
||||
if(drawDomain.includes(this.labelX)) {
|
||||
this.drawLabel(canvas, this.labelPosition, canvas.x2px(this.labelX), canvas.y2px(this.execute(this.labelX)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
147
common/src/objs/bodephase.mjs
Normal file
147
common/src/objs/bodephase.mjs
Normal file
|
@ -0,0 +1,147 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { executeExpression, Expression } from "../math/index.mjs"
|
||||
import { CreateNewObject } from "../history/index.mjs"
|
||||
import * as P from "../parameters.mjs"
|
||||
import Objects from "../module/objects.mjs"
|
||||
import History from "../module/history.mjs"
|
||||
import Latex from "../module/latex.mjs"
|
||||
|
||||
import { ExecutableObject } from "./common.mjs"
|
||||
|
||||
|
||||
export default class BodePhase extends ExecutableObject {
|
||||
static type() {
|
||||
return "Phase Bode"
|
||||
}
|
||||
|
||||
static displayType() {
|
||||
return qsTranslate("bodephase", "Bode Phase")
|
||||
}
|
||||
|
||||
static displayTypeMultiple() {
|
||||
return qsTranslate("bodephase", "Bode Phases")
|
||||
}
|
||||
|
||||
static properties() {
|
||||
return {
|
||||
[QT_TRANSLATE_NOOP("prop", "om_0")]: new P.ObjectType("Point"),
|
||||
[QT_TRANSLATE_NOOP("prop", "phase")]: new P.Expression(),
|
||||
[QT_TRANSLATE_NOOP("prop", "unit")]: new P.Enum("°", "deg", "rad"),
|
||||
[QT_TRANSLATE_NOOP("prop", "labelPosition")]: P.Enum.Position,
|
||||
[QT_TRANSLATE_NOOP("prop", "labelX")]: "number"
|
||||
}
|
||||
}
|
||||
|
||||
constructor(name = null, visible = true, color = null, labelContent = "name + value",
|
||||
om_0 = "", phase = 90, unit = "°", labelPosition = "above", labelX = 1) {
|
||||
if(name == null) name = Objects.getNewName("φ")
|
||||
if(name === "φ") name = "φ₀" // φ is reserved for sum of Bode phases.
|
||||
super(name, visible, color, labelContent)
|
||||
if(typeof phase == "number" || typeof phase == "string") phase = new Expression(phase.toString())
|
||||
this.phase = phase
|
||||
if(typeof om_0 == "string") {
|
||||
// Point name or create one
|
||||
om_0 = Objects.currentObjectsByName[om_0]
|
||||
if(om_0 == null) {
|
||||
// Create new point
|
||||
om_0 = Objects.createNewRegisteredObject("Point", [Objects.getNewName("ω"), this.color, "name"])
|
||||
om_0.labelPosition = this.phase.execute() >= 0 ? "above" : "below"
|
||||
History.history.addToHistory(new CreateNewObject(om_0.name, "Point", om_0.export()))
|
||||
labelPosition = "below"
|
||||
}
|
||||
om_0.requiredBy.push(this)
|
||||
}
|
||||
/** @type {Point} */
|
||||
this.om_0 = om_0
|
||||
this.unit = unit
|
||||
this.labelPosition = labelPosition
|
||||
this.labelX = labelX
|
||||
}
|
||||
|
||||
getReadableString() {
|
||||
return `${this.name}: ${this.phase.toString(true)}${this.unit} at ${this.om_0.name} = ${this.om_0.x}`
|
||||
}
|
||||
|
||||
getLatexString() {
|
||||
return `${Latex.variable(this.name)}: ${this.phase.latexMarkup}\\textsf{${this.unit} at }${Latex.variable(this.om_0.name)} = ${this.om_0.x.latexMarkup}`
|
||||
}
|
||||
|
||||
execute(x = 1) {
|
||||
if(typeof x == "string") x = executeExpression(x)
|
||||
if(x < this.om_0.x) {
|
||||
return this.om_0.y.execute()
|
||||
} else {
|
||||
return this.om_0.y.execute() + this.phase.execute()
|
||||
}
|
||||
}
|
||||
|
||||
simplify(x = 1) {
|
||||
let xval = x
|
||||
if(typeof x == "string") xval = executeExpression(x)
|
||||
if(xval < this.om_0.x) {
|
||||
return this.om_0.y.toString()
|
||||
} else {
|
||||
let newExp = this.om_0.y.toEditableString() + " + " + this.phase.toEditableString()
|
||||
return new Expression(newExp)
|
||||
}
|
||||
}
|
||||
|
||||
canExecute(x = 1) {
|
||||
return true
|
||||
}
|
||||
|
||||
draw(canvas) {
|
||||
let baseX = canvas.x2px(this.om_0.x.execute())
|
||||
let omy = this.om_0.y.execute()
|
||||
let augmt = this.phase.execute()
|
||||
let baseY = canvas.y2px(omy)
|
||||
let augmtY = canvas.y2px(omy + augmt)
|
||||
// Before change line.
|
||||
canvas.drawLine(0, baseY, Math.min(baseX, canvas.height), baseY)
|
||||
// Transition line.
|
||||
canvas.drawLine(baseX, baseY, baseX, augmtY)
|
||||
// After change line
|
||||
canvas.drawLine(Math.max(0, baseX), augmtY, canvas.width, augmtY)
|
||||
|
||||
// Label
|
||||
this.drawLabel(canvas, this.labelPosition, canvas.x2px(this.labelX), canvas.y2px(this.execute(this.labelX)))
|
||||
}
|
||||
|
||||
update() {
|
||||
super.update()
|
||||
/** @type {BodePhaseSum[]} */
|
||||
let sumObjs = Objects.currentObjects["Somme phases Bode"]
|
||||
if(sumObjs !== undefined && sumObjs.length > 0) {
|
||||
sumObjs[0].recalculateCache()
|
||||
} else {
|
||||
Objects.createNewRegisteredObject("Somme phases Bode")
|
||||
}
|
||||
}
|
||||
|
||||
delete() {
|
||||
super.update()
|
||||
/** @type {BodePhaseSum[]} */
|
||||
let sumObjs = Objects.currentObjects["Somme phases Bode"]
|
||||
if(sumObjs !== undefined && sumObjs.length > 0) {
|
||||
sumObjs[0].recalculateCache()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
139
common/src/objs/bodephasesum.mjs
Normal file
139
common/src/objs/bodephasesum.mjs
Normal file
|
@ -0,0 +1,139 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { executeExpression, Expression } from "../math/index.mjs"
|
||||
import * as P from "../parameters.mjs"
|
||||
import Objects from "../module/objects.mjs"
|
||||
import Latex from "../module/latex.mjs"
|
||||
|
||||
import { ExecutableObject } from "./common.mjs"
|
||||
|
||||
export default class BodePhaseSum extends ExecutableObject {
|
||||
static type() {
|
||||
return "Somme phases Bode"
|
||||
}
|
||||
|
||||
static displayType() {
|
||||
return qsTranslate("bodephasesum", "Bode Phases Sum")
|
||||
}
|
||||
|
||||
static displayTypeMultiple() {
|
||||
return qsTranslate("bodephasesum", "Bode Phases Sum")
|
||||
}
|
||||
|
||||
static createable() {
|
||||
return false
|
||||
}
|
||||
|
||||
static properties() {
|
||||
return {
|
||||
[QT_TRANSLATE_NOOP("prop", "labelPosition")]: P.Enum.Position,
|
||||
[QT_TRANSLATE_NOOP("prop", "labelX")]: "number"
|
||||
}
|
||||
}
|
||||
|
||||
constructor(name = null, visible = true, color = null, labelContent = "name + value",
|
||||
labelPosition = "above", labelX = 1) {
|
||||
if(name == null) name = "φ"
|
||||
super(name, visible, color, labelContent)
|
||||
this.labelPosition = labelPosition
|
||||
this.labelX = labelX
|
||||
this.recalculateCache()
|
||||
}
|
||||
|
||||
getReadableString() {
|
||||
return `${this.name} = ${Objects.getObjectsName("Phase Bode").join(" + ")}`
|
||||
}
|
||||
|
||||
getLatexString() {
|
||||
return `${Latex.variable(this.name)} = ${Objects.getObjectsName("Phase Bode").map(name => Latex.variable(name)).join(" + ")}`
|
||||
}
|
||||
|
||||
execute(x = 1) {
|
||||
if(typeof x == "string") x = executeExpression(x)
|
||||
for(let i = 0; i < this.om0xList.length - 1; i++) {
|
||||
if(x >= this.om0xList[i] && x < this.om0xList[i + 1]) return this.phasesList[i]
|
||||
}
|
||||
}
|
||||
|
||||
simplify(x = 1) {
|
||||
let xval = x
|
||||
if(typeof x == "string") xval = executeExpression(x)
|
||||
for(let i = 0; i < this.om0xList.length - 1; i++) {
|
||||
if(xval >= this.om0xList[i] && xval < this.om0xList[i + 1]) {
|
||||
return (new Expression(this.phasesExprList[i])).simplify()
|
||||
}
|
||||
}
|
||||
return "0"
|
||||
}
|
||||
|
||||
canExecute(x = 1) {
|
||||
return true
|
||||
}
|
||||
|
||||
recalculateCache() {
|
||||
// Minimum to draw (can be expended if needed, just not infinite or it'll cause issues.
|
||||
let drawMin = 1e-20
|
||||
let drawMax = 1e20
|
||||
this.om0xList = [drawMin, drawMax]
|
||||
this.phasesList = [0]
|
||||
this.phasesExprList = ["0"]
|
||||
let phasesDict = new Map()
|
||||
let phasesExprDict = new Map()
|
||||
phasesDict.set(drawMax, 0)
|
||||
|
||||
let phaseObjects = Objects.currentObjects["Phase Bode"]
|
||||
if(phaseObjects === undefined || phaseObjects.length < 1) {
|
||||
Objects.deleteObject(this.name)
|
||||
} else {
|
||||
console.log("Recalculating cache phase")
|
||||
for(/** @type {BodePhase} */ let obj of phaseObjects) {
|
||||
this.om0xList.push(obj.om_0.x.execute())
|
||||
if(!phasesDict.has(obj.om_0.x.execute())) {
|
||||
phasesDict.set(obj.om_0.x.execute(), obj.phase.execute())
|
||||
phasesExprDict.set(obj.om_0.x.execute(), obj.phase.toEditableString())
|
||||
} else {
|
||||
phasesDict.set(obj.om_0.x.execute(), obj.phase.execute() + phasesDict.get(obj.om_0.x.execute()))
|
||||
phasesExprDict.set(obj.om_0.x.execute(), obj.phase.toEditableString() + "+" + phasesExprDict.get(obj.om_0.x.execute()))
|
||||
}
|
||||
this.phasesList[0] += obj.om_0.y.execute()
|
||||
this.phasesExprList[0] += "+" + obj.om_0.y.toEditableString()
|
||||
}
|
||||
this.om0xList.sort((a, b) => a - b)
|
||||
for(let i = 1; i < this.om0xList.length; i++) {
|
||||
this.phasesList[i] = this.phasesList[i - 1] + phasesDict.get(this.om0xList[i])
|
||||
this.phasesExprList[i] = this.phasesExprList[i - 1] + "+" + phasesDict.get(this.om0xList[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draw(canvas) {
|
||||
for(let i = 0; i < this.om0xList.length - 1; i++) {
|
||||
let om0xBegin = canvas.x2px(this.om0xList[i])
|
||||
let om0xEnd = canvas.x2px(this.om0xList[i + 1])
|
||||
let baseY = canvas.y2px(this.phasesList[i])
|
||||
let nextY = canvas.y2px(this.phasesList[i + 1])
|
||||
canvas.drawLine(om0xBegin, baseY, om0xEnd, baseY)
|
||||
canvas.drawLine(om0xEnd, baseY, om0xEnd, nextY)
|
||||
}
|
||||
|
||||
// Label
|
||||
this.drawLabel(canvas, this.labelPosition, canvas.x2px(this.labelX), canvas.y2px(this.execute(this.labelX)))
|
||||
}
|
||||
}
|
||||
|
418
common/src/objs/common.mjs
Normal file
418
common/src/objs/common.mjs
Normal file
|
@ -0,0 +1,418 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { getRandomColor } from "../utils.mjs"
|
||||
import Objects from "../module/objects.mjs"
|
||||
import Latex from "../module/latex.mjs"
|
||||
import { ensureTypeSafety, serializesByPropertyType } from "../parameters.mjs"
|
||||
|
||||
// This file contains the default data to be imported from all other objects
|
||||
|
||||
/**
|
||||
* Class to extend for every type of object that
|
||||
* can be drawn on the canvas.
|
||||
*/
|
||||
export class DrawableObject {
|
||||
/**
|
||||
* Base name of the object. Needs to be constant over time.
|
||||
* @return {string} Type of the object.
|
||||
*/
|
||||
static type() {
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
/**
|
||||
* Translated name of the object to be shown to the user.
|
||||
* @return {string}
|
||||
*/
|
||||
static displayType() {
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
/**
|
||||
* Translated name of the object in plural form to be shown to the user.
|
||||
* @return {string}
|
||||
*/
|
||||
static displayTypeMultiple() {
|
||||
return "Unknowns"
|
||||
}
|
||||
|
||||
/**
|
||||
* True if this object can be created by the user, false if it can only
|
||||
* be instantiated by other objects
|
||||
* @return {boolean}
|
||||
*/
|
||||
static createable() {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* List of properties used in the Object Editor.
|
||||
*
|
||||
* Properties are set with key as property name and
|
||||
* value as it's type name (e.g 'numbers', 'string'...),
|
||||
* an Enum for enumerations, an ObjectType for DrawableObjects
|
||||
* with a specific type, a List instance for lists, a
|
||||
* Dictionary instance for dictionaries, an Expression for expressions...
|
||||
*
|
||||
* If the property is to be translated, the key should be passed
|
||||
* through the QT_TRANSLATE_NOOP macro in that form:
|
||||
* [QT_TRANSLATE_NOOP('prop','key')]
|
||||
* Enums that are translated should be indexed in parameters.mjs and
|
||||
* then be linked directly here.
|
||||
*
|
||||
* @return {Object.<string,string|Enum|List|ObjectType|Dictionary>}
|
||||
*/
|
||||
static properties() {
|
||||
return {}
|
||||
}
|
||||
|
||||
/**
|
||||
* True if this object can be executed, so that an y value might be computed
|
||||
* for an x value. Only ExecutableObjects have that property set to true.
|
||||
* @return {boolean}
|
||||
*/
|
||||
static executable() {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports the object from its serialized form.
|
||||
* @return {DrawableObject}
|
||||
*/
|
||||
static import(name, visible, color, labelContent, ...args) {
|
||||
let importedArgs = [name.toString(), visible === true, color.toString(), labelContent]
|
||||
console.log("Importing", this.type(), name, args)
|
||||
for(let [name, propType] of Object.entries(this.properties()))
|
||||
if(!name.startsWith("comment")) {
|
||||
importedArgs.push(ensureTypeSafety(propType, args[importedArgs.length - 4]))
|
||||
}
|
||||
return new this(...importedArgs)
|
||||
}
|
||||
|
||||
/**
|
||||
* Base constructor for the object.
|
||||
* @param {string} name - Name of the object
|
||||
* @param {boolean} visible - true if the object is visible, false otherwise.
|
||||
* @param {color|string} color - Color of the object (can be string or QColor)
|
||||
* @param {"null"|"name"|"name + value"} labelContent - One of 'null', 'name' or 'name + value' describing the content of the label.
|
||||
* @constructor
|
||||
*/
|
||||
constructor(name, visible = true, color = null, labelContent = "name + value") {
|
||||
if(color == null) color = getRandomColor()
|
||||
this.type = this.constructor.type()
|
||||
this.name = name
|
||||
this.visible = visible
|
||||
this.color = color
|
||||
this.labelContent = labelContent // "null", "name", "name + value"
|
||||
this.requiredBy = []
|
||||
this.requires = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the object in an array that can be JSON serialized.
|
||||
* These parameters will be re-entered in the constructor when restored.
|
||||
* @return {array}
|
||||
*/
|
||||
export() {
|
||||
let exportList = [this.name, this.visible, this.color.toString(), this.labelContent]
|
||||
for(let [name, propType] of Object.entries(this.constructor.properties()))
|
||||
if(!name.startsWith("comment"))
|
||||
exportList.push(serializesByPropertyType(propType, this[name]))
|
||||
return exportList
|
||||
}
|
||||
|
||||
/**
|
||||
* String representing the object that will be displayed to the user.
|
||||
* It allows for 2 lines and translated text, but it shouldn't be too long.
|
||||
* @return {string}
|
||||
*/
|
||||
getReadableString() {
|
||||
return `${this.name} = Unknown`
|
||||
}
|
||||
|
||||
/**
|
||||
* Latex markuped version of the readable string.
|
||||
* Every non latin character should be passed as latex symbols and formulas
|
||||
* should be in latex form.
|
||||
* See ../latex.mjs for helper methods.
|
||||
* @return {string}
|
||||
*/
|
||||
getLatexString() {
|
||||
return this.getReadableString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Readable string content of the label depending on the value of the latexContent.
|
||||
* @return {string}
|
||||
*/
|
||||
getLabel() {
|
||||
switch(this.labelContent) {
|
||||
case "name":
|
||||
return this.name
|
||||
case "name + value":
|
||||
return this.getReadableString()
|
||||
case "null":
|
||||
return ""
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Latex markup string content of the label depending on the value of the latexContent.
|
||||
* Every non latin character should be passed as latex symbols and formulas
|
||||
* should be in latex form.
|
||||
* See ../latex.mjs for helper methods.
|
||||
* @return {string}
|
||||
*/
|
||||
getLatexLabel() {
|
||||
switch(this.labelContent) {
|
||||
case "name":
|
||||
return Latex.variable(this.name)
|
||||
case "name + value":
|
||||
return this.getLatexString()
|
||||
case "null":
|
||||
return ""
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the recursive list of objects this one depends on.
|
||||
* @return {array}
|
||||
*/
|
||||
getDependenciesList() {
|
||||
let dependencies = this.requires.map(obj => obj)
|
||||
for(let obj of this.requires)
|
||||
dependencies = dependencies.concat(obj.getDependenciesList())
|
||||
return dependencies
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback method when one of the properties of the object is updated.
|
||||
*/
|
||||
update() {
|
||||
// Refreshing dependencies.
|
||||
for(let obj of this.requires)
|
||||
obj.requiredBy = obj.requiredBy.filter(dep => dep !== this)
|
||||
// Checking objects this one depends on
|
||||
this.requires = []
|
||||
let currentObjectsByName = Objects.currentObjectsByName
|
||||
let properties = this.constructor.properties()
|
||||
for(let property in properties)
|
||||
if(typeof properties[property] == "object" && "type" in properties[property])
|
||||
if(properties[property].type === "Expression" && this[property] != null) {
|
||||
// Expressions with dependencies
|
||||
for(let objName of this[property].requiredObjects()) {
|
||||
if(objName in currentObjectsByName && !this.requires.includes(currentObjectsByName[objName])) {
|
||||
this.requires.push(currentObjectsByName[objName])
|
||||
currentObjectsByName[objName].requiredBy.push(this)
|
||||
}
|
||||
}
|
||||
if(this[property].cached && this[property].requiredObjects().length > 0)
|
||||
// Recalculate
|
||||
this[property].recache()
|
||||
|
||||
} else if(properties[property].type === "ObjectType" && this[property] != null) {
|
||||
// Object dependency
|
||||
this.requires.push(this[property])
|
||||
this[property].requiredBy.push(this)
|
||||
}
|
||||
// Updating objects dependent on this one
|
||||
for(let req of this.requiredBy)
|
||||
req.update()
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback method when the object is about to get deleted.
|
||||
*/
|
||||
delete() {
|
||||
for(let toRemove of this.requiredBy) { // Normally, there should be none here, but better leave nothing just in case.
|
||||
Objects.deleteObject(toRemove.name)
|
||||
}
|
||||
for(let toRemoveFrom of this.requires) {
|
||||
toRemoveFrom.requiredBy = toRemoveFrom.requiredBy.filter(o => o !== this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract method. Draw the object onto the canvas with the.
|
||||
* @param {CanvasAPI} canvas
|
||||
*/
|
||||
draw(canvas) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Applicates a drawFunction with two position arguments depending on
|
||||
* both the posX and posY of where the label should be displayed,
|
||||
* and the labelPosition which declares the label should be displayed
|
||||
* relatively to that position.
|
||||
*
|
||||
* @param {string|Enum} labelPosition - Position of the label relative to the marked position
|
||||
* @param {number} offset - Margin between the position and the object to be drawn
|
||||
* @param {{width: number, height: number}} size - Size of the label item, containing two properties, "width" and "height"
|
||||
* @param {number} posX - Component of the marked position on the x-axis
|
||||
* @param {number} posY - Component of the marked position on the y-axis
|
||||
* @param {function} drawFunction - Function with two arguments (x, y) that will be called to draw the label
|
||||
*/
|
||||
drawPositionDivergence(labelPosition, offset, size, posX, posY, drawFunction) {
|
||||
switch(labelPosition) {
|
||||
case "center":
|
||||
drawFunction(posX - size.width / 2, posY - size.height / 2)
|
||||
break
|
||||
case "top":
|
||||
case "above":
|
||||
drawFunction(posX - size.width / 2, posY - size.height - offset)
|
||||
break
|
||||
case "bottom":
|
||||
case "below":
|
||||
drawFunction(posX - size.width / 2, posY + offset)
|
||||
break
|
||||
case "left":
|
||||
drawFunction(posX - size.width - offset, posY - size.height / 2)
|
||||
break
|
||||
case "right":
|
||||
drawFunction(posX + offset, posY - size.height / 2)
|
||||
break
|
||||
case "top-left":
|
||||
case "above-left":
|
||||
drawFunction(posX - size.width, posY - size.height - offset)
|
||||
break
|
||||
case "top-right":
|
||||
case "above-right":
|
||||
drawFunction(posX + offset, posY - size.height - offset)
|
||||
break
|
||||
case "bottom-left":
|
||||
case "below-left":
|
||||
drawFunction(posX - size.width - offset, posY + offset)
|
||||
break
|
||||
case "bottom-right":
|
||||
case "below-right":
|
||||
drawFunction(posX + offset, posY + offset)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically draw text (by default the label of the object on the canvas
|
||||
* depending on user settings.
|
||||
* This method takes into account both the posX and posY of where the label
|
||||
* should be displayed, including the labelPosition relative to it.
|
||||
* The text is get both through the getLatexFunction and getTextFunction
|
||||
* depending on whether to use latex.
|
||||
* Then, it's displayed using the drawFunctionLatex (x,y,imageData) and
|
||||
* drawFunctionText (x,y,text) depending on whether to use latex.
|
||||
*
|
||||
* @param {CanvasAPI} canvas
|
||||
* @param {string|Enum} labelPosition - Position of the label relative to the marked position
|
||||
* @param {number} posX - Component of the marked position on the x-axis
|
||||
* @param {number} posY - Component of the marked position on the y-axis
|
||||
* @param {boolean} forceText - Force the rendering of the label as text
|
||||
* @param {function|null} getLatexFunction - Function (no argument) to get the latex markup to be displayed
|
||||
* @param {function|null} getTextFunction - Function (no argument) to get the text to be displayed
|
||||
* @param {function|null} drawFunctionLatex - Function (x,y,imageData) to display the latex image
|
||||
* @param {function|null} drawFunctionText - Function (x,y,text,textSize) to display the text
|
||||
*/
|
||||
drawLabel(canvas, labelPosition, posX, posY, forceText = false,
|
||||
getLatexFunction = null, getTextFunction = null, drawFunctionLatex = null, drawFunctionText = null) {
|
||||
// Default functions
|
||||
if(getLatexFunction == null)
|
||||
getLatexFunction = this.getLatexLabel.bind(this)
|
||||
if(getTextFunction == null)
|
||||
getTextFunction = this.getLabel.bind(this)
|
||||
if(drawFunctionLatex == null)
|
||||
drawFunctionLatex = (x, y, ltxImg) => canvas.drawVisibleImage(ltxImg.source, x, y, ltxImg.width, ltxImg.height)
|
||||
if(drawFunctionText == null)
|
||||
drawFunctionText = (x, y, text, textSize) => canvas.drawVisibleText(text, x, y + textSize.height) // Positioned from left bottom
|
||||
// Drawing the label
|
||||
if(!forceText && Latex.enabled) {
|
||||
// With latex
|
||||
let drawLblCb = ((ltxImg) => {
|
||||
this.drawPositionDivergence(labelPosition, 8 + canvas.linewidth / 2, ltxImg, posX, posY, (x, y) => drawFunctionLatex(x, y, ltxImg))
|
||||
})
|
||||
let ltxLabel = getLatexFunction()
|
||||
if(ltxLabel !== "")
|
||||
canvas.renderLatexImage(ltxLabel, this.color, drawLblCb)
|
||||
} else {
|
||||
// Without latex
|
||||
let text = getTextFunction()
|
||||
canvas.font = `${canvas.textsize}px sans-serif`
|
||||
let textSize = canvas.measureText(text)
|
||||
this.drawPositionDivergence(labelPosition, 8 + canvas.linewidth / 2, textSize, posX, posY, (x, y) => drawFunctionText(x, y, text, textSize))
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.name
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Class to be extended for every object on which
|
||||
* an y for a x can be computed with the execute function.
|
||||
* If a value cannot be found during execute, it will
|
||||
* return null. However, theses same x values will
|
||||
* return false when passed to canExecute.
|
||||
*/
|
||||
export class ExecutableObject extends DrawableObject {
|
||||
/**
|
||||
* Returns the corresponding y value for an x value.
|
||||
* If the object isn't defined on the given x, then
|
||||
* this function will return null.
|
||||
*
|
||||
* @param {number} x
|
||||
* @returns {number|null}
|
||||
*/
|
||||
execute(x = 1) {
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns false if the object isn't defined on the given x, true otherwise.
|
||||
*
|
||||
* @param {number} x
|
||||
* @returns {boolean}
|
||||
*/
|
||||
canExecute(x = 1) {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the simplified expression string for a given x.
|
||||
*
|
||||
* @param {number} x
|
||||
* @returns {string|Expression}
|
||||
*/
|
||||
simplify(x = 1) {
|
||||
return "0"
|
||||
}
|
||||
|
||||
/**
|
||||
* True if this object can be executed, so that an y value might be computed
|
||||
* for an x value. Only ExecutableObjects have that property set to true.
|
||||
* @return {boolean}
|
||||
*/
|
||||
static executable() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
159
common/src/objs/distribution.mjs
Normal file
159
common/src/objs/distribution.mjs
Normal file
|
@ -0,0 +1,159 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as P from "../parameters.mjs"
|
||||
import Objects from "../module/objects.mjs"
|
||||
import Latex from "../module/latex.mjs"
|
||||
|
||||
import { ExecutableObject } from "./common.mjs"
|
||||
|
||||
|
||||
export default class DistributionFunction extends ExecutableObject {
|
||||
static type() {
|
||||
return "Repartition"
|
||||
}
|
||||
|
||||
static displayType() {
|
||||
return qsTranslate("distribution", "Repartition")
|
||||
}
|
||||
|
||||
static displayTypeMultiple() {
|
||||
return qsTranslate("distribution", "Repartition functions")
|
||||
}
|
||||
|
||||
static properties() {
|
||||
return {
|
||||
"comment1": QT_TRANSLATE_NOOP(
|
||||
"comment",
|
||||
"Note: Specify the probability for each value."
|
||||
),
|
||||
[QT_TRANSLATE_NOOP("prop", "probabilities")]: new P.Dictionary("string", "double", /^-?[\d.,]+$/, /^-?[\d.,]+$/, "P({name_} = ", ") = "),
|
||||
[QT_TRANSLATE_NOOP("prop", "labelPosition")]: P.Enum.Position,
|
||||
[QT_TRANSLATE_NOOP("prop", "labelX")]: "number"
|
||||
}
|
||||
}
|
||||
|
||||
static import(name, visible, color, labelContent, ...args) {
|
||||
console.log(args, args.length)
|
||||
if(args.length === 5) {
|
||||
// Two legacy values no longer included.
|
||||
args.shift()
|
||||
args.shift()
|
||||
}
|
||||
return super.import(name, visible, color, labelContent, ...args)
|
||||
}
|
||||
|
||||
constructor(name = null, visible = true, color = null, labelContent = "name + value",
|
||||
probabilities = { "0": "0" }, labelPosition = "above", labelX = 1) {
|
||||
if(name == null) name = Objects.getNewName("XYZUVW", "F_")
|
||||
super(name, visible, color, labelContent)
|
||||
this.probabilities = probabilities
|
||||
this.labelPosition = labelPosition
|
||||
this.labelX = labelX
|
||||
this.update()
|
||||
}
|
||||
|
||||
getReadableString() {
|
||||
let keys = Object.keys(this.probabilities).sort((a, b) => a - b)
|
||||
let varname = this.name.substring(this.name.indexOf("_") + 1)
|
||||
return `${this.name}(x) = P(${varname} ≤ x)\n` + keys.map(idx => `P(${varname}=${idx})=${this.probabilities[idx]}`).join("; ")
|
||||
}
|
||||
|
||||
getLatexString() {
|
||||
let keys = Object.keys(this.probabilities).sort((a, b) => a - b)
|
||||
let funcName = Latex.variable(this.name)
|
||||
let varName = Latex.variable(this.name.substring(this.name.indexOf("_") + 1))
|
||||
return `\\begin{array}{l}{${funcName}}(x) = P(${varName} \\le x)\\\\` + keys.map(idx => `P(${varName}=${idx})=${this.probabilities[idx]}`).join("; ") + "\\end{array}"
|
||||
}
|
||||
|
||||
execute(x = 1) {
|
||||
let ret = 0
|
||||
Object.keys(this.probabilities).sort((a, b) => a - b).forEach(idx => {
|
||||
if(x >= idx) ret += parseFloat(this.probabilities[idx].replace(/,/g, "."))
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
canExecute(x = 1) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Simplify returns the simplified string of the expression.
|
||||
simplify(x = 1) {
|
||||
return this.execute(x).toString()
|
||||
}
|
||||
|
||||
getLabel() {
|
||||
switch(this.labelContent) {
|
||||
case "name":
|
||||
return `${this.name}(x)`
|
||||
case "name + value":
|
||||
return this.getReadableString()
|
||||
case "null":
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
draw(canvas) {
|
||||
let currentY = 0
|
||||
let keys = Object.keys(this.probabilities).map(idx => parseInt(idx)).sort((a, b) => a - b)
|
||||
if(canvas.isVisible(keys[0], this.probabilities[keys[0]].replace(/,/g, "."))) {
|
||||
canvas.drawLine(0, canvas.y2px(0), canvas.x2px(keys[0]), canvas.y2px(0))
|
||||
if(canvas.isVisible(keys[0], 0)) {
|
||||
canvas.arc(canvas.x2px(keys[0]) + 4, canvas.y2px(0), 4, Math.PI / 2, 3 * Math.PI / 2)
|
||||
}
|
||||
}
|
||||
for(let i = 0; i < keys.length - 1; i++) {
|
||||
let idx = keys[i]
|
||||
currentY += parseFloat(this.probabilities[idx].replace(/,/g, "."))
|
||||
if(canvas.isVisible(idx, currentY) || canvas.isVisible(keys[i + 1], currentY)) {
|
||||
canvas.drawLine(
|
||||
Math.max(0, canvas.x2px(idx)),
|
||||
canvas.y2px(currentY),
|
||||
Math.min(canvas.width, canvas.x2px(keys[i + 1])),
|
||||
canvas.y2px(currentY)
|
||||
)
|
||||
if(canvas.isVisible(idx, currentY)) {
|
||||
canvas.disc(canvas.x2px(idx), canvas.y2px(currentY), 4)
|
||||
}
|
||||
if(canvas.isVisible(keys[i + 1], currentY)) {
|
||||
canvas.arc(canvas.x2px(keys[i + 1]) + 4, canvas.y2px(currentY), 4, Math.PI / 2, 3 * Math.PI / 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
if(canvas.isVisible(keys[keys.length - 1], currentY + parseFloat(this.probabilities[keys[keys.length - 1]]))) {
|
||||
canvas.drawLine(
|
||||
Math.max(0, canvas.x2px(keys[keys.length - 1])),
|
||||
canvas.y2px(currentY + parseFloat(this.probabilities[keys[keys.length - 1]].replace(/,/g, "."))),
|
||||
canvas.width,
|
||||
canvas.y2px(currentY + parseFloat(this.probabilities[keys[keys.length - 1]].replace(/,/g, ".")))
|
||||
)
|
||||
canvas.disc(
|
||||
canvas.x2px(keys[keys.length - 1]),
|
||||
canvas.y2px(
|
||||
currentY + parseFloat(this.probabilities[keys[keys.length - 1]].replace(/,/g, "."))
|
||||
),
|
||||
4
|
||||
)
|
||||
}
|
||||
|
||||
// Label
|
||||
this.drawLabel(canvas, this.labelPosition, canvas.x2px(this.labelX), canvas.y2px(this.execute(this.labelX)))
|
||||
}
|
||||
}
|
||||
|
201
common/src/objs/function.mjs
Normal file
201
common/src/objs/function.mjs
Normal file
|
@ -0,0 +1,201 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { textsub } from "../utils.mjs"
|
||||
import Objects from "../module/objects.mjs"
|
||||
import { ExecutableObject } from "./common.mjs"
|
||||
import { parseDomain, Expression, SpecialDomain } from "../math/index.mjs"
|
||||
import * as P from "../parameters.mjs"
|
||||
import Latex from "../module/latex.mjs"
|
||||
|
||||
|
||||
export default class Function extends ExecutableObject {
|
||||
static type() {
|
||||
return "Function"
|
||||
}
|
||||
|
||||
static displayType() {
|
||||
return qsTranslate("function", "Function")
|
||||
}
|
||||
|
||||
static displayTypeMultiple() {
|
||||
return qsTranslate("function", "Functions")
|
||||
}
|
||||
|
||||
static properties() {
|
||||
return {
|
||||
[QT_TRANSLATE_NOOP("prop", "expression")]: new P.Expression("x"),
|
||||
[QT_TRANSLATE_NOOP("prop", "definitionDomain")]: "Domain",
|
||||
[QT_TRANSLATE_NOOP("prop", "destinationDomain")]: "Domain",
|
||||
"comment1": QT_TRANSLATE_NOOP(
|
||||
"comment",
|
||||
"Ex: R+* (ℝ⁺*), N (ℕ), Z-* (ℤ⁻*), ]0;1[, {3;4;5}"
|
||||
),
|
||||
[QT_TRANSLATE_NOOP("prop", "displayMode")]: P.Enum.FunctionDisplayType,
|
||||
[QT_TRANSLATE_NOOP("prop", "labelPosition")]: P.Enum.Position,
|
||||
[QT_TRANSLATE_NOOP("prop", "labelX")]: "number",
|
||||
"comment2": QT_TRANSLATE_NOOP(
|
||||
"comment",
|
||||
"The following parameters are used when the definition domain is a non-continuous set. (Ex: ℕ, ℤ, sets like {0;3}...)"
|
||||
),
|
||||
[QT_TRANSLATE_NOOP("prop", "drawPoints")]: "boolean",
|
||||
[QT_TRANSLATE_NOOP("prop", "drawDashedLines")]: "boolean"
|
||||
}
|
||||
}
|
||||
|
||||
constructor(name = null, visible = true, color = null, labelContent = "name + value",
|
||||
expression = "x", definitionDomain = "RPE", destinationDomain = "R",
|
||||
displayMode = "application", labelPosition = "above", labelX = 1,
|
||||
drawPoints = true, drawDashedLines = true) {
|
||||
if(name == null) name = Objects.getNewName("fghjqlmnopqrstuvwabcde")
|
||||
super(name, visible, color, labelContent)
|
||||
if(typeof expression == "number" || typeof expression == "string") expression = new Expression(expression.toString())
|
||||
this.expression = expression
|
||||
if(typeof definitionDomain == "string") definitionDomain = parseDomain(definitionDomain)
|
||||
this.definitionDomain = definitionDomain
|
||||
if(typeof destinationDomain == "string") destinationDomain = parseDomain(destinationDomain)
|
||||
this.destinationDomain = destinationDomain
|
||||
this.displayMode = displayMode
|
||||
this.labelPosition = labelPosition
|
||||
this.labelX = labelX
|
||||
this.drawPoints = drawPoints
|
||||
this.drawDashedLines = drawDashedLines
|
||||
}
|
||||
|
||||
getReadableString() {
|
||||
if(this.displayMode === "application") {
|
||||
return `${this.name}: ${this.definitionDomain} ⟶ ${this.destinationDomain}\n ${" ".repeat(this.name.length)}x ⟼ ${this.expression.toString()}`
|
||||
} else {
|
||||
return `${this.name}(x) = ${this.expression.toString()}\nD${textsub(this.name)} = ${this.definitionDomain}`
|
||||
}
|
||||
}
|
||||
|
||||
getLatexString() {
|
||||
if(this.displayMode === "application") {
|
||||
return `${Latex.variable(this.name)}:\\begin{array}{llll}${this.definitionDomain.latexMarkup}\\textrm{ } & \\rightarrow & \\textrm{ }${this.destinationDomain.latexMarkup}\\\\
|
||||
x\\textrm{ } & \\mapsto & \\textrm{ }${this.expression.latexMarkup}\\end{array}`
|
||||
} else {
|
||||
return `\\begin{array}{l}${Latex.variable(this.name)}(x) = ${this.expression.latexMarkup}\\\\ D_{${this.name}} = ${this.definitionDomain.latexMarkup}\\end{array}`
|
||||
}
|
||||
}
|
||||
|
||||
execute(x = 1) {
|
||||
if(this.definitionDomain.includes(x))
|
||||
return this.expression.execute(x)
|
||||
return null
|
||||
}
|
||||
|
||||
canExecute(x = 1) {
|
||||
return this.definitionDomain.includes(x)
|
||||
}
|
||||
|
||||
simplify(x = 1) {
|
||||
if(this.definitionDomain.includes(x))
|
||||
return this.expression.simplify(x)
|
||||
return ""
|
||||
}
|
||||
|
||||
draw(canvas) {
|
||||
Function.drawFunction(canvas, this.expression, this.definitionDomain, this.destinationDomain, this.drawPoints, this.drawDashedLines)
|
||||
// Label
|
||||
this.drawLabel(canvas, this.labelPosition, canvas.x2px(this.labelX), canvas.y2px(this.execute(this.labelX)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable in other objects.
|
||||
* Drawing small traits every few pixels
|
||||
*/
|
||||
static drawFunction(canvas, expr, definitionDomain, destinationDomain, drawPoints = true, drawDash = true) {
|
||||
let pxprecision = 10
|
||||
let previousX = canvas.px2x(0)
|
||||
let previousY = null
|
||||
if(definitionDomain instanceof SpecialDomain && definitionDomain.moveSupported) {
|
||||
// Point based functions.
|
||||
previousX = definitionDomain.next(previousX)
|
||||
if(previousX === null) previousX = definitionDomain.next(canvas.px2x(0))
|
||||
previousY = expr.execute(previousX)
|
||||
if(!drawPoints && !drawDash) return
|
||||
while(previousX !== null && canvas.x2px(previousX) < canvas.width) {
|
||||
// Reconverted for canvas to fix for logarithmic scales.
|
||||
let currentX = definitionDomain.next(canvas.px2x(canvas.x2px(previousX) + pxprecision))
|
||||
let currentY = expr.execute(currentX)
|
||||
if(currentX === null) break
|
||||
if((definitionDomain.includes(currentX) || definitionDomain.includes(previousX)) &&
|
||||
(destinationDomain.includes(currentY) || destinationDomain.includes(previousY))) {
|
||||
if(drawDash)
|
||||
canvas.drawDashedLine(canvas.x2px(previousX), canvas.y2px(previousY), canvas.x2px(currentX), canvas.y2px(currentY))
|
||||
if(drawPoints) {
|
||||
canvas.fillRect(canvas.x2px(previousX) - 5, canvas.y2px(previousY) - 1, 10, 2)
|
||||
canvas.fillRect(canvas.x2px(previousX) - 1, canvas.y2px(previousY) - 5, 2, 10)
|
||||
}
|
||||
}
|
||||
previousX = currentX
|
||||
previousY = currentY
|
||||
}
|
||||
if(drawPoints) {
|
||||
// Drawing the last cross
|
||||
canvas.fillRect(canvas.x2px(previousX) - 5, canvas.y2px(previousY) - 1, 10, 2)
|
||||
canvas.fillRect(canvas.x2px(previousX) - 1, canvas.y2px(previousY) - 5, 2, 10)
|
||||
}
|
||||
} else {
|
||||
// Use max precision if function is trigonometrical on log scale.
|
||||
let exprString = expr.expr
|
||||
if(exprString.includes("sin") || exprString.includes("cos") || exprString.includes("tan"))
|
||||
pxprecision = (canvas.logscalex || exprString.includes("tan")) ? 1 : 3
|
||||
// Calculate the previousY at the start of the canvas
|
||||
if(definitionDomain.includes(previousX))
|
||||
previousY = expr.execute(previousX)
|
||||
for(let px = pxprecision; px < canvas.width; px += pxprecision) {
|
||||
let currentX = canvas.px2x(px)
|
||||
if(!definitionDomain.includes(previousX) && definitionDomain.includes(currentX)) {
|
||||
// Should draw up to currentX, but NOT at previousX.
|
||||
// Need to find the starting point.
|
||||
let tmpPx = px - pxprecision
|
||||
do {
|
||||
tmpPx++
|
||||
previousX = canvas.px2x(tmpPx)
|
||||
} while(!definitionDomain.includes(previousX))
|
||||
// Recaclulate previousY
|
||||
previousY = expr.execute(previousX)
|
||||
} else if(!definitionDomain.includes(currentX)) {
|
||||
// Next x is NOT in the definition domain.
|
||||
// Augmenting the pixel precision until this condition is fulfilled.
|
||||
let tmpPx = px
|
||||
do {
|
||||
tmpPx--
|
||||
currentX = canvas.px2x(tmpPx)
|
||||
} while(!definitionDomain.includes(currentX) && currentX !== previousX)
|
||||
}
|
||||
// This max variation is needed for functions with asymptotical vertical lines (e.g. 1/x, tan x...)
|
||||
let maxvariation = (canvas.px2y(0) - canvas.px2y(canvas.height))
|
||||
if(definitionDomain.includes(previousX) && definitionDomain.includes(currentX)) {
|
||||
let currentY = expr.execute(currentX)
|
||||
if(destinationDomain.includes(currentY)) {
|
||||
if(previousY != null && destinationDomain.includes(previousY) && Math.abs(previousY - currentY) < maxvariation) {
|
||||
canvas.drawLine(canvas.x2px(previousX), canvas.y2px(previousY), canvas.x2px(currentX), canvas.y2px(currentY))
|
||||
}
|
||||
}
|
||||
previousY = currentY
|
||||
} else {
|
||||
previousY = null // Last y was invalid, so let's not draw anything from it.
|
||||
}
|
||||
previousX = canvas.px2x(px)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
87
common/src/objs/point.mjs
Normal file
87
common/src/objs/point.mjs
Normal file
|
@ -0,0 +1,87 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Expression } from "../math/index.mjs"
|
||||
import * as P from "../parameters.mjs"
|
||||
import Objects from "../module/objects.mjs"
|
||||
import Latex from "../module/latex.mjs"
|
||||
|
||||
import { DrawableObject } from "./common.mjs"
|
||||
|
||||
|
||||
export default class Point extends DrawableObject {
|
||||
static type() {
|
||||
return "Point"
|
||||
}
|
||||
|
||||
static displayType() {
|
||||
return qsTranslate("point", "Point")
|
||||
}
|
||||
|
||||
static displayTypeMultiple() {
|
||||
return qsTranslate("point", "Points")
|
||||
}
|
||||
|
||||
static properties() {
|
||||
return {
|
||||
[QT_TRANSLATE_NOOP("prop", "x")]: new P.Expression(),
|
||||
[QT_TRANSLATE_NOOP("prop", "y")]: new P.Expression(),
|
||||
[QT_TRANSLATE_NOOP("prop", "labelPosition")]: P.Enum.Position,
|
||||
[QT_TRANSLATE_NOOP("prop", "pointStyle")]: new P.Enum("●", "✕", "+")
|
||||
}
|
||||
}
|
||||
|
||||
constructor(name = null, visible = true, color = null, labelContent = "name + value",
|
||||
x = 1, y = 0, labelPosition = "above", pointStyle = "●") {
|
||||
if(name == null) name = Objects.getNewName("ABCDEFJKLMNOPQRSTUVW")
|
||||
super(name, visible, color, labelContent)
|
||||
if(typeof x == "number" || typeof x == "string") x = new Expression(x.toString())
|
||||
this.x = x
|
||||
if(typeof y == "number" || typeof y == "string") y = new Expression(y.toString())
|
||||
this.y = y
|
||||
this.labelPosition = labelPosition
|
||||
this.pointStyle = pointStyle
|
||||
}
|
||||
|
||||
getReadableString() {
|
||||
return `${this.name} = (${this.x}, ${this.y})`
|
||||
}
|
||||
|
||||
getLatexString() {
|
||||
return `${Latex.variable(this.name)} = \\left(${this.x.latexMarkup}, ${this.y.latexMarkup}\\right)`
|
||||
}
|
||||
|
||||
draw(canvas) {
|
||||
let [canvasX, canvasY] = [canvas.x2px(this.x.execute()), canvas.y2px(this.y.execute())]
|
||||
let pointSize = 8 + (canvas.linewidth * 2)
|
||||
switch(this.pointStyle) {
|
||||
case "●":
|
||||
canvas.disc(canvasX, canvasY, pointSize / 2)
|
||||
break
|
||||
case "✕":
|
||||
canvas.drawLine(canvasX - pointSize / 2, canvasY - pointSize / 2, canvasX + pointSize / 2, canvasY + pointSize / 2)
|
||||
canvas.drawLine(canvasX - pointSize / 2, canvasY + pointSize / 2, canvasX + pointSize / 2, canvasY - pointSize / 2)
|
||||
break
|
||||
case "+":
|
||||
canvas.fillRect(canvasX - pointSize / 2, canvasY - 1, pointSize, 2)
|
||||
canvas.fillRect(canvasX - 1, canvasY - pointSize / 2, 2, pointSize)
|
||||
break
|
||||
}
|
||||
this.drawLabel(canvas, this.labelPosition, canvasX, canvasY)
|
||||
}
|
||||
}
|
140
common/src/objs/sequence.mjs
Normal file
140
common/src/objs/sequence.mjs
Normal file
|
@ -0,0 +1,140 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Sequence as MathSequence, Domain } from "../math/index.mjs"
|
||||
import * as P from "../parameters.mjs"
|
||||
import Latex from "../module/latex.mjs"
|
||||
import Objects from "../module/objects.mjs"
|
||||
|
||||
import { ExecutableObject } from "./common.mjs"
|
||||
import Function from "./function.mjs"
|
||||
|
||||
|
||||
export default class Sequence extends ExecutableObject {
|
||||
static type() {
|
||||
return "Sequence"
|
||||
}
|
||||
|
||||
static displayType() {
|
||||
return qsTranslate("sequence", "Sequence")
|
||||
}
|
||||
|
||||
static displayTypeMultiple() {
|
||||
return qsTranslate("sequence", "Sequences")
|
||||
}
|
||||
|
||||
static properties() {
|
||||
return {
|
||||
[QT_TRANSLATE_NOOP("prop", "drawPoints")]: "boolean",
|
||||
[QT_TRANSLATE_NOOP("prop", "drawDashedLines")]: "boolean",
|
||||
[QT_TRANSLATE_NOOP("prop", "defaultExpression")]: new P.Dictionary("string", "int", /^.+$/, /^\d+$/, "{name}[n+", "] = ", true),
|
||||
"comment1": QT_TRANSLATE_NOOP(
|
||||
"comment",
|
||||
"Note: Use %1[n] to refer to %1ₙ, %1[n+1] for %1ₙ₊₁..."
|
||||
),
|
||||
[QT_TRANSLATE_NOOP("prop", "baseValues")]: new P.Dictionary("string", "int", /^.+$/, /^\d+$/, "{name}[", "] = "),
|
||||
[QT_TRANSLATE_NOOP("prop", "labelPosition")]: P.Enum.Position,
|
||||
[QT_TRANSLATE_NOOP("prop", "labelX")]: "number"
|
||||
}
|
||||
}
|
||||
|
||||
constructor(name = null, visible = true, color = null, labelContent = "name + value",
|
||||
drawPoints = true, drawDashedLines = true, defaultExp = { 1: "n" },
|
||||
baseValues = { 0: 0 }, labelPosition = "above", labelX = 1) {
|
||||
if(name == null) name = Objects.getNewName("uvwPSUVWabcde")
|
||||
super(name, visible, color, labelContent)
|
||||
this.drawPoints = drawPoints
|
||||
this.drawDashedLines = drawDashedLines
|
||||
this.defaultExpression = defaultExp
|
||||
this.baseValues = baseValues
|
||||
this.labelPosition = labelPosition
|
||||
this.labelX = labelX
|
||||
this.update()
|
||||
}
|
||||
|
||||
update() {
|
||||
console.log("Updating sequence", this.sequence)
|
||||
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.toString() !== Object.keys(this.defaultExpression)[0]
|
||||
)
|
||||
this.sequence = new MathSequence(
|
||||
this.name, this.baseValues,
|
||||
parseFloat(Object.keys(this.defaultExpression)[0]),
|
||||
Object.values(this.defaultExpression)[0]
|
||||
)
|
||||
}
|
||||
|
||||
getReadableString() {
|
||||
return this.sequence.toString()
|
||||
}
|
||||
|
||||
getLatexString() {
|
||||
return this.sequence.toLatexString()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
getLabel() {
|
||||
switch(this.labelContent) {
|
||||
case "name":
|
||||
return `(${this.name}ₙ)`
|
||||
case "name + value":
|
||||
return this.getReadableString()
|
||||
case "null":
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
getLatexLabel() {
|
||||
switch(this.labelContent) {
|
||||
case "name":
|
||||
return `(${Latex.variable(this.name)}_n)`
|
||||
case "name + value":
|
||||
return this.getLatexString()
|
||||
case "null":
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
draw(canvas) {
|
||||
Function.drawFunction(canvas, this.sequence, canvas.logscalex ? Domain.NE : Domain.N, Domain.R, this.drawPoints, this.drawDashedLines)
|
||||
|
||||
// Label
|
||||
this.drawLabel(canvas, this.labelPosition, canvas.x2px(this.labelX), canvas.y2px(this.execute(this.labelX)))
|
||||
}
|
||||
}
|
||||
|
108
common/src/objs/text.mjs
Normal file
108
common/src/objs/text.mjs
Normal file
|
@ -0,0 +1,108 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Expression } from "../math/index.mjs"
|
||||
import * as P from "../parameters.mjs"
|
||||
import Objects from "../module/objects.mjs"
|
||||
import Latex from "../module/latex.mjs"
|
||||
|
||||
import { DrawableObject } from "./common.mjs"
|
||||
|
||||
|
||||
export default class Text extends DrawableObject {
|
||||
static type() {
|
||||
return "Text"
|
||||
}
|
||||
|
||||
static displayType() {
|
||||
return qsTranslate("text", "Text")
|
||||
}
|
||||
|
||||
static displayTypeMultiple() {
|
||||
return qsTranslate("text", "Texts")
|
||||
}
|
||||
|
||||
static properties() {
|
||||
return {
|
||||
[QT_TRANSLATE_NOOP("prop", "x")]: new P.Expression(),
|
||||
[QT_TRANSLATE_NOOP("prop", "y")]: new P.Expression(),
|
||||
[QT_TRANSLATE_NOOP("prop", "labelPosition")]: P.Enum.Positioning,
|
||||
[QT_TRANSLATE_NOOP("prop", "text")]: "string",
|
||||
"comment1": QT_TRANSLATE_NOOP(
|
||||
"comment",
|
||||
"If you have latex enabled, you can use use latex markup in between $$ to create equations."
|
||||
),
|
||||
[QT_TRANSLATE_NOOP("prop", "disableLatex")]: "boolean"
|
||||
}
|
||||
}
|
||||
|
||||
constructor(name = null, visible = true, color = null, labelContent = "null",
|
||||
x = 1, y = 0, labelPosition = "center", text = "New text", disableLatex = false) {
|
||||
if(name == null) name = Objects.getNewName("t")
|
||||
super(name, visible, color, labelContent)
|
||||
if(typeof x == "number" || typeof x == "string") x = new Expression(x.toString())
|
||||
this.x = x
|
||||
if(typeof y == "number" || typeof y == "string") y = new Expression(y.toString())
|
||||
this.y = y
|
||||
this.labelPosition = labelPosition
|
||||
this.text = text
|
||||
this.disableLatex = disableLatex
|
||||
}
|
||||
|
||||
getReadableString() {
|
||||
return `${this.name} = "${this.text}"`
|
||||
}
|
||||
|
||||
latexMarkupText() {
|
||||
// Check whether the text contains latex escaped elements.
|
||||
let txt = []
|
||||
this.text.split("$$").forEach(function(t) {
|
||||
txt = txt.concat(Latex.variable(t, true).replace(/\$\$/g, "").split("$"))
|
||||
})
|
||||
let newTxt = txt[0]
|
||||
let i
|
||||
// Split between normal text and latex escaped.
|
||||
for(i = 0; i < txt.length - 1; i++)
|
||||
if(i & 0x01) // Every odd number
|
||||
newTxt += "\\textsf{" + Latex.variable(txt[i + 1])
|
||||
else
|
||||
newTxt += "}" + txt[i + 1]
|
||||
// Finished by a }
|
||||
if(i & 0x01)
|
||||
newTxt += "{"
|
||||
return newTxt
|
||||
}
|
||||
|
||||
getLatexString() {
|
||||
return `${Latex.variable(this.name)} = "\\textsf{${this.latexMarkupText()}}"`
|
||||
}
|
||||
|
||||
getLabel() {
|
||||
return this.text
|
||||
}
|
||||
|
||||
getLatexLabel() {
|
||||
return `\\textsf{${this.latexMarkupText()}}`
|
||||
}
|
||||
|
||||
draw(canvas) {
|
||||
let yOffset = this.disableLatex ? canvas.textsize - 4 : 0
|
||||
this.drawLabel(canvas, this.labelPosition, canvas.x2px(this.x.execute()), canvas.y2px(this.y.execute()) + yOffset, this.disableLatex)
|
||||
}
|
||||
}
|
||||
|
167
common/src/objs/xcursor.mjs
Normal file
167
common/src/objs/xcursor.mjs
Normal file
|
@ -0,0 +1,167 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Expression } from "../math/index.mjs"
|
||||
import * as P from "../parameters.mjs"
|
||||
import Latex from "../module/latex.mjs"
|
||||
import Objects from "../module/objects.mjs"
|
||||
|
||||
import { DrawableObject } from "./common.mjs"
|
||||
|
||||
|
||||
export default class XCursor extends DrawableObject {
|
||||
static type(){return 'X Cursor'}
|
||||
static displayType(){return qsTranslate("xcursor", 'X Cursor')}
|
||||
static displayTypeMultiple(){return qsTranslate("xcursor", 'X Cursors')}
|
||||
static properties() {return {
|
||||
[QT_TRANSLATE_NOOP('prop','x')]: new P.Expression(),
|
||||
[QT_TRANSLATE_NOOP('prop','targetElement')]: new P.ObjectType('ExecutableObject', true),
|
||||
[QT_TRANSLATE_NOOP('prop','labelPosition')]: P.Enum.Position,
|
||||
[QT_TRANSLATE_NOOP('prop','approximate')]: 'boolean',
|
||||
[QT_TRANSLATE_NOOP('prop','rounding')]: 'int',
|
||||
[QT_TRANSLATE_NOOP('prop','displayStyle')]: new P.Enum(
|
||||
'— — — — — — —',
|
||||
'⸺⸺⸺⸺⸺⸺',
|
||||
'• • • • • • • • • •'
|
||||
),
|
||||
[QT_TRANSLATE_NOOP('prop','targetValuePosition')]: P.Enum.XCursorValuePosition,
|
||||
}}
|
||||
|
||||
constructor(name = null, visible = true, color = null, labelContent = 'name + value',
|
||||
x = 1, targetElement = null, labelPosition = 'left', approximate = true,
|
||||
rounding = 3, displayStyle = '— — — — — — —', targetValuePosition = 'Next to target') {
|
||||
if(name == null) name = Objects.getNewName('X')
|
||||
super(name, visible, color, labelContent)
|
||||
this.approximate = approximate
|
||||
this.rounding = rounding
|
||||
if(typeof x == 'number' || typeof x == 'string') x = new Expression(x.toString())
|
||||
this.x = x
|
||||
this.targetElement = targetElement
|
||||
if(typeof targetElement == "string") {
|
||||
this.targetElement = Objects.currentObjectsByName[targetElement]
|
||||
}
|
||||
this.labelPosition = labelPosition
|
||||
this.displayStyle = displayStyle
|
||||
this.targetValuePosition = targetValuePosition
|
||||
}
|
||||
|
||||
getReadableString() {
|
||||
if(this.targetElement == null) return `${this.name} = ${this.x.toString()}`
|
||||
return `${this.name} = ${this.x.toString()}\n${this.getTargetValueLabel()}`
|
||||
}
|
||||
|
||||
getLatexString() {
|
||||
if(this.targetElement == null) return `${Latex.variable(this.name)} = ${this.x.latexMarkup}`
|
||||
return `\\begin{array}{l}
|
||||
${Latex.variable(this.name)} = ${this.x.latexMarkup} \\\\
|
||||
${this.getTargetValueLatexLabel()}
|
||||
\\end{array}`
|
||||
}
|
||||
|
||||
getApprox() {
|
||||
let approx = ''
|
||||
if(this.approximate) {
|
||||
approx = (this.targetElement.execute(this.x.execute()))
|
||||
let intLength = Math.round(approx).toString().length
|
||||
let rounding = Math.min(this.rounding, approx.toString().length - intLength - 1)
|
||||
approx = approx.toPrecision(rounding + intLength)
|
||||
}
|
||||
return approx
|
||||
}
|
||||
|
||||
getTargetValueLabel() {
|
||||
const t = this.targetElement
|
||||
const approx = this.getApprox()
|
||||
return `${t.name}(${this.name}) = ${t.simplify(this.x.toEditableString())}` +
|
||||
(this.approximate ? ' ≈ ' + approx : '')
|
||||
}
|
||||
|
||||
getTargetValueLatexLabel() {
|
||||
const t = this.targetElement
|
||||
const approx = this.getApprox()
|
||||
const simpl = t.simplify(this.x.toEditableString())
|
||||
return `${Latex.variable(t.name)}(${Latex.variable(this.name)}) = ${simpl.latexMarkup ? simpl.latexMarkup : Latex.variable(simpl)}` +
|
||||
(this.approximate ? ' \\simeq ' + approx : '')
|
||||
}
|
||||
|
||||
getLabel() {
|
||||
switch(this.labelContent) {
|
||||
case 'name':
|
||||
return this.name
|
||||
case 'name + value':
|
||||
switch(this.targetValuePosition) {
|
||||
case 'Next to target':
|
||||
case 'Hidden':
|
||||
return `${this.name} = ${this.x.toString()}`
|
||||
case 'With label':
|
||||
return this.getReadableString()
|
||||
}
|
||||
case 'null':
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
getLatexLabel() {
|
||||
switch(this.labelContent) {
|
||||
case 'name':
|
||||
return Latex.variable(this.name)
|
||||
case 'name + value':
|
||||
switch(this.targetValuePosition) {
|
||||
case 'Next to target':
|
||||
case 'Hidden':
|
||||
return `${Latex.variable(this.name)} = ${this.x.latexMarkup}`
|
||||
case 'With label':
|
||||
return this.getLatexString()
|
||||
}
|
||||
case 'null':
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
draw(canvas) {
|
||||
let xpos = canvas.x2px(this.x.execute())
|
||||
switch(this.displayStyle) {
|
||||
case '— — — — — — —':
|
||||
canvas.drawDashedLine(xpos, 0, xpos, canvas.height, 20)
|
||||
break;
|
||||
case '⸺⸺⸺⸺⸺⸺':
|
||||
canvas.drawXLine(this.x.execute())
|
||||
break;
|
||||
case '• • • • • • • • • •':
|
||||
let pointDistancePx = 10
|
||||
let pointSize = 2
|
||||
for(let i = 0; i < canvas.height; i += pointDistancePx)
|
||||
canvas.disc(xpos, i, pointSize)
|
||||
break;
|
||||
}
|
||||
|
||||
// Drawing label at the top of the canvas.
|
||||
this.drawLabel(canvas, this.labelPosition, xpos, 0, false, null, null,
|
||||
(x,y,ltxImg) => canvas.drawVisibleImage(ltxImg.source, x, 5, ltxImg.width, ltxImg.height),
|
||||
(x,y,text,textSize) => canvas.drawVisibleText(text, x, textSize.height+5))
|
||||
|
||||
// Drawing label at the position of the target element.
|
||||
if(this.targetValuePosition === 'Next to target' && this.targetElement != null) {
|
||||
let ypos = canvas.y2px(this.targetElement.execute(this.x.execute()))
|
||||
this.drawLabel(canvas, this.labelPosition, xpos, ypos, false,
|
||||
this.getTargetValueLatexLabel.bind(this), this.getTargetValueLabel.bind(this),
|
||||
(x,y,ltxImg) => canvas.drawVisibleImage(ltxImg.source, x, y, ltxImg.width, ltxImg.height),
|
||||
(x,y,text,textSize) => canvas.drawVisibleText(text, x, y+textSize.height))
|
||||
}
|
||||
}
|
||||
}
|
380
common/src/parameters.mjs
Normal file
380
common/src/parameters.mjs
Normal file
|
@ -0,0 +1,380 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { parseDomain, Expression as Expr, Domain } from "./math/index.mjs"
|
||||
import Objects from "./module/objects.mjs"
|
||||
|
||||
const NONE = class Empty {
|
||||
}
|
||||
|
||||
let stringValuesValidators = {
|
||||
"int": [parseInt, (x) => !isNaN(x)],
|
||||
"double": [parseFloat, (x) => !isNaN(x)],
|
||||
"string": [(x) => x, () => true]
|
||||
}
|
||||
let stringValidatorTypes = Object.keys(stringValuesValidators)
|
||||
|
||||
|
||||
class PropertyType {
|
||||
/**
|
||||
* Validates if a value corresponds to the current property type, and if so, returns it.
|
||||
* @param value
|
||||
* @returns {null|object}
|
||||
*/
|
||||
parse(value) {
|
||||
throw new TypeError(`validate function of ${typeof this} has not been implemented.`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports the value of this property type.
|
||||
* @param value
|
||||
* @returns {string|number|bool|object}
|
||||
*/
|
||||
export(value) {
|
||||
throw new TypeError(`export function of ${typeof this} has not been implemented.`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class Expression extends PropertyType {
|
||||
constructor(...variables) {
|
||||
super()
|
||||
this.type = "Expression"
|
||||
this.variables = variables
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.variables.length === 0 ? "Number" : `Expression(${this.variables.join(", ")})`
|
||||
}
|
||||
|
||||
parse(value) {
|
||||
let result = NONE
|
||||
if(typeof value == "string")
|
||||
try {
|
||||
result = new Expr(value)
|
||||
} catch(e) {
|
||||
// Silently error and return null
|
||||
console.trace()
|
||||
console.log(`Error parsing expression ${value}:`)
|
||||
console.error(e)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export(value) {
|
||||
if(value instanceof Expr)
|
||||
return value.toEditableString()
|
||||
else
|
||||
throw new TypeError(`Exportation error: ${value} is not an expression.`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class Enum extends PropertyType {
|
||||
constructor(...values) {
|
||||
super()
|
||||
this.type = "Enum"
|
||||
this.values = values
|
||||
this.legacyValues = {}
|
||||
this.translatedValues = values.map(x => qsTranslate("parameters", x))
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `${this.type}(${this.values.join(", ")})`
|
||||
}
|
||||
|
||||
parse(value) {
|
||||
let result = NONE
|
||||
if(this.values.includes(value))
|
||||
result = value
|
||||
else if(this.legacyValues[value])
|
||||
result = this.legacyValues[value]
|
||||
return result
|
||||
}
|
||||
|
||||
export(value) {
|
||||
if(this.values.includes(value))
|
||||
return value
|
||||
else if(this.legacyValues[value])
|
||||
return this.legacyValues[value]
|
||||
else
|
||||
throw new TypeError(`Exportation error: ${value} not one of ${this.values.join(", ")}.`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class ObjectType extends PropertyType {
|
||||
constructor(objType, allowNull = false) {
|
||||
super()
|
||||
this.type = "ObjectType"
|
||||
this.objType = objType
|
||||
this.allowNull = allowNull
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.objType
|
||||
}
|
||||
|
||||
parse(name) {
|
||||
let result = NONE
|
||||
if(typeof name == "string" && name in Objects.currentObjectsByName) {
|
||||
let obj = Objects.currentObjectsByName[name]
|
||||
if(obj.type === this.objType || (this.objType === "ExecutableObject" && obj.execute)) {
|
||||
result = obj
|
||||
} else {
|
||||
// Silently error and return null
|
||||
console.trace()
|
||||
console.error(new TypeError(`Object ${name} is of not of type ${this.objType}:`))
|
||||
}
|
||||
} else if(this.allowNull && (name == null || name === "null"))
|
||||
result = null
|
||||
return result
|
||||
}
|
||||
|
||||
export(value) {
|
||||
if(value == null && this.allowNull)
|
||||
return null
|
||||
else if(value.type === this.objType || (this.objType === "ExecutableObject" && value.execute))
|
||||
return value.name
|
||||
else
|
||||
throw new TypeError(`Exportation error: ${value} not a ${this.objType}.`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class List extends PropertyType {
|
||||
constructor(type, format = /^.+$/, label = "", forbidAdding = false) {
|
||||
super()
|
||||
// type can be string, int and double.
|
||||
this.type = "List"
|
||||
this.valueType = type
|
||||
if(!stringValidatorTypes.includes(this.valueType))
|
||||
throw new TypeError(`${this.valueType} must be one of ${stringValidatorTypes.join(", ")}.`)
|
||||
this.format = format
|
||||
this.label = label
|
||||
this.forbidAdding = forbidAdding
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `${this.type}(${this.valueType}:${this.format})`
|
||||
}
|
||||
|
||||
parse(value) {
|
||||
let result = NONE
|
||||
if(typeof value == "object" && value.__proto__ === Array) {
|
||||
let valid = 0
|
||||
for(let v of value) {
|
||||
if(this.format.test(v)) {
|
||||
v = stringValuesValidators[this.valueType][0](v)
|
||||
if(stringValuesValidators[this.valueType][1](v))
|
||||
valid++
|
||||
}
|
||||
}
|
||||
if(valid === value.length) // Ensure every value is valid.
|
||||
result = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export(value) {
|
||||
if(typeof value == "object" && value.__proto__ === Array)
|
||||
return value
|
||||
else
|
||||
throw new TypeError(`Exportation error: ${value} not a list.`)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class Dictionary extends PropertyType {
|
||||
constructor(type, keyType = "string", format = /^.+$/, keyFormat = /^.+$/, preKeyLabel = "", postKeyLabel = ": ", forbidAdding = false) {
|
||||
super()
|
||||
// type & keyType can be string, int and double.
|
||||
this.type = "Dict"
|
||||
this.valueType = type
|
||||
this.keyType = keyType
|
||||
this.format = format
|
||||
this.keyFormat = keyFormat
|
||||
this.preKeyLabel = preKeyLabel
|
||||
this.postKeyLabel = postKeyLabel
|
||||
this.forbidAdding = forbidAdding
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `${this.type}(${this.keyType}:${this.keyFormat}: ${this.valueType}:${this.format})`
|
||||
}
|
||||
|
||||
parse(value) {
|
||||
let result = NONE
|
||||
if(typeof value == "object" && value.__proto__ !== Array) {
|
||||
let dict = []
|
||||
for(let [k, v] of Object.entries(value)) {
|
||||
if(this.format.test(v) && this.keyFormat.test(k)) {
|
||||
k = stringValuesValidators[this.keyType][0](k)
|
||||
v = stringValuesValidators[this.valueType][0](v)
|
||||
if(stringValuesValidators[this.keyType][1](k))
|
||||
if(stringValuesValidators[this.valueType][1](v))
|
||||
dict[k] = v
|
||||
}
|
||||
}
|
||||
if(Object.keys(dict).length === Object.keys(value).length)
|
||||
result = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export(value) {
|
||||
if(typeof value == "object" && value.__proto__ !== Array)
|
||||
return value
|
||||
else
|
||||
throw new TypeError(`Exportation error: ${value} not a dictionary.`)
|
||||
}
|
||||
}
|
||||
|
||||
// Common parameters for Enums
|
||||
|
||||
Enum.Position = new Enum(
|
||||
QT_TRANSLATE_NOOP("parameters", "above"),
|
||||
QT_TRANSLATE_NOOP("parameters", "below"),
|
||||
QT_TRANSLATE_NOOP("parameters", "left"),
|
||||
QT_TRANSLATE_NOOP("parameters", "right"),
|
||||
QT_TRANSLATE_NOOP("parameters", "above-left"),
|
||||
QT_TRANSLATE_NOOP("parameters", "above-right"),
|
||||
QT_TRANSLATE_NOOP("parameters", "below-left"),
|
||||
QT_TRANSLATE_NOOP("parameters", "below-right")
|
||||
)
|
||||
Enum.Position.legacyValues = {
|
||||
"top": "above",
|
||||
"bottom": "below",
|
||||
"top-left": "above-left",
|
||||
"top-right": "above-right",
|
||||
"bottom-left": "below-left",
|
||||
"bottom-right": "below-right"
|
||||
}
|
||||
|
||||
Enum.Positioning = new Enum(
|
||||
QT_TRANSLATE_NOOP("parameters", "center"),
|
||||
QT_TRANSLATE_NOOP("parameters", "top"),
|
||||
QT_TRANSLATE_NOOP("parameters", "bottom"),
|
||||
QT_TRANSLATE_NOOP("parameters", "left"),
|
||||
QT_TRANSLATE_NOOP("parameters", "right"),
|
||||
QT_TRANSLATE_NOOP("parameters", "top-left"),
|
||||
QT_TRANSLATE_NOOP("parameters", "top-right"),
|
||||
QT_TRANSLATE_NOOP("parameters", "bottom-left"),
|
||||
QT_TRANSLATE_NOOP("parameters", "bottom-right")
|
||||
)
|
||||
|
||||
Enum.FunctionDisplayType = new Enum(
|
||||
QT_TRANSLATE_NOOP("parameters", "application"),
|
||||
QT_TRANSLATE_NOOP("parameters", "function")
|
||||
)
|
||||
|
||||
Enum.BodePass = new Enum(
|
||||
QT_TRANSLATE_NOOP("parameters", "high"),
|
||||
QT_TRANSLATE_NOOP("parameters", "low")
|
||||
)
|
||||
|
||||
|
||||
Enum.XCursorValuePosition = new Enum(
|
||||
QT_TRANSLATE_NOOP("parameters", "Next to target"),
|
||||
QT_TRANSLATE_NOOP("parameters", "With label"),
|
||||
QT_TRANSLATE_NOOP("parameters", "Hidden")
|
||||
)
|
||||
|
||||
/**
|
||||
* Ensures whether a provided value is of the corresponding type.
|
||||
* @param {string|PropertyType} propertyType
|
||||
* @param {string|number|boolean|array|object}value
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function ensureTypeSafety(propertyType, value) {
|
||||
let result
|
||||
let error = false
|
||||
if(typeof propertyType == "string")
|
||||
switch(propertyType) {
|
||||
case "string":
|
||||
result = value
|
||||
error = typeof value !== "string"
|
||||
break
|
||||
case "number":
|
||||
result = parseFloat(value)
|
||||
error = isNaN(result)
|
||||
break
|
||||
case "boolean":
|
||||
result = value
|
||||
error = value !== true && value !== false
|
||||
break
|
||||
case "Domain":
|
||||
try {
|
||||
error = typeof value !== "string"
|
||||
if(!error)
|
||||
result = parseDomain(value)
|
||||
} catch(e) {
|
||||
// Parse domain sometimes returns an empty set when it cannot parse a domain.
|
||||
// It's okay though, it shouldn't be user parsed value, so returning an empty set
|
||||
// is okay for a corrupted file, rather than erroring.
|
||||
console.trace()
|
||||
console.log(`Error parsing domain ${value}:`)
|
||||
console.error(e)
|
||||
error = true
|
||||
}
|
||||
break
|
||||
}
|
||||
else if(propertyType instanceof PropertyType) {
|
||||
result = propertyType.parse(value)
|
||||
error = result === NONE
|
||||
} else
|
||||
throw new TypeError(`Importation error: Unknown property type ${propertyType}.`)
|
||||
if(error) {
|
||||
console.trace()
|
||||
throw new TypeError(`Importation error: Couldn't parse ${JSON.stringify(value)} as ${propertyType}.`)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes a property by its type to export into JSON.
|
||||
* @param {string|PropertyType} propertyType
|
||||
* @param value
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function serializesByPropertyType(propertyType, value) {
|
||||
let result
|
||||
if(typeof propertyType == "string")
|
||||
switch(propertyType) {
|
||||
case "string":
|
||||
result = value.toString()
|
||||
break
|
||||
case "number":
|
||||
result = parseFloat(value)
|
||||
break
|
||||
case "boolean":
|
||||
result = value === true
|
||||
break
|
||||
case "Domain":
|
||||
if(value instanceof Domain)
|
||||
result = value.toString()
|
||||
else
|
||||
throw new TypeError(`Exportation error: ${value} is not a domain.`)
|
||||
break
|
||||
}
|
||||
else if(propertyType instanceof PropertyType) {
|
||||
result = propertyType.export(value)
|
||||
} else
|
||||
throw new TypeError(`Exportation error: Unknown property type ${propertyType}.`)
|
||||
return result
|
||||
}
|
5
common/src/parsing/README.md
Normal file
5
common/src/parsing/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# General information
|
||||
|
||||
Here lies the potential new, abandoned, cleaner implementation of the parsing system that was supposed to replace expr-eval.js, but never came to be because it's unfinished. If somebody has the will to finish this, you're welcome to try, as I won't.
|
||||
|
||||
Currently, the tokenizer is complete in use to provide tokens for the syntax highlighting, and the reference to provide usage.
|
50
common/src/parsing/common.mjs
Normal file
50
common/src/parsing/common.mjs
Normal file
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
export default class InputExpression {
|
||||
constructor(expression) {
|
||||
this.position = 0
|
||||
this.input = expression
|
||||
}
|
||||
|
||||
next() {
|
||||
return this.input[this.position++]
|
||||
}
|
||||
|
||||
peek() {
|
||||
return this.input[this.position]
|
||||
}
|
||||
|
||||
skip(char) {
|
||||
if(!this.atEnd() && this.peek() === char) {
|
||||
this.position++
|
||||
} else {
|
||||
this.raise("Unexpected character " + this.peek() + ". Expected character " + char)
|
||||
}
|
||||
}
|
||||
|
||||
atEnd() {
|
||||
return this.position >= this.input.length
|
||||
}
|
||||
|
||||
raise(message) {
|
||||
throw new SyntaxError(message + " at " + this.position + ".")
|
||||
}
|
||||
}
|
||||
|
32
common/src/parsing/index.mjs
Normal file
32
common/src/parsing/index.mjs
Normal file
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as Reference from "./reference.mjs"
|
||||
import * as T from "./tokenizer.mjs"
|
||||
import InputExpression from "./common.mjs"
|
||||
|
||||
export const Input = InputExpression
|
||||
export const TokenType = T.TokenType
|
||||
export const Token = T.Token
|
||||
export const Tokenizer = T.ExpressionTokenizer
|
||||
|
||||
export const FUNCTIONS_LIST = Reference.FUNCTIONS_LIST
|
||||
export const FUNCTIONS = Reference.FUNCTIONS
|
||||
export const FUNCTIONS_USAGE = Reference.FUNCTIONS_USAGE
|
||||
export const CONSTANTS_LIST = Reference.CONSTANTS_LIST
|
||||
export const CONSTANTS = Reference.CONSTANTS
|
174
common/src/parsing/reference.mjs
Normal file
174
common/src/parsing/reference.mjs
Normal file
|
@ -0,0 +1,174 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as Polyfill from "../lib/expr-eval/polyfill.mjs"
|
||||
|
||||
export const CONSTANTS = {
|
||||
"π": Math.PI,
|
||||
"pi": Math.PI,
|
||||
"inf": Infinity,
|
||||
"infinity": Infinity,
|
||||
"∞": Infinity,
|
||||
"e": Math.E
|
||||
}
|
||||
export const CONSTANTS_LIST = Object.keys(CONSTANTS)
|
||||
|
||||
export const FUNCTIONS = {
|
||||
// The functions commented are the one either not implemented
|
||||
// in the parser, or not to be used for autocompletion.
|
||||
// Unary operators
|
||||
//'+': Number,
|
||||
//'-': (x) => -x,
|
||||
//'!'
|
||||
// Other operations
|
||||
"length": (s) => Array.isArray(s) ? s.length : String(s).length,
|
||||
// Boolean functions
|
||||
"not": (x) => !x,
|
||||
// Math functions
|
||||
"abs": Math.abs,
|
||||
"acos": Math.acos,
|
||||
"acosh": Math.acosh,
|
||||
"asin": Math.asin,
|
||||
"asinh": Math.asinh,
|
||||
"atan": Math.atan,
|
||||
"atan2": Math.atan2,
|
||||
"atanh": Math.atanh,
|
||||
"cbrt": Math.cbrt,
|
||||
"ceil": Math.ceil,
|
||||
//'clz32': Math.clz32,
|
||||
"cos": Math.cos,
|
||||
"cosh": Math.cosh,
|
||||
"exp": Math.exp,
|
||||
"expm1": Math.expm1,
|
||||
"floor": Math.floor,
|
||||
//'fround': Math.fround,
|
||||
"hypot": Math.hypot,
|
||||
//'imul': Math.imul,
|
||||
"lg": Math.log10,
|
||||
"ln": Math.log,
|
||||
"log": Math.log,
|
||||
"log10": Math.log10,
|
||||
"log1p": Math.log1p,
|
||||
"log2": Math.log2,
|
||||
"max": Math.max,
|
||||
"min": Math.min,
|
||||
"pow": Math.log2,
|
||||
"random": Math.random,
|
||||
"round": Math.round,
|
||||
"sign": Math.sign,
|
||||
"sin": Math.sin,
|
||||
"sinh": Math.sinh,
|
||||
"sqrt": Math.sqrt,
|
||||
"tan": Math.tan,
|
||||
"tanh": Math.tanh,
|
||||
"trunc": Math.trunc,
|
||||
// Functions in expr-eval, ported here.
|
||||
"fac": Polyfill.factorial,
|
||||
"gamma": Polyfill.gamma,
|
||||
"Γ": Polyfill.gamma,
|
||||
"roundTo": (x, exp) => Number(x).toFixed(exp),
|
||||
// 'map': Polyfill.arrayMap,
|
||||
// 'fold': Polyfill.arrayFold,
|
||||
// 'filter': Polyfill.arrayFilter,
|
||||
// 'indexOf': Polyfill.indexOf,
|
||||
// 'join': Polyfill.arrayJoin,
|
||||
// Integral & derivative (only here for autocomplete).
|
||||
"integral": () => 0, // TODO: Implement
|
||||
"derivative": () => 0
|
||||
}
|
||||
export const FUNCTIONS_LIST = Object.keys(FUNCTIONS)
|
||||
|
||||
export class P {
|
||||
// Parameter class.
|
||||
constructor(type, name = "", optional = false, multipleAllowed = false) {
|
||||
this.name = name
|
||||
this.type = type
|
||||
this.optional = optional
|
||||
this.multipleAllowed = multipleAllowed
|
||||
}
|
||||
|
||||
toString() {
|
||||
let base_string = this.type
|
||||
if(this.name !== "")
|
||||
base_string = `${this.name}: ${base_string}`
|
||||
if(this.multipleAllowed)
|
||||
base_string += "..."
|
||||
if(!this.optional)
|
||||
base_string = `<${base_string}>`
|
||||
else
|
||||
base_string = `[${base_string}]`
|
||||
return base_string
|
||||
}
|
||||
}
|
||||
|
||||
export let string = new P("string")
|
||||
export let bool = new P("bool")
|
||||
export let number = new P("number")
|
||||
export let array = new P("array")
|
||||
|
||||
export const FUNCTIONS_USAGE = {
|
||||
"length": [string],
|
||||
"not": [bool],
|
||||
// Math functions
|
||||
"abs": [number],
|
||||
"acos": [number],
|
||||
"acosh": [number],
|
||||
"asin": [number],
|
||||
"asinh": [number],
|
||||
"atan": [number],
|
||||
"atan2": [number],
|
||||
"atanh": [number],
|
||||
"cbrt": [number],
|
||||
"ceil": [number],
|
||||
//'clz32': [number],
|
||||
"cos": [number],
|
||||
"cosh": [number],
|
||||
"exp": [number],
|
||||
"expm1": [number],
|
||||
"floor": [number],
|
||||
//'fround': [number],
|
||||
"hypot": [number],
|
||||
//'imul': [number],
|
||||
"lg": [number],
|
||||
"ln": [number],
|
||||
"log": [number],
|
||||
"log10": [number],
|
||||
"log1p": [number],
|
||||
"log2": [number],
|
||||
"max": [number, number, new P("numbers", "", true, true)],
|
||||
"min": [number, number, new P("numbers", "", true, true)],
|
||||
"pow": [number, new P("number", "exp")],
|
||||
"random": [number, number],
|
||||
"round": [number],
|
||||
"sign": [number],
|
||||
"sin": [number],
|
||||
"sinh": [number],
|
||||
"sqrt": [number],
|
||||
"tan": [number],
|
||||
"tanh": [number],
|
||||
"trunc": [number],
|
||||
// Functions in expr-eval, ported here.
|
||||
"fac": [number],
|
||||
"gamma": [number],
|
||||
"Γ": [number],
|
||||
"roundTo": [number, new P("number")],
|
||||
// Function manipulation
|
||||
"derivative": [new P("f"), new P("string", "var", true), number],
|
||||
"integral": [new P("from"), new P("to"), new P("f"), new P("string", "var", true)]
|
||||
}
|
||||
|
174
common/src/parsing/tokenizer.mjs
Normal file
174
common/src/parsing/tokenizer.mjs
Normal file
|
@ -0,0 +1,174 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
import * as Reference from "./reference.mjs"
|
||||
|
||||
const WHITESPACES = " \t\n\r"
|
||||
const STRING_LIMITERS = "\"'`"
|
||||
const OPERATORS = "+-*/^%?:=!><"
|
||||
const PUNCTUATION = "()[]{},."
|
||||
const NUMBER_CHARS = "0123456789"
|
||||
const IDENTIFIER_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789_₀₁₂₃₄₅₆₇₈₉αβγδεζηθκλμξρςστφχψωₐₑₒₓₔₕₖₗₘₙₚₛₜ"
|
||||
|
||||
export const TokenType = {
|
||||
// Expression type
|
||||
"WHITESPACE": "WHITESPACE",
|
||||
"VARIABLE": "VARIABLE",
|
||||
"CONSTANT": "CONSTANT",
|
||||
"FUNCTION": "FUNCTION",
|
||||
"OPERATOR": "OPERATOR",
|
||||
"PUNCT": "PUNCT",
|
||||
"NUMBER": "NUMBER",
|
||||
"STRING": "STRING",
|
||||
"UNKNOWN": "UNKNOWN"
|
||||
}
|
||||
|
||||
export class Token {
|
||||
constructor(type, value, startPosition) {
|
||||
this.type = type
|
||||
this.value = value
|
||||
this.startPosition = startPosition
|
||||
}
|
||||
}
|
||||
|
||||
export class ExpressionTokenizer {
|
||||
/**
|
||||
*
|
||||
* @param {InputExpression} input
|
||||
* @param {boolean} tokenizeWhitespaces
|
||||
* @param {boolean} errorOnUnknown
|
||||
*/
|
||||
constructor(input, tokenizeWhitespaces = false, errorOnUnknown = true) {
|
||||
this.input = input
|
||||
this.currentToken = null
|
||||
this.tokenizeWhitespaces = tokenizeWhitespaces
|
||||
this.errorOnUnknown = errorOnUnknown
|
||||
}
|
||||
|
||||
skipWhitespaces() {
|
||||
while(!this.input.atEnd() && WHITESPACES.includes(this.input.peek()))
|
||||
this.input.next()
|
||||
}
|
||||
|
||||
readWhitespaces() {
|
||||
let included = ""
|
||||
while(!this.input.atEnd() && WHITESPACES.includes(this.input.peek())) {
|
||||
included += this.input.next()
|
||||
}
|
||||
return new Token(TokenType.WHITESPACE, included, this.input.position - included.length)
|
||||
}
|
||||
|
||||
readString() {
|
||||
let delimitation = this.input.peek()
|
||||
if(STRING_LIMITERS.includes(delimitation)) {
|
||||
this.input.skip(delimitation)
|
||||
let included = ""
|
||||
let justEscaped = false
|
||||
while(!this.input.atEnd() && (!STRING_LIMITERS.includes(this.input.peek()) || justEscaped)) {
|
||||
justEscaped = this.input.peek() === "\\"
|
||||
if(!justEscaped)
|
||||
included += this.input.next()
|
||||
}
|
||||
this.input.skip(delimitation)
|
||||
let token = new Token(TokenType.STRING, included, this.input.position - included.length)
|
||||
token.limitator = delimitation
|
||||
return token
|
||||
} else {
|
||||
this.input.raise("Unexpected " + delimitation + ". Expected string delimitator")
|
||||
}
|
||||
}
|
||||
|
||||
readNumber() {
|
||||
let included = ""
|
||||
let hasDot = false
|
||||
while(!this.input.atEnd() && (NUMBER_CHARS.includes(this.input.peek()) || this.input.peek() === ".")) {
|
||||
if(this.input.peek() === ".") {
|
||||
if(hasDot) this.input.raise("Unexpected '.'. Expected digit")
|
||||
hasDot = true
|
||||
}
|
||||
included += this.input.next()
|
||||
}
|
||||
return new Token(TokenType.NUMBER, included, this.input.position - included.length)
|
||||
}
|
||||
|
||||
readOperator() {
|
||||
let included = ""
|
||||
while(!this.input.atEnd() && OPERATORS.includes(this.input.peek())) {
|
||||
included += this.input.next()
|
||||
}
|
||||
return new Token(TokenType.OPERATOR, included, this.input.position - included.length)
|
||||
}
|
||||
|
||||
readIdentifier() {
|
||||
let identifier = ""
|
||||
while(!this.input.atEnd() && IDENTIFIER_CHARS.includes(this.input.peek().toLowerCase())) {
|
||||
identifier += this.input.next()
|
||||
}
|
||||
if(Reference.CONSTANTS_LIST.includes(identifier.toLowerCase())) {
|
||||
return new Token(TokenType.CONSTANT, identifier.toLowerCase(), this.input.position - identifier.length)
|
||||
} else if(Reference.FUNCTIONS_LIST.includes(identifier.toLowerCase())) {
|
||||
return new Token(TokenType.FUNCTION, identifier.toLowerCase(), this.input.position - identifier.length)
|
||||
} else {
|
||||
return new Token(TokenType.VARIABLE, identifier, this.input.position - identifier.length)
|
||||
}
|
||||
}
|
||||
|
||||
readNextToken() {
|
||||
if(!this.tokenizeWhitespaces)
|
||||
this.skipWhitespaces()
|
||||
if(this.input.atEnd()) return null
|
||||
let c = this.input.peek()
|
||||
if(this.tokenizeWhitespaces && WHITESPACES.includes(c)) return this.readWhitespaces()
|
||||
if(STRING_LIMITERS.includes(c)) return this.readString()
|
||||
if(NUMBER_CHARS.includes(c)) return this.readNumber()
|
||||
if(IDENTIFIER_CHARS.includes(c.toLowerCase())) return this.readIdentifier()
|
||||
if(OPERATORS.includes(c)) return this.readOperator()
|
||||
if(Reference.CONSTANTS_LIST.includes(c)) return new Token(TokenType.CONSTANT, this.input.next(), this.input.position - 1)
|
||||
if(PUNCTUATION.includes(c)) return new Token(TokenType.PUNCT, this.input.next(), this.input.position - 1)
|
||||
if(this.errorOnUnknown)
|
||||
this.input.raise("Unknown token character " + c)
|
||||
else
|
||||
return new Token(TokenType.UNKNOWN, this.input.next(), this.input.position - 1)
|
||||
}
|
||||
|
||||
peek() {
|
||||
if(this.currentToken == null) this.currentToken = this.readNextToken()
|
||||
return this.currentToken
|
||||
}
|
||||
|
||||
next() {
|
||||
let tmp
|
||||
if(this.currentToken == null)
|
||||
tmp = this.readNextToken()
|
||||
else
|
||||
tmp = this.currentToken
|
||||
this.currentToken = null
|
||||
return tmp
|
||||
}
|
||||
|
||||
atEnd() {
|
||||
return this.peek() == null
|
||||
}
|
||||
|
||||
skip(type) {
|
||||
let next = this.next()
|
||||
if(next.type !== type)
|
||||
this.input.raise("Unexpected token " + next.type.toLowerCase() + " \"" + next.value + "\". Expected " + type.toLowerCase())
|
||||
}
|
||||
}
|
136
common/src/preferences/common.mjs
Normal file
136
common/src/preferences/common.mjs
Normal file
|
@ -0,0 +1,136 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Expression } from "../math/index.mjs"
|
||||
|
||||
class Setting {
|
||||
constructor(type, name, nameInConfig, icon) {
|
||||
this.type = type
|
||||
this.name = name
|
||||
this.nameInConfig = nameInConfig
|
||||
this.icon = icon
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the setting.
|
||||
* @returns {string|boolean|number}
|
||||
*/
|
||||
value() {
|
||||
throw new TypeError(`value of ${this.constructor} not implemented.`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of the setting
|
||||
* @param {string|boolean|number|Expression} value
|
||||
*/
|
||||
set(value) {
|
||||
throw new TypeError(`value of ${this.constructor} not implemented.`)
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `Setting<${this.type} ${this.name}>`
|
||||
}
|
||||
}
|
||||
|
||||
export class BoolSetting extends Setting {
|
||||
constructor(name, nameInConfig, icon) {
|
||||
super("bool", name, nameInConfig, icon)
|
||||
}
|
||||
|
||||
value() {
|
||||
return Helper.getSettingBool(this.nameInConfig)
|
||||
}
|
||||
|
||||
set(value) {
|
||||
Helper.setSettingBool(this.nameInConfig, value)
|
||||
}
|
||||
}
|
||||
|
||||
export class NumberSetting extends Setting {
|
||||
constructor(name, nameInConfig, icon, min = -Infinity, max = +Infinity) {
|
||||
super("number", name, nameInConfig, icon)
|
||||
this.min = typeof min == "number" ? () => min : min
|
||||
this.max = typeof max == "number" ? () => max : max
|
||||
}
|
||||
|
||||
value() {
|
||||
return Helper.getSettingInt(this.nameInConfig)
|
||||
}
|
||||
|
||||
set(value) {
|
||||
Helper.setSettingInt(this.nameInConfig, value)
|
||||
}
|
||||
}
|
||||
|
||||
export class EnumIntSetting extends Setting {
|
||||
constructor(name, nameInConfig, icon, values = []) {
|
||||
super("enum", name, nameInConfig, icon)
|
||||
this.values = values
|
||||
}
|
||||
|
||||
value() {
|
||||
return Helper.getSettingInt(this.nameInConfig)
|
||||
}
|
||||
|
||||
set(value) {
|
||||
Helper.setSettingInt(this.nameInConfig, value)
|
||||
}
|
||||
}
|
||||
|
||||
export class ExpressionSetting extends Setting {
|
||||
constructor(name, nameInConfig, icon, variables = []) {
|
||||
super("expression", name, nameInConfig, icon)
|
||||
this.variables = variables
|
||||
}
|
||||
|
||||
value() {
|
||||
return Helper.getSetting(this.nameInConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Expression} value
|
||||
*/
|
||||
set(value) {
|
||||
let vars = value.variables()
|
||||
if(vars.length === this.variables.length && vars.every(x => this.variables.includes(x)))
|
||||
Helper.setSetting(this.nameInConfig, value)
|
||||
else {
|
||||
let undefinedVars = vars.filter(x => !this.variables.includes(x))
|
||||
let allowed = ""
|
||||
if(this.variables.length > 0)
|
||||
allowed = `Allowed variables: ${this.variables.join(", ")}.`
|
||||
throw new TypeError(`Cannot use variable(s) ${undefinedVars.join(", or ")} to define ${this.displayName}. ${allowed}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class StringSetting extends Setting {
|
||||
constructor(name, nameInConfig, icon, defaultValues = []) {
|
||||
super("string", name, nameInConfig, icon)
|
||||
this.defaultValues = defaultValues
|
||||
}
|
||||
|
||||
value() {
|
||||
return Helper.getSetting(this.nameInConfig)
|
||||
}
|
||||
|
||||
set(value) {
|
||||
Helper.setSetting(this.nameInConfig, value)
|
||||
}
|
||||
}
|
120
common/src/preferences/default.mjs
Normal file
120
common/src/preferences/default.mjs
Normal file
|
@ -0,0 +1,120 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { BoolSetting, ExpressionSetting, NumberSetting, StringSetting } from "./common.mjs"
|
||||
|
||||
|
||||
const XZOOM = new NumberSetting(
|
||||
qsTranslate("Settings", "X Zoom"),
|
||||
"default_graph.xzoom",
|
||||
"xzoom",
|
||||
0.1
|
||||
)
|
||||
|
||||
const YZOOM = new NumberSetting(
|
||||
qsTranslate("Settings", "Y Zoom"),
|
||||
"default_graph.xzoom",
|
||||
"yzoom",
|
||||
0.1
|
||||
)
|
||||
|
||||
const XMIN = new NumberSetting(
|
||||
qsTranslate("Settings", "Min X"),
|
||||
"default_graph.xmin",
|
||||
"xmin",
|
||||
() => Helper.getSettingBool("default_graph.logscalex") ? 1e-100 : -Infinity
|
||||
)
|
||||
|
||||
const YMAX = new NumberSetting(
|
||||
qsTranslate("Settings", "Max Y"),
|
||||
"default_graph.ymax",
|
||||
"ymax"
|
||||
)
|
||||
|
||||
const XAXISSTEP = new ExpressionSetting(
|
||||
qsTranslate("Settings", "X Axis Step"),
|
||||
"default_graph.xaxisstep",
|
||||
"xaxisstep"
|
||||
)
|
||||
|
||||
const YAXISSTEP = new ExpressionSetting(
|
||||
qsTranslate("Settings", "Y Axis Step"),
|
||||
"default_graph.yaxisstep",
|
||||
"yaxisstep"
|
||||
)
|
||||
|
||||
const LINE_WIDTH = new NumberSetting(
|
||||
qsTranslate("Settings", "Line width"),
|
||||
"default_graph.linewidth",
|
||||
"linewidth",
|
||||
1
|
||||
)
|
||||
|
||||
const TEXT_SIZE = new NumberSetting(
|
||||
qsTranslate("Settings", "Text size (px)"),
|
||||
"default_graph.textsize",
|
||||
"textsize"
|
||||
)
|
||||
|
||||
const X_LABEL = new StringSetting(
|
||||
qsTranslate("Settings", "X Label"),
|
||||
"default_graph.xlabel",
|
||||
"xlabel",
|
||||
["", "x", "ω (rad/s)"]
|
||||
)
|
||||
|
||||
const Y_LABEL = new StringSetting(
|
||||
qsTranslate("Settings", "Y Label"),
|
||||
"default_graph.ylabel",
|
||||
"xlabel",
|
||||
["", "y", "G (dB)", "φ (°)", "φ (deg)", "φ (rad)"]
|
||||
)
|
||||
|
||||
const LOG_SCALE_X = new BoolSetting(
|
||||
qsTranslate("Settings", "X Log scale"),
|
||||
"default_graph.logscalex",
|
||||
"logscalex"
|
||||
)
|
||||
|
||||
const SHOW_X_GRAD = new BoolSetting(
|
||||
qsTranslate("Settings", "Show X graduation"),
|
||||
"default_graph.showxgrad",
|
||||
"showxgrad"
|
||||
)
|
||||
|
||||
const SHOW_Y_GRAD = new BoolSetting(
|
||||
qsTranslate("Settings", "Show Y graduation"),
|
||||
"default_graph.showygrad",
|
||||
"showygrad"
|
||||
)
|
||||
|
||||
export default [
|
||||
XZOOM,
|
||||
YZOOM,
|
||||
XMIN,
|
||||
YMAX,
|
||||
XAXISSTEP,
|
||||
YAXISSTEP,
|
||||
LINE_WIDTH,
|
||||
TEXT_SIZE,
|
||||
X_LABEL,
|
||||
Y_LABEL,
|
||||
LOG_SCALE_X,
|
||||
SHOW_X_GRAD,
|
||||
SHOW_Y_GRAD
|
||||
]
|
51
common/src/preferences/expression.mjs
Normal file
51
common/src/preferences/expression.mjs
Normal file
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { BoolSetting, EnumIntSetting } from "./common.mjs"
|
||||
|
||||
const AUTOCLOSE_FORMULA = new BoolSetting(
|
||||
qsTranslate("expression", "Automatically close parenthesises and brackets"),
|
||||
"expression_editor.autoclose",
|
||||
"text"
|
||||
)
|
||||
|
||||
const ENABLE_SYNTAX_HIGHLIGHTING = new BoolSetting(
|
||||
qsTranslate("expression", "Enable syntax highlighting"),
|
||||
"expression_editor.colorize",
|
||||
"appearance"
|
||||
)
|
||||
|
||||
const ENABLE_AUTOCOMPLETE = new BoolSetting(
|
||||
qsTranslate("expression", "Enable autocompletion"),
|
||||
"autocompletion.enabled",
|
||||
"label"
|
||||
)
|
||||
|
||||
const PICK_COLOR_SCHEME = new EnumIntSetting(
|
||||
qsTranslate("expression", "Color Scheme"),
|
||||
"expression_editor.color_scheme",
|
||||
"color",
|
||||
["Breeze Light", "Breeze Dark", "Solarized", "Github Light", "Github Dark", "Nord", "Monokai"]
|
||||
)
|
||||
|
||||
export default [
|
||||
AUTOCLOSE_FORMULA,
|
||||
ENABLE_AUTOCOMPLETE,
|
||||
ENABLE_SYNTAX_HIGHLIGHTING,
|
||||
PICK_COLOR_SCHEME
|
||||
]
|
53
common/src/preferences/general.mjs
Normal file
53
common/src/preferences/general.mjs
Normal file
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { BoolSetting } from "./common.mjs"
|
||||
import Canvas from "../module/canvas.mjs"
|
||||
import LatexAPI from "../module/latex.mjs"
|
||||
|
||||
const CHECK_FOR_UPDATES = new BoolSetting(
|
||||
qsTranslate("general", "Check for updates on startup"),
|
||||
"check_for_updates",
|
||||
"update"
|
||||
)
|
||||
|
||||
const RESET_REDO_STACK = new BoolSetting(
|
||||
qsTranslate("general", "Reset redo stack automaticly"),
|
||||
"reset_redo_stack",
|
||||
"timeline"
|
||||
)
|
||||
|
||||
class EnableLatex extends BoolSetting {
|
||||
constructor() {
|
||||
super(qsTranslate("general", "Enable LaTeX rendering"), "enable_latex", "Expression")
|
||||
}
|
||||
|
||||
set(value) {
|
||||
if(!value || Latex.checkLatexInstallation()) {
|
||||
super.set(value)
|
||||
LatexAPI.enabled = value
|
||||
Canvas.requestPaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default [
|
||||
CHECK_FOR_UPDATES,
|
||||
RESET_REDO_STACK,
|
||||
new EnableLatex()
|
||||
]
|
414
common/src/utils.mjs
Normal file
414
common/src/utils.mjs
Normal file
|
@ -0,0 +1,414 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// Add string methods
|
||||
/**
|
||||
* Replaces latin characters with their uppercase versions.
|
||||
* @return {string}
|
||||
*/
|
||||
String.prototype.toLatinUppercase = String.prototype.toLatinUppercase || function() {
|
||||
return this.replace(/[a-z]/g, function(match) {
|
||||
return match.toUpperCase()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the 'enclosers' of a string (e.g. quotes, parentheses, brackets...)
|
||||
* @return {string}
|
||||
*/
|
||||
String.prototype.removeEnclosure = function() {
|
||||
return this.substring(1, this.length - 1)
|
||||
}
|
||||
|
||||
const powerpos = {
|
||||
"-": "⁻",
|
||||
"+": "⁺",
|
||||
"=": "⁼",
|
||||
" ": " ",
|
||||
"(": "⁽",
|
||||
")": "⁾",
|
||||
"0": "⁰",
|
||||
"1": "¹",
|
||||
"2": "²",
|
||||
"3": "³",
|
||||
"4": "⁴",
|
||||
"5": "⁵",
|
||||
"6": "⁶",
|
||||
"7": "⁷",
|
||||
"8": "⁸",
|
||||
"9": "⁹",
|
||||
"a": "ᵃ",
|
||||
"b": "ᵇ",
|
||||
"c": "ᶜ",
|
||||
"d": "ᵈ",
|
||||
"e": "ᵉ",
|
||||
"f": "ᶠ",
|
||||
"g": "ᵍ",
|
||||
"h": "ʰ",
|
||||
"i": "ⁱ",
|
||||
"j": "ʲ",
|
||||
"k": "ᵏ",
|
||||
"l": "ˡ",
|
||||
"m": "ᵐ",
|
||||
"n": "ⁿ",
|
||||
"o": "ᵒ",
|
||||
"p": "ᵖ",
|
||||
"r": "ʳ",
|
||||
"s": "ˢ",
|
||||
"t": "ᵗ",
|
||||
"u": "ᵘ",
|
||||
"v": "ᵛ",
|
||||
"w": "ʷ",
|
||||
"x": "ˣ",
|
||||
"y": "ʸ",
|
||||
"z": "ᶻ"
|
||||
}
|
||||
|
||||
const exponents = [
|
||||
"⁰","¹","²","³","⁴","⁵","⁶","⁷","⁸","⁹"
|
||||
]
|
||||
|
||||
const exponentReg = new RegExp('(['+exponents.join('')+']+)', 'g')
|
||||
|
||||
const indicepos = {
|
||||
"-": "₋",
|
||||
"+": "₊",
|
||||
"=": "₌",
|
||||
"(": "₍",
|
||||
")": "₎",
|
||||
" ": " ",
|
||||
"0": "₀",
|
||||
"1": "₁",
|
||||
"2": "₂",
|
||||
"3": "₃",
|
||||
"4": "₄",
|
||||
"5": "₅",
|
||||
"6": "₆",
|
||||
"7": "₇",
|
||||
"8": "₈",
|
||||
"9": "₉",
|
||||
"a": "ₐ",
|
||||
"e": "ₑ",
|
||||
"h": "ₕ",
|
||||
"i": "ᵢ",
|
||||
"j": "ⱼ",
|
||||
"k": "ₖ",
|
||||
"l": "ₗ",
|
||||
"m": "ₘ",
|
||||
"n": "ₙ",
|
||||
"o": "ₒ",
|
||||
"p": "ₚ",
|
||||
"r": "ᵣ",
|
||||
"s": "ₛ",
|
||||
"t": "ₜ",
|
||||
"u": "ᵤ",
|
||||
"v": "ᵥ",
|
||||
"x": "ₓ",
|
||||
}
|
||||
// Put a text in sup position
|
||||
export function textsup(text) {
|
||||
let ret = ""
|
||||
text = text.toString()
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
if(Object.keys(powerpos).indexOf(text[i]) >= 0) {
|
||||
ret += powerpos[text[i]]
|
||||
} else {
|
||||
ret += text[i]
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Put a text in sub position
|
||||
export function textsub(text) {
|
||||
let ret = ""
|
||||
text = text.toString()
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
if(Object.keys(indicepos).indexOf(text[i]) >= 0) {
|
||||
ret += indicepos[text[i]]
|
||||
} else {
|
||||
ret += text[i]
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplifies (mathematically) a mathematical expression.
|
||||
* @param {string} str - Expression to parse
|
||||
* @returns {string}
|
||||
*/
|
||||
export function simplifyExpression(str) {
|
||||
let 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$2 $7 $8 $4 $5 $7 $8$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)
|
||||
//let [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)
|
||||
[ope2, ope4] = [ope2, ope4].map(ope => ope === '/' ? '/' : '*')
|
||||
let coeff1 = n1*n2
|
||||
let coeff2 = n3*n4
|
||||
let 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,
|
||||
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/subtractions.
|
||||
/(^|[^*\/] |\()([-.\d]+) [+-] (\([^)(]+\)|[^)(]+) [+-] ([-.\d]+)($| [^*\/]|\))/g,
|
||||
function(match, b4, n1, op1, middle, op2, n2, after) {
|
||||
let 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.
|
||||
/^\s*\((.*)\)\s*$/g,
|
||||
function(match, middle) {
|
||||
let 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(\.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
|
||||
let found
|
||||
do {
|
||||
found = false
|
||||
for(let replacement of replacements)
|
||||
while(replacement[0].test(str)) {
|
||||
found = true
|
||||
str = str.replace(replacement[0], replacement[1])
|
||||
}
|
||||
} while(found)
|
||||
return str
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Transforms a mathematical expression to make it readable by humans.
|
||||
* NOTE: Will break parsing of expression.
|
||||
* @param {string} str - Expression to parse.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function makeExpressionReadable(str) {
|
||||
let replacements = [
|
||||
// letiables
|
||||
[/pi/g, 'π'],
|
||||
[/Infinity/g, '∞'],
|
||||
[/inf/g, '∞'],
|
||||
// Other
|
||||
[/ \* /g, '×'],
|
||||
[/ \^ /g, '^'],
|
||||
[/\^\(([\d\w+-]+)\)/g, function(match, p1) { return textsup(p1) }],
|
||||
[/\^([\d\w+-]+)/g, function(match, p1) { return textsup(p1) }],
|
||||
[/_\(([\d\w+-]+)\)/g, function(match, p1) { return textsub(p1) }],
|
||||
[/_([\d\w+-]+)/g, function(match, p1) { return textsub(p1) }],
|
||||
[/\[([^\[\]]+)\]/g, function(match, p1) { return textsub(p1) }],
|
||||
[/(\d|\))×/g, '$1'],
|
||||
[/integral\((.+),\s?(.+),\s?["'](.+)["'],\s?["'](.+)["']\)/g, function(match, a, b, p1, body, p2, p3, by, p4) {
|
||||
if(a.length < b.length) {
|
||||
return `∫${textsub(a)}${textsup(b)} ${body} d${by}`
|
||||
} else {
|
||||
return `∫${textsup(b)}${textsub(a)} ${body} d${by}`
|
||||
}
|
||||
}],
|
||||
[/derivative\(?["'](.+)["'], ?["'](.+)["'], ?(.+)\)?/g, function(match, p1, body, p2, p3, by, p4, x) {
|
||||
return `d(${body.replace(new RegExp(by, 'g'), 'x')})/dx`
|
||||
}]
|
||||
]
|
||||
|
||||
// str = simplifyExpression(str)
|
||||
// Replacements
|
||||
for(let replacement of replacements)
|
||||
while(replacement[0].test(str))
|
||||
str = str.replace(replacement[0], replacement[1])
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a variable name to make it readable by humans.
|
||||
*
|
||||
* @param {string} str - Variable name to parse
|
||||
* @param {boolean} removeUnallowed - Remove domain symbols disallowed in name.
|
||||
* @returns {string} - The parsed name
|
||||
*/
|
||||
export function parseName(str, removeUnallowed = true) {
|
||||
let replacements = [
|
||||
// Greek letters
|
||||
[/([^a-z]|^)al(pha)?([^a-z]|$)/g, '$1α$3'],
|
||||
[/([^a-z]|^)be(ta)?([^a-z]|$)/g, '$1β$3'],
|
||||
[/([^a-z]|^)ga(mma)?([^a-z]|$)/g, '$1γ$3'],
|
||||
[/([^a-z]|^)de(lta)?([^a-z]|$)/g, '$1δ$3'],
|
||||
[/([^a-z]|^)ep(silon)?([^a-z]|$)/g, '$1ε$3'],
|
||||
[/([^a-z]|^)ze(ta)?([^a-z]|$)/g, '$1ζ$3'],
|
||||
[/([^a-z]|^)et(a)?([^a-z]|$)/g, '$1η$3'],
|
||||
[/([^a-z]|^)th(eta)?([^a-z]|$)/g, '$1θ$3'],
|
||||
[/([^a-z]|^)io(ta)?([^a-z]|$)/g, '$1ι$3'],
|
||||
[/([^a-z]|^)ka(ppa)([^a-z]|$)?/g, '$1κ$3'],
|
||||
[/([^a-z]|^)la(mbda)?([^a-z]|$)/g, '$1λ$3'],
|
||||
[/([^a-z]|^)mu([^a-z]|$)/g, '$1μ$2'],
|
||||
[/([^a-z]|^)nu([^a-z]|$)/g, '$1ν$2'],
|
||||
[/([^a-z]|^)xi([^a-z]|$)/g, '$1ξ$2'],
|
||||
[/([^a-z]|^)rh(o)?([^a-z]|$)/g, '$1ρ$3'],
|
||||
[/([^a-z]|^)si(gma)?([^a-z]|$)/g, '$1σ$3'],
|
||||
[/([^a-z]|^)ta(u)?([^a-z]|$)/g, '$1τ$3'],
|
||||
[/([^a-z]|^)up(silon)?([^a-z]|$)/g, '$1υ$3'],
|
||||
[/([^a-z]|^)ph(i)?([^a-z]|$)/g, '$1φ$3'],
|
||||
[/([^a-z]|^)ch(i)?([^a-z]|$)/g, '$1χ$3'],
|
||||
[/([^a-z]|^)ps(i)?([^a-z]|$)/g, '$1ψ$3'],
|
||||
[/([^a-z]|^)om(ega)?([^a-z]|$)/g, '$1ω$3'],
|
||||
// Capital greek letters
|
||||
[/([^a-z]|^)gga(mma)?([^a-z]|$)/g, '$1Γ$3'],
|
||||
[/([^a-z]|^)gde(lta)?([^a-z]|$)/g, '$1Δ$3'],
|
||||
[/([^a-z]|^)gth(eta)?([^a-z]|$)/g, '$1Θ$3'],
|
||||
[/([^a-z]|^)gla(mbda)?([^a-z]|$)/g, '$1Λ$3'],
|
||||
[/([^a-z]|^)gxi([^a-z]|$)/g, '$1Ξ$2'],
|
||||
[/([^a-z]|^)gpi([^a-z]|$)/g, '$1Π$2'],
|
||||
[/([^a-z]|^)gsi(gma)([^a-z]|$)?/g, '$1Σ$3'],
|
||||
[/([^a-z]|^)gph(i)?([^a-z]|$)/g, '$1Φ$3'],
|
||||
[/([^a-z]|^)gps(i)?([^a-z]|$)/g, '$1Ψ$3'],
|
||||
[/([^a-z]|^)gom(ega)?([^a-z]|$)/g, '$1Ω$3'],
|
||||
// Underscores
|
||||
// [/_\(([^_]+)\)/g, function(match, p1) { return textsub(p1) }],
|
||||
// [/_([^" ]+)/g, function(match, p1) { return textsub(p1) }],
|
||||
// Array elements
|
||||
[/\[([^\]\[]+)\]/g, function(match, p1) { return textsub(p1) }],
|
||||
// Removing
|
||||
[/[xπℝℕ\\∪∩\]\[ ()^/÷*×+=\d-]/g , ''],
|
||||
]
|
||||
if(!removeUnallowed) replacements.pop()
|
||||
// Replacements
|
||||
for(let replacement of replacements)
|
||||
str = str.replace(replacement[0], replacement[1])
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms camel case strings to a space separated one.
|
||||
*
|
||||
* @param {string} label - Camel case to parse
|
||||
* @returns {string} Parsed label.
|
||||
*/
|
||||
export function camelCase2readable(label) {
|
||||
let parsed = parseName(label, false)
|
||||
return parsed.charAt(0).toLatinUppercase() + parsed.slice(1).replace(/([A-Z])/g," $1")
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a randomized color string.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getRandomColor() {
|
||||
let clrs = '0123456789ABCDEF';
|
||||
let color = '#';
|
||||
for(let i = 0; i < 6; i++) {
|
||||
color += clrs[Math.floor(Math.random() * (16-5*(i%2===0)))];
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes text to html entities.
|
||||
* @param {string} str
|
||||
* @returns {string}
|
||||
*/
|
||||
export function escapeHTML(str) {
|
||||
return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') ;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Parses exponents and replaces them with expression values
|
||||
* @param {string} expression - The expression to replace in.
|
||||
* @return {string} The parsed expression
|
||||
*/
|
||||
export function exponentsToExpression(expression) {
|
||||
return expression.replace(exponentReg, (m, exp) => '^' + exp.split('').map((x) => exponents.indexOf(x)).join(''))
|
||||
}
|
30
common/test/hooks.mjs
Normal file
30
common/test/hooks.mjs
Normal file
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import * as fs from "./mock/fs.mjs";
|
||||
import Qt from "./mock/qt.mjs";
|
||||
import { MockHelper } from "./mock/helper.mjs";
|
||||
import { MockLatex } from "./mock/latex.mjs";
|
||||
import Modules from "../../LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/module/index.mjs";
|
||||
|
||||
function setup() {
|
||||
globalThis.Helper = new MockHelper()
|
||||
globalThis.Latex = new MockLatex()
|
||||
Modules.Latex.initialize({ latex: Latex, helper: Helper })
|
||||
}
|
||||
|
||||
setup()
|
64
common/test/math/domain.mjs
Normal file
64
common/test/math/domain.mjs
Normal file
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { describe, it } from "mocha"
|
||||
import { expect } from "chai"
|
||||
|
||||
import { Domain, parseDomainSimple } from "../../../LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/math/domain.mjs"
|
||||
|
||||
describe("math.domain", function() {
|
||||
describe("#parseDomainSimple", function() {
|
||||
it("returns predefined domains", function() {
|
||||
const predefinedToCheck = [
|
||||
// Real domains
|
||||
{ domain: Domain.R, shortcuts: ["R", "ℝ"] },
|
||||
// Zero exclusive real domains
|
||||
{ domain: Domain.RE, shortcuts: ["RE", "R*", "ℝ*"] },
|
||||
// Real positive domains
|
||||
{ domain: Domain.RP, shortcuts: ["RP", "R+", "ℝ⁺", "ℝ+"] },
|
||||
// Zero-exclusive real positive domains
|
||||
{ domain: Domain.RPE, shortcuts: ["RPE", "REP", "R+*", "R*+", "ℝ*⁺", "ℝ⁺*", "ℝ*+", "ℝ+*"] },
|
||||
// Real negative domain
|
||||
{ domain: Domain.RM, shortcuts: ["RM", "R-", "ℝ⁻", "ℝ-"] },
|
||||
// Zero-exclusive real negative domains
|
||||
{ domain: Domain.RME, shortcuts: ["RME", "REM", "R-*", "R*-", "ℝ⁻*", "ℝ*⁻", "ℝ-*", "ℝ*-"] },
|
||||
// Natural integers domain
|
||||
{ domain: Domain.N, shortcuts: ["ℕ", "N", "ZP", "Z+", "ℤ⁺", "ℤ+"] },
|
||||
// Zero-exclusive natural integers domain
|
||||
{ domain: Domain.NE, shortcuts: ["NE", "NP", "N*", "N+", "ℕ*", "ℕ⁺", "ℕ+", "ZPE", "ZEP", "Z+*", "Z*+", "ℤ⁺*", "ℤ*⁺", "ℤ+*", "ℤ*+"] },
|
||||
// Logarithmic natural domains
|
||||
{ domain: Domain.NLog, shortcuts: ["NLOG", "ℕˡᵒᵍ", "ℕLOG"] },
|
||||
// All integers domains
|
||||
{ domain: Domain.Z, shortcuts: ["Z", "ℤ"] },
|
||||
// Zero-exclusive all integers domain
|
||||
{ domain: Domain.ZE, shortcuts: ["ZE", "Z*", "ℤ*"] },
|
||||
// Negative integers domain
|
||||
{ domain: Domain.ZM, shortcuts: ["ZM", "Z-", "ℤ⁻", "ℤ-"] },
|
||||
// Zero-exclusive negative integers domain
|
||||
{ domain: Domain.ZME, shortcuts: ["ZME", "ZEM", "Z-*", "Z*-", "ℤ⁻*", "ℤ*⁻", "ℤ-*", "ℤ*-"] },
|
||||
]
|
||||
|
||||
// Real domains
|
||||
for(const { domain, shortcuts } of predefinedToCheck)
|
||||
for(const shortcut of shortcuts)
|
||||
expect(parseDomainSimple(shortcut)).to.be.equal(domain)
|
||||
})
|
||||
|
||||
it("")
|
||||
})
|
||||
})
|
44
common/test/mock/fs.mjs
Normal file
44
common/test/mock/fs.mjs
Normal file
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { readFileSync as readNode } from "node:fs"
|
||||
import { dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
export const HOME = "/home/user"
|
||||
export const TMP = "/tmp"
|
||||
|
||||
|
||||
const filesystem = {
|
||||
[`${HOME}/test1.lpf`]: readNode(__dirname + "/../../../ci/test1.lpf")
|
||||
}
|
||||
|
||||
|
||||
export function existsSync(file) {
|
||||
return filesystem[file] !== undefined
|
||||
}
|
||||
|
||||
export function writeFileSync(file, data, encoding) {
|
||||
filesystem[file] = Buffer.from(data, encoding)
|
||||
}
|
||||
|
||||
export function readFileSync(file, encoding) {
|
||||
return filesystem[file].toString(encoding)
|
||||
}
|
158
common/test/mock/helper.mjs
Normal file
158
common/test/mock/helper.mjs
Normal file
|
@ -0,0 +1,158 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync, existsSync } from "./fs.mjs"
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
"check_for_updates": true,
|
||||
"reset_redo_stack": true,
|
||||
"last_install_greet": "0",
|
||||
"enable_latex": false,
|
||||
"expression_editor": {
|
||||
"autoclose": true,
|
||||
"colorize": true,
|
||||
"color_scheme": 0
|
||||
},
|
||||
"autocompletion": {
|
||||
"enabled": true
|
||||
},
|
||||
"default_graph": {
|
||||
"xzoom": 100,
|
||||
"yzoom": 10,
|
||||
"xmin": 5 / 10,
|
||||
"ymax": 25,
|
||||
"xaxisstep": "4",
|
||||
"yaxisstep": "4",
|
||||
"xlabel": "",
|
||||
"ylabel": "",
|
||||
"linewidth": 1,
|
||||
"textsize": 18,
|
||||
"logscalex": true,
|
||||
"showxgrad": true,
|
||||
"showygrad": true
|
||||
}
|
||||
}
|
||||
|
||||
export class MockHelper {
|
||||
constructor() {
|
||||
this.__settings = { ...DEFAULT_SETTINGS }
|
||||
}
|
||||
|
||||
__getSetting(settingName) {
|
||||
const namespace = settingName.split(".")
|
||||
let data = this.__settings
|
||||
for(const name of namespace)
|
||||
if(data.hasOwnProperty(name))
|
||||
data = data[name]
|
||||
else
|
||||
throw new Error(`Setting ${namespace} does not exist.`)
|
||||
return data
|
||||
}
|
||||
|
||||
__setSetting(settingName, value) {
|
||||
const namespace = settingName.split(".")
|
||||
const finalName = namespace.pop()
|
||||
let data = this.__settings
|
||||
for(const name of namespace)
|
||||
if(data.hasOwnProperty(name))
|
||||
data = data[name]
|
||||
else
|
||||
throw new Error(`Setting ${namespace} does not exist.`)
|
||||
data[finalName] = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a setting from the config
|
||||
* @param {string} settingName - Setting (and its dot-separated namespace) to get (e.g. "default_graph.xmin")
|
||||
* @returns {boolean} Value of the setting
|
||||
*/
|
||||
getSettingBool(settingName) {
|
||||
return this.__getSetting(settingName) === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a setting from the config
|
||||
* @param {string} settingName - Setting (and its dot-separated namespace) to get (e.g. "default_graph.xmin")
|
||||
* @returns {number} Value of the setting
|
||||
*/
|
||||
getSettingInt(settingName) {
|
||||
return +(this.__getSetting(settingName))
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a setting from the config
|
||||
* @param {string} settingName - Setting (and its dot-separated namespace) to get (e.g. "default_graph.xmin")
|
||||
* @returns {string} Value of the setting
|
||||
*/
|
||||
getSetting(settingName) {
|
||||
return this.__getSetting(settingName).toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a setting in the config
|
||||
* @param {string} settingName - Setting (and its dot-separated namespace) to set (e.g. "default_graph.xmin")
|
||||
* @param {boolean} value
|
||||
*/
|
||||
setSettingBool(settingName, value) {
|
||||
return this.__setSetting(settingName, value === true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a setting in the config
|
||||
* @param {string} settingName - Setting (and its dot-separated namespace) to set (e.g. "default_graph.xmin")
|
||||
* @param {number} value
|
||||
*/
|
||||
setSettingInt(settingName, value) {
|
||||
return this.__setSetting(settingName, +(value))
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a setting in the config
|
||||
* @param {string} settingName - Setting (and its dot-separated namespace) to set (e.g. "default_graph.xmin")
|
||||
* @param {string} value
|
||||
*/
|
||||
setSetting(settingName, value) {
|
||||
return this.__setSetting(settingName, value.toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends data to be written
|
||||
* @param {string} file
|
||||
* @param {string} dataToWrite - just JSON encoded, requires the "LPFv1" mime to be added before writing
|
||||
*/
|
||||
write(file, dataToWrite) {
|
||||
writeFileSync(file, "LPFv1" + dataToWrite)
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests data to be read from a file
|
||||
* @param {string} file
|
||||
* @returns {string} the loaded data - just JSON encoded, requires the "LPFv1" mime to be stripped
|
||||
*/
|
||||
load(file) {
|
||||
if(existsSync(file)) {
|
||||
const data = readFileSync(file, "utf8")
|
||||
if(data.startsWith("LPFv1"))
|
||||
return data.substring(5)
|
||||
else
|
||||
throw new Error(`Invalid LogarithmPlotter file.`)
|
||||
} else
|
||||
throw new Error(`File not found.`)
|
||||
}
|
||||
|
||||
}
|
90
common/test/mock/latex.mjs
Normal file
90
common/test/mock/latex.mjs
Normal file
|
@ -0,0 +1,90 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { TMP, existsSync, writeFileSync } from "./fs.mjs"
|
||||
|
||||
const PIXEL = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAACklEQVR4AWNgAAAAAgABc3UBGAAAAABJRU5ErkJggg=="
|
||||
|
||||
export class MockLatex {
|
||||
constructor() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a simple string hash.
|
||||
* @param {string} string
|
||||
* @return {number}
|
||||
* @private
|
||||
*/
|
||||
__hash(string) {
|
||||
let hash = 0
|
||||
let i, chr
|
||||
if(string.length === 0) return hash
|
||||
for(i = 0; i < string.length; i++) {
|
||||
chr = string.charCodeAt(i)
|
||||
hash = ((hash << 5) - hash) + chr
|
||||
hash |= 0 // Convert to 32bit integer
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} markup
|
||||
* @param {number} fontSize
|
||||
* @param {string} color
|
||||
* @return {string}
|
||||
* @private
|
||||
*/
|
||||
__getFileName(markup, fontSize, color) {
|
||||
const name = this.__hash(`${markup}_${fontSize}_${color}`)
|
||||
return `${TMP}/${name}.png`
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} markup - LaTeX markup to render
|
||||
* @param {number} fontSize - Font size (in pt) to render
|
||||
* @param {string} color - Color of the text to render
|
||||
* @returns {string} - Comma separated data of the image (source, width, height)
|
||||
*/
|
||||
render(markup, fontSize, color) {
|
||||
const file = this.__getFileName(markup, fontSize, color)
|
||||
writeFileSync(file, PIXEL, "base64")
|
||||
return `${file},1,1`
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} markup - LaTeX markup to render
|
||||
* @param {number} fontSize - Font size (in pt) to render
|
||||
* @param {string} color - Color of the text to render
|
||||
* @returns {string} - Comma separated data of the image (source, width, height)
|
||||
*/
|
||||
findPrerendered(markup, fontSize, color) {
|
||||
const file = this.__getFileName(markup, fontSize, color)
|
||||
if(existsSync(file))
|
||||
return `${file},1,1`
|
||||
return ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the Latex installation is valid
|
||||
* @returns {boolean}
|
||||
*/
|
||||
checkLatexInstallation() {
|
||||
return true // We're not *actually* doing any latex.
|
||||
}
|
||||
}
|
60
common/test/mock/qt.mjs
Normal file
60
common/test/mock/qt.mjs
Normal file
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||
* Copyright (C) 2021-2024 Ad5001
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// Mock qt methods.
|
||||
|
||||
/**
|
||||
* Polyfill for Qt.rect.
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} width
|
||||
* @param {number} height
|
||||
* @returns {{x, width, y, height}}
|
||||
*/
|
||||
function rect(x, y, width, height) {
|
||||
return { x, y, width, height }
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock for QT_TRANSLATE_NOOP and qsTranslate
|
||||
* @param {string} category
|
||||
* @param {string} string
|
||||
* @return {string}
|
||||
*/
|
||||
function QT_TRANSLATE_NOOP(category, string) {
|
||||
return string
|
||||
}
|
||||
|
||||
function setup() {
|
||||
globalThis.Qt = {
|
||||
rect
|
||||
}
|
||||
|
||||
globalThis.QT_TRANSLATE_NOOP = QT_TRANSLATE_NOOP
|
||||
globalThis.qsTranslate = QT_TRANSLATE_NOOP
|
||||
|
||||
String.prototype.arg = function() { return this; } // No need to reimplement it for now.
|
||||
}
|
||||
|
||||
setup()
|
||||
|
||||
export default {
|
||||
rect,
|
||||
QT_TRANSLATE_NOOP,
|
||||
qtTranslate: QT_TRANSLATE_NOOP,
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue