Reorganizing paths

This commit is contained in:
Adsooi 2024-09-30 00:23:39 +02:00
parent e9d204daab
commit 34cb856dd4
Signed by: Ad5001
GPG key ID: EF45F9C6AFE20160
249 changed files with 118 additions and 294 deletions

25
common/.mocharc.jsonc Normal file
View 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
View file

@ -0,0 +1,6 @@
{
"presets": ["@babel/preset-env"],
"targets": {
"esmodules": true
}
}

4440
common/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

33
common/package.json Normal file
View 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
View 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"
}),
]
}

View 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;\">&nbsp;" + this.targetName + "&nbsp;</b>")
.arg(this.formatColor(this.previousValue)).arg(this.formatColor(this.newValue))
}
}

View 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()
}
}

View 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>")
}
}

View 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>")
}
}

View 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);\">&nbsp;" + this.prevString + "&nbsp;</tt>"
this.nextHTML = "<tt style=\"background: rgba(128,128,128,0.1);\">&nbsp;" + this.nextString + "&nbsp;</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;\">&nbsp;" + this.targetName + "&nbsp;</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)
}
}

View 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,
}

View 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>')
}
}

View 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);\">&nbsp;" + escapeHTML(this.prevString) + "&nbsp;</tt>"
this.nextHTML = "<tt style=\"background: rgba(128,128,128,0.1);\">&nbsp;" + escapeHTML(this.nextString) + "&nbsp;</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;\">&nbsp;" + this.targetName + "&nbsp;</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)
}
}

View 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
View 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"

View 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)
}
}
}

View 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)
}

View 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]
}
}

View 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))
}
}
}
}

View 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
}

View 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))
}
}

View 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
}
}

View 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
View 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()
}

View 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
View 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

View 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
}
}

View 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

View 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
}
}

View 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

View 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

View 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
}

View 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
View 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
View 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

View 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

View 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

View 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)
}

View 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()
}
}
}

View 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)))
}
}
}
}
}

View 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()
}
}
}

View 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
View 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
}
}

View 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)))
}
}

View 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
View 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)
}
}

View 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
View 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
View 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
View 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
}

View 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.

View 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 + ".")
}
}

View 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

View 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)]
}

View 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())
}
}

View 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)
}
}

View 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
]

View 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
]

View 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
View 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;') ;
}
/**
* 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
View 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()

View 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
View 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
View 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.`)
}
}

View 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
View 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,
}