Compare commits

..

No commits in common. "master" and "rollup-js" have entirely different histories.

116 changed files with 4710 additions and 8223 deletions

6
.gitignore vendored
View file

@ -37,10 +37,8 @@ docs/html
*.lpf *.lpf
*.lgg *.lgg
# Tests
common/coverage/
**/.coverage
# npm # npm
common/node_modules common/node_modules
common/coverage/
common/.coverage
runtime-pyside6/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/index.mjs* runtime-pyside6/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/index.mjs*

View file

@ -1,4 +1,4 @@
# ![icon](https://apps.ad5001.eu/icons/apps/svg/logarithmplotter.svg) LogarithmPlotter # ![icon](https://git.ad5001.eu/Ad5001/LogarithmPlotter/raw/branch/master/logplotter.svg) LogarithmPlotter
[![Build Status](https://ci.ad5001.eu/api/badges/Ad5001/LogarithmPlotter/status.svg)](https://ci.ad5001.eu/Ad5001/LogarithmPlotter) [![Build Status](https://ci.ad5001.eu/api/badges/Ad5001/LogarithmPlotter/status.svg)](https://ci.ad5001.eu/Ad5001/LogarithmPlotter)
[![Translation status](https://hosted.weblate.org/widgets/logarithmplotter/-/logarithmplotter/svg-badge.svg)](https://hosted.weblate.org/engage/logarithmplotter/) [![Translation status](https://hosted.weblate.org/widgets/logarithmplotter/-/logarithmplotter/svg-badge.svg)](https://hosted.weblate.org/engage/logarithmplotter/)
@ -24,7 +24,7 @@ First, you'll need to install all the required dependencies:
- [npm](https://npmjs.com) (or [yarn](https://yarnpkg.com/)), go to the `common` directory, and run `npm install` (or `yarn install`). - [npm](https://npmjs.com) (or [yarn](https://yarnpkg.com/)), go to the `common` directory, and run `npm install` (or `yarn install`).
You can simply run LogarithmPlotter using `python3 run.py`. It automatically compiles the language files (requires You can simply run LogarithmPlotter using `python3 run.py`. It automatically compiles the language files (requires
`pyside6-lrelease` to be installed and in path), and the JavaScript modules. `lrelease` to be installed and in path), and the JavaScript modules.
If you do not wish do recompile the files again on every run, you can use the build script (`scripts/build.sh`) and run If you do not wish do recompile the files again on every run, you can use the build script (`scripts/build.sh`) and run
`python3 build/runtime-pyside6/LogarithmPlotter/logarithmplotter.py`. `python3 build/runtime-pyside6/LogarithmPlotter/logarithmplotter.py`.
@ -68,13 +68,7 @@ To run LogarithmPlotter's tests, follow these steps:
- Python - Python
- Install python3 and [poetry](https://python-poetry.org/) - Install python3 and [poetry](https://python-poetry.org/)
- Create and activate virtual env (recommended) - Run `poetry install --with test`
- Go into `runtime-pyside6` and run `poetry install --with test`
- ECMAScript
- Install node with npm
- Go into `common` and run `npm install -D`
Finally, to actually run the tests:
- Run `scripts/run-tests.sh` - Run `scripts/run-tests.sh`
## Legal notice ## Legal notice

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,2 +1,2 @@
#!/bin/bash #!/bin/bash
pyside6-lrelease *.ts lrelease *.ts

View file

@ -21,7 +21,7 @@ replace() {
rm ../qml/eu/ad5001/LogarithmPlotter/js/index.mjs # Remove index which should not be scanned rm ../qml/eu/ad5001/LogarithmPlotter/js/index.mjs # Remove index which should not be scanned
files=$(find ../../common/src -name '*.mjs') files=$(find .. -name *.mjs)
for file in $files; do for file in $files; do
echo "Moving '$file' to '${file%.*}.js'..." echo "Moving '$file' to '${file%.*}.js'..."
mv "$file" "${file%.*}.js" mv "$file" "${file%.*}.js"
@ -33,14 +33,12 @@ for file in $files; do
replace "${file%.*}.js" "^export" "/*export*/" replace "${file%.*}.js" "^export" "/*export*/"
replace "${file%.*}.js" "async " "/*async */" replace "${file%.*}.js" "async " "/*async */"
replace "${file%.*}.js" "await" "/*await */" replace "${file%.*}.js" "await" "/*await */"
replace "${file%.*}.js" " #" "// #"
replace "${file%.*}.js" "this.#" "/*this.#*/"
done done
echo "----------------------------" echo "----------------------------"
echo "| Updating translations... |" echo "| Updating translations... |"
echo "----------------------------" echo "----------------------------"
pyside6-lupdate -extensions js,qs,qml,py -recursive ../../common/src -recursive ../../runtime-pyside6/LogarithmPlotter -ts lp_*.ts lupdate -extensions js,qs,qml,py -recursive .. -ts lp_*.ts
# Updating locations in files # Updating locations in files
for lp in *.ts; do for lp in *.ts; do
echo "Replacing locations in $lp..." echo "Replacing locations in $lp..."
@ -57,9 +55,7 @@ for file in $files; do
replace "$file" "/*async */" "async " replace "$file" "/*async */" "async "
replace "$file" "^/*export*/" "export" replace "$file" "^/*export*/" "export"
replace "$file" "^/*export default*/" "export default" replace "$file" "^/*export default*/" "export default"
replace "$file" '.mjs"*/' '.mjs"'
replace "$file" "^/*import" "import" replace "$file" "^/*import" "import"
replace "$file" "^/*export" "export" replace "$file" "^/*export" "export"
replace "$file" "// #" " #" replace "$file" '.mjs"*/$' '.mjs"'
replace "$file" "/*this.#*/" "this.#"
done done

View file

@ -3,7 +3,7 @@ Source: logarithmplotter
Version: 0.6.0 Version: 0.6.0
Architecture: all Architecture: all
Maintainer: Ad5001 <mail@ad5001.eu> Maintainer: Ad5001 <mail@ad5001.eu>
Depends: python3 (>= 3.9), python3-pip, python3-pyside6-essentials (>= 6.7.0), python3-pyside6-addons (>= 6.7), texlive-latex-base, dvipng Depends: python3 (>= 3.9), python3-pip, python3-pyside6-essentials (>= 6.7.0), texlive-latex-base, dvipng
Build-Depends: debhelper (>=11~), dh-python, dpkg-dev (>= 1.16.1~), python-setuptools Build-Depends: debhelper (>=11~), dh-python, dpkg-dev (>= 1.16.1~), python-setuptools
Section: science Section: science

View file

@ -1 +0,0 @@
python3 (>= 3.9), python3-pip, python3-pyside6.qtcore (>= 6), python3-pyside6.qtgui (>= 6), python3-pyside6.qtqml (>= 6), python3-pyside6.qtwidgets (>= 6), python3-pyside6.qtquick (>= 6), python3-pyside6.qtquickcontrols2 (>= 6), qml6-module-qt-labs-platform (>= 6), qml6-module-qtquick-dialogs (>= 6), texlive-latex-base, dvipng

View file

@ -66,54 +66,50 @@
<categories> <categories>
<category>Science</category> <category>Science</category>
<category>Education</category> <category>Education</category>
<category>Qt</category>
</categories> </categories>
<url type="homepage">https://apps.ad5001.eu/logarithmplotter/</url> <url type="homepage">https://apps.ad5001.eu/logarithmplotter/</url>
<url type="bugtracker">https://git.ad5001.eu/Ad5001/LogarithmPlotter/issues/</url> <url type="bugtracker">https://git.ad5001.eu/Ad5001/LogarithmPlotter/issues/</url>
<url type="help">https://git.ad5001.eu/Ad5001/LogarithmPlotter/wiki/</url> <url type="help">https://git.ad5001.eu/Ad5001/LogarithmPlotter/wiki/</url>
<url type="translate">https://hosted.weblate.org/engage/logarithmplotter/</url> <url type="translate">https://hosted.weblate.org/engage/logarithmplotter/</url>
<screenshots> <screenshots>
<screenshot type="default"> <screenshot type="default">
<image>https://apps.ad5001.eu/img/en/logarithmplotter/gain.png?v=0.6</image> <image>https://apps.ad5001.eu/img/en/logarithmplotter/gain.png?v=0.5</image>
<image xml:lang="de">https://apps.ad5001.eu/img/de/logarithmplotter/gain.png?v=0.6</image> <image xml:lang="de">https://apps.ad5001.eu/img/de/logarithmplotter/gain.png?v=0.5</image>
<image xml:lang="fr">https://apps.ad5001.eu/img/fr/logarithmplotter/gain.png?v=0.6</image> <image xml:lang="fr">https://apps.ad5001.eu/img/fr/logarithmplotter/gain.png?v=0.5</image>
<image xml:lang="hu">https://apps.ad5001.eu/img/hu/logarithmplotter/gain.png?v=0.6</image> <image xml:lang="hu">https://apps.ad5001.eu/img/hu/logarithmplotter/gain.png?v=0.5</image>
<image xml:lang="no">https://apps.ad5001.eu/img/no/logarithmplotter/gain.png?v=0.6</image> <image xml:lang="no">https://apps.ad5001.eu/img/no/logarithmplotter/gain.png?v=0.5</image>
<image xml:lang="es">https://apps.ad5001.eu/img/es/logarithmplotter/gain.png?v=0.6</image>
<caption>Main view of LogarithmPlotter showing an asymptotic Bode magnitude plot.</caption> <caption>Main view of LogarithmPlotter showing an asymptotic Bode magnitude plot.</caption>
<caption xml:lang="de">Die Hauptansicht des LogarithmPlotters zeigt eine asymptotische Bode-Magnitude-Darstellung.</caption> <caption xml:lang="de">Die Hauptansicht des LogarithmPlotters zeigt eine asymptotische Bode-Magnitude-Darstellung.</caption>
<caption xml:lang="fr">Vue principale de LogarithmPlotter montrant un tracé asymptotique d'une magnitude de Bode.</caption> <caption xml:lang="fr">Vue principale de LogarithmPlotter montrant un tracé asymptotique d'une magnitude de Bode.</caption>
<caption xml:lang="hu">A LogarithmPlotter fő nézete, amely egy aszimptotikus Bode-magnitúdó ábrát mutat.</caption> <caption xml:lang="hu">A LogarithmPlotter fő nézete, amely egy aszimptotikus Bode-magnitúdó ábrát mutat.</caption>
<caption xml:lang="no">Hovedvisning av LogarithmPlotter som viser et asymptotisk Bode-størrelsesplott.</caption> <caption xml:lang="no">Hovedvisning av LogarithmPlotter som viser et asymptotisk Bode-størrelsesplott.</caption>
<caption xml:lang="es">Vista principal de LogarithmPlotter mostrando un gráfico asintótico de una magnitud de Bode.</caption>
</screenshot> </screenshot>
<screenshot> <screenshot>
<image>https://apps.ad5001.eu/img/en/logarithmplotter/phase.png?v=0.6</image> <image>https://apps.ad5001.eu/img/en/logarithmplotter/phase.png?v=0.5</image>
<image xml:lang="de">https://apps.ad5001.eu/img/de/logarithmplotter/phase.png?v=0.6</image> <image xml:lang="de">https://apps.ad5001.eu/img/de/logarithmplotter/phase.png?v=0.5</image>
<image xml:lang="fr">https://apps.ad5001.eu/img/fr/logarithmplotter/phase.png?v=0.6</image> <image xml:lang="fr">https://apps.ad5001.eu/img/fr/logarithmplotter/phase.png?v=0.5</image>
<image xml:lang="hu">https://apps.ad5001.eu/img/hu/logarithmplotter/phase.png?v=0.6</image> <image xml:lang="hu">https://apps.ad5001.eu/img/hu/logarithmplotter/phase.png?v=0.5</image>
<image xml:lang="no">https://apps.ad5001.eu/img/no/logarithmplotter/phase.png?v=0.6</image> <image xml:lang="no">https://apps.ad5001.eu/img/no/logarithmplotter/phase.png?v=0.5</image>
<image xml:lang="es">https://apps.ad5001.eu/img/es/logarithmplotter/phase.png?v=0.6</image>
<caption>Main view of LogarithmPlotter showing an asymptotic Bode phase plot.</caption> <caption>Main view of LogarithmPlotter showing an asymptotic Bode phase plot.</caption>
<caption xml:lang="de">Hauptansicht des LogarithmPlotters mit einer asymptotischen Bode-Phasendarstellung.</caption> <caption xml:lang="de">Hauptansicht des LogarithmPlotters mit einer asymptotischen Bode-Phasendarstellung.</caption>
<caption xml:lang="fr">Vue principale de LogarithmPlotter montrant un tracé asymptotique d'une phase de Bode.</caption> <caption xml:lang="fr">Vue principale de LogarithmPlotter montrant un tracé asymptotique d'une phase de Bode.</caption>
<caption xml:lang="hu">A LogarithmPlotter fő nézete, amely egy aszimptotikus Bode-fázis ábrát mutat.</caption> <caption xml:lang="hu">A LogarithmPlotter fő nézete, amely egy aszimptotikus Bode-fázis ábrát mutat.</caption>
<caption xml:lang="no">Hovedvisning av LogarithmPlotter som viser et asymptotisk Bode-fasediagram.</caption> <caption xml:lang="no">Hovedvisning av LogarithmPlotter som viser et asymptotisk Bode-fasediagram.</caption>
<caption xml:lang="es">Vista principal de LogarithmPlotter mostrando un gráfico asintótico de una fase de Bode.</caption>
</screenshot> </screenshot>
<screenshot> <screenshot>
<image>https://apps.ad5001.eu/img/en/logarithmplotter/welcome.png?v=0.6</image> <image>https://apps.ad5001.eu/img/en/logarithmplotter/welcome.png?v=0.5</image>
<image xml:lang="de">https://apps.ad5001.eu/img/de/logarithmplotter/welcome.png?v=0.6</image> <image xml:lang="de">https://apps.ad5001.eu/img/de/logarithmplotter/welcome.png?v=0.5</image>
<image xml:lang="fr">https://apps.ad5001.eu/img/fr/logarithmplotter/welcome.png?v=0.6</image> <image xml:lang="fr">https://apps.ad5001.eu/img/fr/logarithmplotter/welcome.png?v=0.5</image>
<image xml:lang="hu">https://apps.ad5001.eu/img/hu/logarithmplotter/welcome.png?v=0.6</image> <image xml:lang="hu">https://apps.ad5001.eu/img/hu/logarithmplotter/welcome.png?v=0.5</image>
<image xml:lang="no">https://apps.ad5001.eu/img/no/logarithmplotter/welcome.png?v=0.6</image> <image xml:lang="no">https://apps.ad5001.eu/img/no/logarithmplotter/welcome.png?v=0.5</image>
<image xml:lang="es">https://apps.ad5001.eu/img/es/logarithmplotter/welcome.png?v=0.6</image>
<caption>LogarithmPlotter's welcome page.</caption> <caption>LogarithmPlotter's welcome page.</caption>
<caption xml:lang="de">LogarithmPlotter's Willkommensseite.</caption> <caption xml:lang="de">LogarithmPlotter's Willkommensseite.</caption>
<caption xml:lang="fr">Page d'accueil de LogarithmPlotter.</caption> <caption xml:lang="fr">Page d'accueil de LogarithmPlotter.</caption>
<caption xml:lang="hu">LogarithmPlotter üdvözlő oldala.</caption> <caption xml:lang="hu">LogarithmPlotter üdvözlő oldala.</caption>
<caption xml:lang="no">LogarithmPlotters velkomstside.</caption> <caption xml:lang="no">LogarithmPlotters velkomstside.</caption>
<caption xml:lang="es">Página de bienvenida de LogarithmPlotter.</caption>
</screenshot> </screenshot>
</screenshots> </screenshots>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info"> <mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type xmlns="http://www.freedesktop.org/standards/shared-mime-info" type="application/x-logarithm-plot"> <mime-type xmlns="http://www.freedesktop.org/standards/shared-mime-info" type="application/x-logarithm-plot">
<comment>Logarithmic Plot File</comment> <comment>Logarithm Plot File</comment>
<comment xml:lang="fr">Fichier Graphe Logarithmique</comment> <comment xml:lang="fr">Fichier Graphe Logarithmique</comment>
<icon name="application-x-logarithm-plot"/> <icon name="application-x-logarithm-plot"/>
<glob-deleteall/> <glob-deleteall/>

View file

@ -12,21 +12,28 @@ steps:
- git submodule update --init --recursive - git submodule update --init --recursive
- name: Build - name: Build
image: ad5001/ubuntu-pyside-xvfb:linux-6-latest-latex-node image: node:18-bookworm
commands: commands:
- cd common && npm install && cd .. - cd common && npm install && cd ..
- apt update
- apt install -y qtchooser qttools5-dev-tools
# Start building
- bash scripts/build.sh - bash scripts/build.sh
when:
event: [ push, tag ]
- name: Unit Tests - name: Unit Tests
image: ad5001/ubuntu-pyside-xvfb:linux-6-latest-latex-node image: ad5001/ubuntu-pyside-xvfb:linux-6-latest-latex
commands: commands:
- apt update
- apt install -y npm
- cd common && npm install -D && cd .. - cd common && npm install -D && cd ..
- xvfb-run bash scripts/run-tests.sh --no-rebuild - xvfb-run bash scripts/run-tests.sh --no-rebuild
when: when:
event: [ push, tag ] event: [ push, tag ]
- name: File Tests - name: File Tests
image: ad5001/ubuntu-pyside-xvfb:linux-6-latest-latex-node image: ad5001/ubuntu-pyside-xvfb:linux-6-latest-latex
commands: commands:
- xvfb-run python3 run.py --test-build --no-check-for-updates - xvfb-run python3 run.py --test-build --no-check-for-updates
- xvfb-run python3 run.py --test-build --no-check-for-updates ./ci/test1.lpf - xvfb-run python3 run.py --test-build --no-check-for-updates ./ci/test1.lpf

1539
common/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@
"name": "logarithmplotter", "name": "logarithmplotter",
"version": "0.6.0", "version": "0.6.0",
"description": "2D plotter software to make Bode plots, sequences and distribution functions.", "description": "2D plotter software to make Bode plots, sequences and distribution functions.",
"main": "src/index.mjs", "main": "LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/autoload.mjs",
"scripts": { "scripts": {
"build": "rollup --config rollup.config.mjs", "build": "rollup --config rollup.config.mjs",
"test": "c8 mocha test/**/*.mjs" "test": "c8 mocha test/**/*.mjs"
@ -24,12 +24,9 @@
}, },
"devDependencies": { "devDependencies": {
"@types/chai": "^5.0.0", "@types/chai": "^5.0.0",
"@types/chai-spies": "^1.0.6",
"@types/chai-as-promised": "^8.0.1",
"@types/mocha": "^10.0.8", "@types/mocha": "^10.0.8",
"chai": "^5.1.1", "chai": "^5.1.1",
"chai-as-promised": "^8.0.0", "chai-as-promised": "^8.0.0",
"chai-spies": "^1.1.0",
"esm": "^3.2.25", "esm": "^3.2.25",
"mocha": "^10.7.3" "mocha": "^10.7.3"
} }

View file

@ -1,116 +0,0 @@
/**
* 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/>.
*/
/**
* We do not inherit the DOM's Event, because not only the DOM part is unnecessary,
* but also because it does not exist within Qt environments.
*/
export class BaseEvent {
___name = ""
/**
* @property {string} name - Name of the event.
*/
constructor(name) {
this.___name = name
}
get name() {
return this.___name
}
}
/**
* Base class for all classes which can emit events.
*/
export class BaseEventEmitter {
static emits = []
/** @type {Record<string, Set<function>>} */
#listeners = {}
constructor() {
for(const eventType of this.constructor.emits) {
this.#listeners[eventType] = new Set()
}
}
/**
* Adds a listener to an event that can be emitted by this object.
*
* @param {string} eventType - Name of the event to listen to. Throws an error if this object does not emit this kind of event.
* @param {function(BaseEvent)} eventListener - The function to be called back when the event is emitted.
*/
on(eventType, eventListener) {
if(eventType.includes(" ")) // Listen to several different events with the same listener.
for(const type of eventType.split(" "))
this.on(type, eventListener)
else {
if(!this.constructor.emits.includes(eventType)) {
const className = this.constructor.name
const eventTypes = this.constructor.emits.join(", ")
throw new Error(`Cannot listen to unknown event ${eventType} in class ${className}. ${className} only emits: ${eventTypes}`)
}
if(!this.#listeners[eventType].has(eventListener))
this.#listeners[eventType].add(eventListener)
}
}
/**
* Removes a listener from an event that can be emitted by this object.
*
* @param {string} eventType - Name of the event that was listened to. Throws an error if this object does not emit this kind of event.
* @param {function(BaseEvent)} eventListener - The function previously registered as a listener.
* @returns {boolean} True if the listener was removed, false if it was not found.
*/
off(eventType, eventListener) {
if(eventType.includes(" ")) { // Unlisten to several different events with the same listener.
let found = false
for(const type of eventType.split(" "))
found ||= this.off(type, eventListener)
return found
} else {
if(!this.constructor.emits.includes(eventType)) {
const className = this.constructor.name
const eventTypes = this.constructor.emits.join(", ")
throw new Error(`Cannot listen to unknown event ${eventType} in class ${className}. ${className} only emits: ${eventTypes}`)
}
return this.#listeners[eventType].delete(eventListener)
}
}
/**
* Emits an event to all of its listeners.
*
* @param {BaseEvent} e
*/
emit(e) {
if(!(e instanceof BaseEvent))
throw new Error("Cannot emit non event object.")
if(!this.constructor.emits.includes(e.name)) {
const className = this.constructor.name
const eventTypes = this.constructor.emits.join(", ")
throw new Error(`Cannot emit event '${e.name}' from class ${className}. ${className} can only emit: ${eventTypes}`)
}
for(const listener of this.#listeners[e.name])
listener(e)
}
}

View file

@ -111,7 +111,7 @@ function simplify(tokens, unaryOps, binaryOps, ternaryOps, values) {
* In the given instructions, replaces variable by expr. * In the given instructions, replaces variable by expr.
* @param {Instruction[]} tokens * @param {Instruction[]} tokens
* @param {string} variable * @param {string} variable
* @param {ExprEvalExpression} expr * @param {number} expr
* @return {Instruction[]} * @return {Instruction[]}
*/ */
function substitute(tokens, variable, expr) { function substitute(tokens, variable, expr) {
@ -171,6 +171,9 @@ function evaluate(tokens, expr, values) {
nstack.push(n1 ? !!evaluate(n2, expr, values) : false) nstack.push(n1 ? !!evaluate(n2, expr, values) : false)
} else if(item.value === "or") { } else if(item.value === "or") {
nstack.push(n1 ? true : !!evaluate(n2, expr, values)) 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 { } else {
f = expr.binaryOps[item.value] f = expr.binaryOps[item.value]
nstack.push(f(resolveExpression(n1, values), resolveExpression(n2, values))) nstack.push(f(resolveExpression(n1, values), resolveExpression(n2, values)))
@ -487,6 +490,18 @@ export class ExprEvalExpression {
return evaluate(this.tokens, this, values) 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() { toString() {
return expressionToString(this.tokens, false) return expressionToString(this.tokens, false)
} }

View file

@ -47,7 +47,9 @@ const optionNameMap = {
"not": "logical", "not": "logical",
"?": "conditional", "?": "conditional",
":": "conditional", ":": "conditional",
//'=': 'assignment', // Disable assignment
"[": "array" "[": "array"
//'()=': 'fndef' // Diable function definition
} }
export class Parser { export class Parser {
@ -107,6 +109,7 @@ export class Parser {
and: Polyfill.andOperator, and: Polyfill.andOperator,
or: Polyfill.orOperator, or: Polyfill.orOperator,
"in": Polyfill.inOperator, "in": Polyfill.inOperator,
"=": Polyfill.setVar,
"[": Polyfill.arrayIndex "[": Polyfill.arrayIndex
} }
@ -120,13 +123,18 @@ export class Parser {
min: Polyfill.min, min: Polyfill.min,
max: Polyfill.max, max: Polyfill.max,
hypot: Math.hypot || Polyfill.hypot, hypot: Math.hypot || Polyfill.hypot,
pyt: Math.hypot || Polyfill.hypot, pyt: Math.hypot || Polyfill.hypot, // backward compat
pow: Math.pow, pow: Math.pow,
atan2: Math.atan2, atan2: Math.atan2,
"if": Polyfill.condition, "if": Polyfill.condition,
gamma: Polyfill.gamma, gamma: Polyfill.gamma,
"Γ": Polyfill.gamma, "Γ": Polyfill.gamma,
roundTo: Polyfill.roundTo, 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. // These constants will automatically be replaced the MOMENT they are parsed.
@ -151,6 +159,10 @@ export class Parser {
return new ExprEvalExpression(instr, this) return new ExprEvalExpression(instr, this)
} }
evaluate(expr, variables) {
return this.parse(expr).evaluate(variables)
}
isOperatorEnabled(op) { isOperatorEnabled(op) {
const optionName = optionNameMap.hasOwnProperty(op) ? optionNameMap[op] : op const optionName = optionNameMap.hasOwnProperty(op) ? optionNameMap[op] : op
const operators = this.options.operators || {} const operators = this.options.operators || {}

View file

@ -210,8 +210,9 @@ export function gamma(n) {
} }
export function stringOrArrayLength(s) { export function stringOrArrayLength(s) {
if(Array.isArray(s)) if(Array.isArray(s)) {
return s.length return s.length
}
return String(s).length return String(s).length
} }
@ -266,6 +267,11 @@ export function roundTo(value, exp) {
return +(value[0] + "e" + (value[1] ? (+value[1] + exp) : exp)) 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) { export function arrayIndex(array, index) {
return array[index | 0] return array[index | 0]
} }
@ -290,6 +296,58 @@ export function min(array) {
} }
} }
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) { export function sign(x) {
return ((x > 0) - (x < 0)) || +x return ((x > 0) - (x < 0)) || +x
} }

View file

@ -472,7 +472,7 @@ export class TokenStream {
this.current = this.newToken(TOP, "==") this.current = this.newToken(TOP, "==")
this.pos++ this.pos++
} else { } else {
return false this.current = this.newToken(TOP, c)
} }
} else if(c === "!") { } else if(c === "!") {
if(this.expression.charAt(this.pos + 1) === "=") { if(this.expression.charAt(this.pos + 1) === "=") {

View file

@ -1,4 +1,4 @@
/*! /**
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions. * LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 Ad5001 * Copyright (C) 2021-2024 Ad5001
* *
@ -64,20 +64,10 @@ function arrayFlatMap(callbackFn, thisArg) {
* @return {String} * @return {String}
*/ */
function stringReplaceAll(from, to) { function stringReplaceAll(from, to) {
return this.split(from).join(to) let str = this
} while(str.includes(from))
str = str.replace(from, to)
/** return str
* Returns the value of an element of the array at a given index.
* Accepts negative indexes.
* @this {Array|string}
* @param {number} index
* @return {*}
*/
function arrayAt(index) {
if(typeof index !== "number")
throw new Error(`${index} is not a number`)
return index >= 0 ? this[index] : this[this.length + index]
} }
@ -108,8 +98,8 @@ const polyfills = {
[String.prototype, "replaceAll", stringReplaceAll] [String.prototype, "replaceAll", stringReplaceAll]
], ],
2022: [ 2022: [
[Array.prototype, "at", arrayAt], [Array.prototype, "at", notPolyfilled("Array.prototype.at")],
[String.prototype, "at", arrayAt], [String.prototype, "at", notPolyfilled("String.prototype.at")],
[Object, "hasOwn", notPolyfilled("Object.hasOwn")] [Object, "hasOwn", notPolyfilled("Object.hasOwn")]
], ],
2023: [ 2023: [

View file

@ -23,25 +23,25 @@ import { Expression } from "../math/index.mjs"
import Latex from "./latex.mjs" import Latex from "./latex.mjs"
import Objects from "./objects.mjs" import Objects from "./objects.mjs"
import History from "./history.mjs" import History from "./history.mjs"
import Settings from "./settings.mjs"
class CanvasAPI extends Module { class CanvasAPI extends Module {
/** @type {CanvasInterface} */
#canvas = null
/** @type {CanvasRenderingContext2D} */
#ctx = null
/** Lock to prevent asynchronous stuff from printing stuff that is outdated. */
#redrawCount = 0
/** @type {{show(string, string, string)}} */
#drawingErrorDialog = null
constructor() { constructor() {
super("Canvas", { super("Canvas", {
canvas: CanvasInterface, canvas: CanvasInterface,
drawingErrorDialog: DialogInterface 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}>} * @type {Object.<string, {expression: Expression, value: number, maxDraw: number}>}
@ -67,18 +67,18 @@ class CanvasAPI extends Module {
*/ */
initialize({ canvas, drawingErrorDialog }) { initialize({ canvas, drawingErrorDialog }) {
super.initialize({ canvas, drawingErrorDialog }) super.initialize({ canvas, drawingErrorDialog })
this.#canvas = canvas this._canvas = canvas
this.#drawingErrorDialog = drawingErrorDialog this._drawingErrorDialog = drawingErrorDialog
} }
get width() { get width() {
if(!this.initialized) throw new Error("Attempting width before initialize!") if(!this.initialized) throw new Error("Attempting width before initialize!")
return this.#canvas.width return this._canvas.width
} }
get height() { get height() {
if(!this.initialized) throw new Error("Attempting height before initialize!") if(!this.initialized) throw new Error("Attempting height before initialize!")
return this.#canvas.height return this._canvas.height
} }
/** /**
@ -87,7 +87,7 @@ class CanvasAPI extends Module {
*/ */
get xmin() { get xmin() {
if(!this.initialized) throw new Error("Attempting xmin before initialize!") if(!this.initialized) throw new Error("Attempting xmin before initialize!")
return Settings.xmin return this._canvas.xmin
} }
/** /**
@ -96,7 +96,7 @@ class CanvasAPI extends Module {
*/ */
get xzoom() { get xzoom() {
if(!this.initialized) throw new Error("Attempting xzoom before initialize!") if(!this.initialized) throw new Error("Attempting xzoom before initialize!")
return Settings.xzoom return this._canvas.xzoom
} }
/** /**
@ -105,7 +105,7 @@ class CanvasAPI extends Module {
*/ */
get ymax() { get ymax() {
if(!this.initialized) throw new Error("Attempting ymax before initialize!") if(!this.initialized) throw new Error("Attempting ymax before initialize!")
return Settings.ymax return this._canvas.ymax
} }
/** /**
@ -114,7 +114,7 @@ class CanvasAPI extends Module {
*/ */
get yzoom() { get yzoom() {
if(!this.initialized) throw new Error("Attempting yzoom before initialize!") if(!this.initialized) throw new Error("Attempting yzoom before initialize!")
return Settings.yzoom return this._canvas.yzoom
} }
/** /**
@ -123,7 +123,7 @@ class CanvasAPI extends Module {
*/ */
get xlabel() { get xlabel() {
if(!this.initialized) throw new Error("Attempting xlabel before initialize!") if(!this.initialized) throw new Error("Attempting xlabel before initialize!")
return Settings.xlabel return this._canvas.xlabel
} }
/** /**
@ -132,7 +132,7 @@ class CanvasAPI extends Module {
*/ */
get ylabel() { get ylabel() {
if(!this.initialized) throw new Error("Attempting ylabel before initialize!") if(!this.initialized) throw new Error("Attempting ylabel before initialize!")
return Settings.ylabel return this._canvas.ylabel
} }
/** /**
@ -141,7 +141,7 @@ class CanvasAPI extends Module {
*/ */
get linewidth() { get linewidth() {
if(!this.initialized) throw new Error("Attempting linewidth before initialize!") if(!this.initialized) throw new Error("Attempting linewidth before initialize!")
return Settings.linewidth return this._canvas.linewidth
} }
/** /**
@ -150,7 +150,7 @@ class CanvasAPI extends Module {
*/ */
get textsize() { get textsize() {
if(!this.initialized) throw new Error("Attempting textsize before initialize!") if(!this.initialized) throw new Error("Attempting textsize before initialize!")
return Settings.textsize return this._canvas.textsize
} }
/** /**
@ -159,7 +159,7 @@ class CanvasAPI extends Module {
*/ */
get logscalex() { get logscalex() {
if(!this.initialized) throw new Error("Attempting logscalex before initialize!") if(!this.initialized) throw new Error("Attempting logscalex before initialize!")
return Settings.logscalex return this._canvas.logscalex
} }
/** /**
@ -168,7 +168,7 @@ class CanvasAPI extends Module {
*/ */
get showxgrad() { get showxgrad() {
if(!this.initialized) throw new Error("Attempting showxgrad before initialize!") if(!this.initialized) throw new Error("Attempting showxgrad before initialize!")
return Settings.showxgrad return this._canvas.showxgrad
} }
/** /**
@ -177,7 +177,7 @@ class CanvasAPI extends Module {
*/ */
get showygrad() { get showygrad() {
if(!this.initialized) throw new Error("Attempting showygrad before initialize!") if(!this.initialized) throw new Error("Attempting showygrad before initialize!")
return Settings.showygrad return this._canvas.showygrad
} }
/** /**
@ -201,7 +201,7 @@ class CanvasAPI extends Module {
requestPaint() { requestPaint() {
if(!this.initialized) throw new Error("Attempting requestPaint before initialize!") if(!this.initialized) throw new Error("Attempting requestPaint before initialize!")
this.#canvas.requestPaint() this._canvas.requestPaint()
} }
/** /**
@ -209,18 +209,17 @@ class CanvasAPI extends Module {
*/ */
redraw() { redraw() {
if(!this.initialized) throw new Error("Attempting redraw before initialize!") if(!this.initialized) throw new Error("Attempting redraw before initialize!")
this.#redrawCount = (this.#redrawCount + 1) % 10000 this._ctx = this._canvas.getContext("2d")
this.#ctx = this.#canvas.getContext("2d")
this._computeAxes() this._computeAxes()
this._reset() this._reset()
this._drawGrid() this._drawGrid()
this._drawAxes() this._drawAxes()
this._drawLabels() this._drawLabels()
this.#ctx.lineWidth = this.linewidth this._ctx.lineWidth = this.linewidth
for(let objType in Objects.currentObjects) { for(let objType in Objects.currentObjects) {
for(let obj of Objects.currentObjects[objType]) { for(let obj of Objects.currentObjects[objType]) {
this.#ctx.strokeStyle = obj.color this._ctx.strokeStyle = obj.color
this.#ctx.fillStyle = obj.color this._ctx.fillStyle = obj.color
if(obj.visible) if(obj.visible)
try { try {
obj.draw(this) obj.draw(this)
@ -228,12 +227,12 @@ class CanvasAPI extends Module {
// Drawing throws an error. Generally, it's due to a new modification (or the opening of a file) // Drawing throws an error. Generally, it's due to a new modification (or the opening of a file)
console.error(e) console.error(e)
console.log(e.stack) console.log(e.stack)
this.#drawingErrorDialog.show(objType, obj.name, e.message) this._drawingErrorDialog.show(objType, obj.name, e.message)
History.undo() History.undo()
} }
} }
} }
this.#ctx.lineWidth = 1 this._ctx.lineWidth = 1
} }
/** /**
@ -241,9 +240,9 @@ class CanvasAPI extends Module {
* @private * @private
*/ */
_computeAxes() { _computeAxes() {
let exprY = new Expression(`x*(${Settings.yaxisstep})`) let exprY = new Expression(`x*(${this._canvas.yaxisstep})`)
let y1 = exprY.execute(1) let y1 = exprY.execute(1)
let exprX = new Expression(`x*(${Settings.xaxisstep})`) let exprX = new Expression(`x*(${this._canvas.xaxisstep})`)
let x1 = exprX.execute(1) let x1 = exprX.execute(1)
this.axesSteps = { this.axesSteps = {
x: { x: {
@ -265,10 +264,10 @@ class CanvasAPI extends Module {
*/ */
_reset() { _reset() {
// Reset // Reset
this.#ctx.fillStyle = "#FFFFFF" this._ctx.fillStyle = "#FFFFFF"
this.#ctx.strokeStyle = "#000000" this._ctx.strokeStyle = "#000000"
this.#ctx.font = `${this.textsize}px sans-serif` this._ctx.font = `${this.textsize}px sans-serif`
this.#ctx.fillRect(0, 0, this.width, this.height) this._ctx.fillRect(0, 0, this.width, this.height)
} }
/** /**
@ -276,7 +275,7 @@ class CanvasAPI extends Module {
* @private * @private
*/ */
_drawGrid() { _drawGrid() {
this.#ctx.strokeStyle = "#C0C0C0" this._ctx.strokeStyle = "#C0C0C0"
if(this.logscalex) { if(this.logscalex) {
for(let xpow = -this.maxgradx; xpow <= this.maxgradx; xpow++) { for(let xpow = -this.maxgradx; xpow <= this.maxgradx; xpow++) {
for(let xmulti = 1; xmulti < 10; xmulti++) { for(let xmulti = 1; xmulti < 10; xmulti++) {
@ -300,7 +299,7 @@ class CanvasAPI extends Module {
* @private * @private
*/ */
_drawAxes() { _drawAxes() {
this.#ctx.strokeStyle = "#000000" this._ctx.strokeStyle = "#000000"
let axisypos = this.logscalex ? 1 : 0 let axisypos = this.logscalex ? 1 : 0
this.drawXLine(axisypos) this.drawXLine(axisypos)
this.drawYLine(0) this.drawYLine(0)
@ -321,19 +320,19 @@ class CanvasAPI extends Module {
let axisypx = this.x2px(this.logscalex ? 1 : 0) // X coordinate of Y axis let axisypx = this.x2px(this.logscalex ? 1 : 0) // X coordinate of Y axis
let axisxpx = this.y2px(0) // Y coordinate of X axis let axisxpx = this.y2px(0) // Y coordinate of X axis
// Labels // Labels
this.#ctx.fillStyle = "#000000" this._ctx.fillStyle = "#000000"
this.#ctx.font = `${this.textsize}px sans-serif` this._ctx.font = `${this.textsize}px sans-serif`
this.#ctx.fillText(this.ylabel, axisypx + 10, 24) this._ctx.fillText(this.ylabel, axisypx + 10, 24)
let textWidth = this.#ctx.measureText(this.xlabel).width let textWidth = this._ctx.measureText(this.xlabel).width
this.#ctx.fillText(this.xlabel, this.width - 14 - textWidth, axisxpx - 5) this._ctx.fillText(this.xlabel, this.width - 14 - textWidth, axisxpx - 5)
// Axis graduation labels // Axis graduation labels
this.#ctx.font = `${this.textsize - 4}px sans-serif` this._ctx.font = `${this.textsize - 4}px sans-serif`
let txtMinus = this.#ctx.measureText("-").width let txtMinus = this._ctx.measureText("-").width
if(this.showxgrad) { if(this.showxgrad) {
if(this.logscalex) { if(this.logscalex) {
for(let xpow = -this.maxgradx; xpow <= this.maxgradx; xpow += 1) { for(let xpow = -this.maxgradx; xpow <= this.maxgradx; xpow += 1) {
textWidth = this.#ctx.measureText("10" + textsup(xpow)).width textWidth = this._ctx.measureText("10" + textsup(xpow)).width
if(xpow !== 0) if(xpow !== 0)
this.drawVisibleText("10" + textsup(xpow), this.x2px(Math.pow(10, xpow)) - textWidth / 2, axisxpx + 16 + (6 * (xpow === 1))) this.drawVisibleText("10" + textsup(xpow), this.x2px(Math.pow(10, xpow)) - textWidth / 2, axisxpx + 16 + (6 * (xpow === 1)))
} }
@ -351,13 +350,13 @@ class CanvasAPI extends Module {
for(let y = 0; y < this.axesSteps.y.maxDraw; y += 1) { for(let y = 0; y < this.axesSteps.y.maxDraw; y += 1) {
let drawY = y * this.axesSteps.y.value let drawY = y * this.axesSteps.y.value
let txtY = this.axesSteps.y.expression.simplify(y).toString().replace(/^\((.+)\)$/, "$1") let txtY = this.axesSteps.y.expression.simplify(y).toString().replace(/^\((.+)\)$/, "$1")
textWidth = this.#ctx.measureText(txtY).width textWidth = this._ctx.measureText(txtY).width
this.drawVisibleText(txtY, axisypx - 6 - textWidth, this.y2px(drawY) + 4 + (10 * (y === 0))) this.drawVisibleText(txtY, axisypx - 6 - textWidth, this.y2px(drawY) + 4 + (10 * (y === 0)))
if(y !== 0) if(y !== 0)
this.drawVisibleText("-" + txtY, axisypx - 6 - textWidth - txtMinus, this.y2px(-drawY) + 4) this.drawVisibleText("-" + txtY, axisypx - 6 - textWidth - txtMinus, this.y2px(-drawY) + 4)
} }
} }
this.#ctx.fillStyle = "#FFFFFF" this._ctx.fillStyle = "#FFFFFF"
} }
// //
@ -395,7 +394,7 @@ class CanvasAPI extends Module {
drawVisibleText(text, x, y) { drawVisibleText(text, x, y) {
if(x > 0 && x < this.width && y > 0 && y < this.height) { if(x > 0 && x < this.width && y > 0 && y < this.height) {
text.toString().split("\n").forEach((txt, i) => { text.toString().split("\n").forEach((txt, i) => {
this.#ctx.fillText(txt, x, y + (this.textsize * i)) this._ctx.fillText(txt, x, y + (this.textsize * i))
}) })
} }
} }
@ -410,8 +409,8 @@ class CanvasAPI extends Module {
* @param {number} height * @param {number} height
*/ */
drawVisibleImage(image, x, y, width, height) { drawVisibleImage(image, x, y, width, height) {
this.#canvas.markDirty(Qt.rect(x, y, width, height)) this._canvas.markDirty(Qt.rect(x, y, width, height))
this.#ctx.drawImage(image, x, y, width, height) this._ctx.drawImage(image, x, y, width, height)
} }
/** /**
@ -425,7 +424,7 @@ class CanvasAPI extends Module {
let defaultHeight = this.textsize * 1.2 // Approximate but good enough! let defaultHeight = this.textsize * 1.2 // Approximate but good enough!
for(let txt of text.split("\n")) { for(let txt of text.split("\n")) {
theight += defaultHeight theight += defaultHeight
if(this.#ctx.measureText(txt).width > twidth) twidth = this.#ctx.measureText(txt).width if(this._ctx.measureText(txt).width > twidth) twidth = this._ctx.measureText(txt).width
} }
return { "width": twidth, "height": theight } return { "width": twidth, "height": theight }
} }
@ -495,10 +494,10 @@ class CanvasAPI extends Module {
* @param {number} y2 * @param {number} y2
*/ */
drawLine(x1, y1, x2, y2) { drawLine(x1, y1, x2, y2) {
this.#ctx.beginPath() this._ctx.beginPath()
this.#ctx.moveTo(x1, y1) this._ctx.moveTo(x1, y1)
this.#ctx.lineTo(x2, y2) this._ctx.lineTo(x2, y2)
this.#ctx.stroke() this._ctx.stroke()
} }
/** /**
@ -510,9 +509,9 @@ class CanvasAPI extends Module {
* @param {number} dashPxSize * @param {number} dashPxSize
*/ */
drawDashedLine(x1, y1, x2, y2, dashPxSize = 6) { drawDashedLine(x1, y1, x2, y2, dashPxSize = 6) {
this.#ctx.setLineDash([dashPxSize / 2, dashPxSize]) this._ctx.setLineDash([dashPxSize / 2, dashPxSize])
this.drawLine(x1, y1, x2, y2) this.drawLine(x1, y1, x2, y2)
this.#ctx.setLineDash([]) this._ctx.setLineDash([])
} }
/** /**
@ -522,22 +521,14 @@ class CanvasAPI extends Module {
* @param {function(LatexRenderResult|{width: number, height: number, source: string})} callback * @param {function(LatexRenderResult|{width: number, height: number, source: string})} callback
*/ */
renderLatexImage(ltxText, color, callback) { renderLatexImage(ltxText, color, callback) {
const currentRedrawCount = this.#redrawCount
const onRendered = (imgData) => { const onRendered = (imgData) => {
if(!this.#canvas.isImageLoaded(imgData.source) && !this.#canvas.isImageLoading(imgData.source)) { if(!this._canvas.isImageLoaded(imgData.source) && !this._canvas.isImageLoading(imgData.source)) {
// Wait until the image is loaded to callback. // Wait until the image is loaded to callback.
this.#canvas.loadImageAsync(imgData.source).then(() => { this._canvas.loadImage(imgData.source)
if(this.#redrawCount === currentRedrawCount) this._canvas.imageLoaders[imgData.source] = [callback, imgData]
callback(imgData)
else
console.log("1. Discard render of", imgData.source, this.#redrawCount, currentRedrawCount)
})
} else { } else {
// Callback directly // Callback directly
if(this.#redrawCount === currentRedrawCount)
callback(imgData) callback(imgData)
else
console.log("2. Discard render of", imgData.source, this.#redrawCount, currentRedrawCount)
} }
} }
const prerendered = Latex.findPrerendered(ltxText, this.textsize, color) const prerendered = Latex.findPrerendered(ltxText, this.textsize, color)
@ -552,11 +543,11 @@ class CanvasAPI extends Module {
// //
get font() { get font() {
return this.#ctx.font return this._ctx.font
} }
set font(value) { set font(value) {
return this.#ctx.font = value return this._ctx.font = value
} }
/** /**
@ -569,9 +560,9 @@ class CanvasAPI extends Module {
* @param {boolean} counterclockwise * @param {boolean} counterclockwise
*/ */
arc(x, y, radius, startAngle, endAngle, counterclockwise = false) { arc(x, y, radius, startAngle, endAngle, counterclockwise = false) {
this.#ctx.beginPath() this._ctx.beginPath()
this.#ctx.arc(x, y, radius, startAngle, endAngle, counterclockwise) this._ctx.arc(x, y, radius, startAngle, endAngle, counterclockwise)
this.#ctx.stroke() this._ctx.stroke()
} }
/** /**
@ -581,9 +572,9 @@ class CanvasAPI extends Module {
* @param {number} radius * @param {number} radius
*/ */
disc(x, y, radius) { disc(x, y, radius) {
this.#ctx.beginPath() this._ctx.beginPath()
this.#ctx.arc(x, y, radius, 0, 2 * Math.PI) this._ctx.arc(x, y, radius, 0, 2 * Math.PI)
this.#ctx.fill() this._ctx.fill()
} }
/** /**
@ -594,7 +585,7 @@ class CanvasAPI extends Module {
* @param {number} h * @param {number} h
*/ */
fillRect(x, y, w, h) { fillRect(x, y, w, h) {
this.#ctx.fillRect(x, y, w, h) this._ctx.fillRect(x, y, w, h)
} }
} }

View file

@ -17,7 +17,6 @@
*/ */
import { Interface } from "./interface.mjs" import { Interface } from "./interface.mjs"
import { BaseEventEmitter } from "../events.mjs"
// Define Modules interface before they are imported. // Define Modules interface before they are imported.
globalThis.Modules = globalThis.Modules || {} globalThis.Modules = globalThis.Modules || {}
@ -25,13 +24,7 @@ globalThis.Modules = globalThis.Modules || {}
/** /**
* Base class for global APIs in runtime. * Base class for global APIs in runtime.
*/ */
export class Module extends BaseEventEmitter { export class Module {
/** @type {string} */
#name
/** @type {Object.<string, (Interface|string|number|boolean)>} */
#initializationParameters
/** @type {boolean} */
#initialized = false
/** /**
* *
@ -39,18 +32,11 @@ export class Module extends BaseEventEmitter {
* @param {Object.<string, (Interface|string|number|boolean)>} initializationParameters - List of parameters for the initialize function. * @param {Object.<string, (Interface|string|number|boolean)>} initializationParameters - List of parameters for the initialize function.
*/ */
constructor(name, initializationParameters = {}) { constructor(name, initializationParameters = {}) {
super()
console.log(`Loading module ${name}...`) console.log(`Loading module ${name}...`)
this.#name = name this.__name = name
this.#initializationParameters = initializationParameters this.__initializationParameters = initializationParameters
} this.initialized = false
get name() {
return this.#name;
}
get initialized() {
return this.#initialized
} }
/** /**
@ -58,17 +44,17 @@ export class Module extends BaseEventEmitter {
* @param {Object.<string, any>} options * @param {Object.<string, any>} options
*/ */
initialize(options) { initialize(options) {
if(this.#initialized) if(this.initialized)
throw new Error(`Cannot reinitialize module ${this.#name}.`) throw new Error(`Cannot reinitialize module ${this.__name}.`)
console.log(`Initializing ${this.#name}...`) console.log(`Initializing ${this.__name}...`)
for(const [name, value] of Object.entries(this.#initializationParameters)) { for(const [name, value] of Object.entries(this.__initializationParameters)) {
if(!options.hasOwnProperty(name)) if(!options.hasOwnProperty(name))
throw new Error(`Option '${name}' of initialize of module ${this.#name} does not exist.`) throw new Error(`Option '${name}' of initialize of module ${this.__name} does not exist.`)
if(typeof value === "function" && value.prototype instanceof Interface) if(typeof value === "function" && value.prototype instanceof Interface)
Interface.checkImplementation(value, options[name]) Interface.check_implementation(value, options[name])
else if(typeof value !== typeof 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]}).`) throw new Error(`Option '${name}' of initialize of module ${this.__name} is not a '${value}' (${typeof options[name]}).`)
} }
this.#initialized = true this.initialized = true
} }
} }

View file

@ -19,7 +19,7 @@
import { Module } from "./common.mjs" import { Module } from "./common.mjs"
import { Parser } from "../lib/expr-eval/parser.mjs" import { Parser } from "../lib/expr-eval/parser.mjs"
const EVAL_VARIABLES = { const evalVariables = {
// Variables not provided by expr-eval.js, needs to be provided manually // Variables not provided by expr-eval.js, needs to be provided manually
"pi": Math.PI, "pi": Math.PI,
"PI": Math.PI, "PI": Math.PI,
@ -35,17 +35,15 @@ const EVAL_VARIABLES = {
} }
class ExprParserAPI extends Module { class ExprParserAPI extends Module {
#parser = new Parser()
constructor() { constructor() {
super("ExprParser") super("ExprParser")
this.currentVars = {} this.currentVars = {}
this.#parser = new Parser() this._parser = new Parser()
this.#parser.consts = Object.assign({}, this.#parser.consts, EVAL_VARIABLES) this._parser.consts = Object.assign({}, this._parser.consts, evalVariables)
this.#parser.functions.integral = this.integral.bind(this) this._parser.functions.integral = this.integral.bind(this)
this.#parser.functions.derivative = this.derivative.bind(this) this._parser.functions.derivative = this.derivative.bind(this)
} }
/** /**
@ -70,7 +68,7 @@ class ExprParserAPI extends Module {
[f, variable] = args [f, variable] = args
if(typeof f !== "string" || typeof variable !== "string") if(typeof f !== "string" || typeof variable !== "string")
throw EvalError(qsTranslate("usage", "Usage:\n%1").arg(usage2)) throw EvalError(qsTranslate("usage", "Usage:\n%1").arg(usage2))
f = this.#parser.parse(f).toJSFunction(variable, this.currentVars) f = this._parser.parse(f).toJSFunction(variable, this.currentVars)
} else } else
throw EvalError(qsTranslate("usage", "Usage:\n%1\n%2").arg(usage1).arg(usage2)) throw EvalError(qsTranslate("usage", "Usage:\n%1\n%2").arg(usage1).arg(usage2))
return f return f
@ -81,14 +79,14 @@ class ExprParserAPI extends Module {
* @returns {ExprEvalExpression} * @returns {ExprEvalExpression}
*/ */
parse(expression) { parse(expression) {
return this.#parser.parse(expression) return this._parser.parse(expression)
} }
integral(a = null, b = null, ...args) { integral(a, b, ...args) {
let usage1 = qsTranslate("usage", "integral(<from: number>, <to: number>, <f: ExecutableObject>)") 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 usage2 = qsTranslate("usage", "integral(<from: number>, <to: number>, <f: string>, <variable: string>)")
let f = this.parseArgumentsForFunction(args, usage1, usage2) let f = this.parseArgumentsForFunction(args, usage1, usage2)
if(typeof a !== "number" || typeof b !== "number") if(a == null || b == null)
throw EvalError(qsTranslate("usage", "Usage:\n%1\n%2").arg(usage1).arg(usage2)) throw EvalError(qsTranslate("usage", "Usage:\n%1\n%2").arg(usage1).arg(usage2))
// https://en.wikipedia.org/wiki/Simpson%27s_rule // https://en.wikipedia.org/wiki/Simpson%27s_rule
@ -101,10 +99,10 @@ class ExprParserAPI extends Module {
let usage2 = qsTranslate("usage", "derivative(<f: string>, <variable: string>, <x: number>)") let usage2 = qsTranslate("usage", "derivative(<f: string>, <variable: string>, <x: number>)")
let x = args.pop() let x = args.pop()
let f = this.parseArgumentsForFunction(args, usage1, usage2) let f = this.parseArgumentsForFunction(args, usage1, usage2)
if(typeof x !== "number") if(x == null)
throw EvalError(qsTranslate("usage", "Usage:\n%1\n%2").arg(usage1).arg(usage2)) throw EvalError(qsTranslate("usage", "Usage:\n%1\n%2").arg(usage1).arg(usage2))
let derivative_precision = 1e-8 let derivative_precision = x / 10
return (f(x + derivative_precision / 2) - f(x - derivative_precision / 2)) / derivative_precision return (f(x + derivative_precision / 2) - f(x - derivative_precision / 2)) / derivative_precision
} }
} }

View file

@ -17,164 +17,60 @@
*/ */
import { Module } from "./common.mjs" import { Module } from "./common.mjs"
import { HelperInterface, NUMBER, STRING } from "./interface.mjs" import { HistoryInterface, NUMBER, STRING } from "./interface.mjs"
import { BaseEvent } from "../events.mjs"
import { Action, Actions } from "../history/index.mjs"
class ClearedEvent extends BaseEvent {
constructor() {
super("cleared")
}
}
class LoadedEvent extends BaseEvent {
constructor() {
super("loaded")
}
}
class AddedEvent extends BaseEvent {
constructor(action) {
super("added")
this.action = action
}
}
class UndoneEvent extends BaseEvent {
constructor(action) {
super("undone")
this.undid = action
}
}
class RedoneEvent extends BaseEvent {
constructor(action) {
super("redone")
this.redid = action
}
}
class HistoryAPI extends Module { class HistoryAPI extends Module {
static emits = ["cleared", "loaded", "added", "undone", "redone"]
#helper
constructor() { constructor() {
super("History", { super("History", {
helper: HelperInterface, historyObj: HistoryInterface,
themeTextColor: STRING, themeTextColor: STRING,
imageDepth: NUMBER, imageDepth: NUMBER,
fontSize: NUMBER fontSize: NUMBER
}) })
// History QML object // History QML object
/** @type {Action[]} */ this.history = null
this.undoStack = []
/** @type {Action[]} */
this.redoStack = []
this.themeTextColor = "#FF0000" this.themeTextColor = "#FF0000"
this.imageDepth = 2 this.imageDepth = 2
this.fontSize = 28 this.fontSize = 28
} }
/** initialize({ historyObj, themeTextColor, imageDepth, fontSize }) {
* @param {HelperInterface} historyObj super.initialize({ historyObj, themeTextColor, imageDepth, fontSize })
* @param {string} themeTextColor this.history = historyObj
* @param {number} imageDepth
* @param {number} fontSize
*/
initialize({ helper, themeTextColor, imageDepth, fontSize }) {
super.initialize({ helper, themeTextColor, imageDepth, fontSize })
this.#helper = helper
this.themeTextColor = themeTextColor this.themeTextColor = themeTextColor
this.imageDepth = imageDepth this.imageDepth = imageDepth
this.fontSize = fontSize this.fontSize = fontSize
} }
/**
* Undoes the Action at the top of the undo stack and pushes it to the top of the redo stack.
*/
undo() { undo() {
if(!this.initialized) throw new Error("Attempting undo before initialize!") if(!this.initialized) throw new Error("Attempting undo before initialize!")
if(this.undoStack.length > 0) { this.history.undo()
const action = this.undoStack.pop()
action.undo()
this.redoStack.push(action)
this.emit(new UndoneEvent(action))
}
} }
/**
* Redoes the Action at the top of the redo stack and pushes it to the top of the undo stack.
*/
redo() { redo() {
if(!this.initialized) throw new Error("Attempting redo before initialize!") if(!this.initialized) throw new Error("Attempting redo before initialize!")
if(this.redoStack.length > 0) { this.history.redo()
const action = this.redoStack.pop()
action.redo()
this.undoStack.push(action)
this.emit(new RedoneEvent(action))
}
} }
/**
* Clears both undo and redo stacks completely.
*/
clear() { clear() {
if(!this.initialized) throw new Error("Attempting clear before initialize!") if(!this.initialized) throw new Error("Attempting clear before initialize!")
this.undoStack = [] this.history.clear()
this.redoStack = []
this.emit(new ClearedEvent())
} }
/**
* Adds an instance of HistoryLib.Action to history.
* @param action
*/
addToHistory(action) { addToHistory(action) {
if(!this.initialized) throw new Error("Attempting addToHistory before initialize!") if(!this.initialized) throw new Error("Attempting addToHistory before initialize!")
if(action instanceof Action) { this.history.addToHistory(action)
console.log("Added new entry to history: " + action.getReadableString())
this.undoStack.push(action)
if(this.#helper.getSetting("reset_redo_stack"))
this.redoStack = []
this.emit(new AddedEvent(action))
}
} }
/** unserialize(...data) {
* Unserializes both the undo stack and redo stack from serialized content.
* @param {[string, any[]][]} undoSt
* @param {[string, any[]][]} redoSt
*/
unserialize(undoSt, redoSt) {
if(!this.initialized) throw new Error("Attempting unserialize before initialize!") if(!this.initialized) throw new Error("Attempting unserialize before initialize!")
this.clear() this.history.unserialize(...data)
for(const [name, args] of undoSt)
this.undoStack.push(
new Actions[name](...args)
)
for(const [name, args] of redoSt)
this.redoStack.push(
new Actions[name](...args)
)
this.emit(new LoadedEvent())
} }
/**
* Serializes history into JSON-able content.
* @return {[[string, any[]], [string, any[]]]}
*/
serialize() { serialize() {
if(!this.initialized) throw new Error("Attempting serialize before initialize!") if(!this.initialized) throw new Error("Attempting serialize before initialize!")
let undoSt = [], redoSt = []; return this.history.serialize()
for(const action of this.undoStack)
undoSt.push([ action.type(), action.export() ])
for(const action of this.redoStack)
redoSt.push([ action.type(), action.export() ])
return [undoSt, redoSt]
} }
} }

View file

@ -17,7 +17,6 @@
*/ */
import Objects from "./objects.mjs" import Objects from "./objects.mjs"
import Settings from "./settings.mjs"
import ExprParser from "./expreval.mjs" import ExprParser from "./expreval.mjs"
import Latex from "./latex.mjs" import Latex from "./latex.mjs"
import History from "./history.mjs" import History from "./history.mjs"
@ -27,7 +26,6 @@ import Preferences from "./preferences.mjs"
export default { export default {
Objects, Objects,
Settings,
ExprParser, ExprParser,
Latex, Latex,
History, History,

View file

@ -1,4 +1,4 @@
/** /*!
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions. * LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* *
* @author Ad5001 <mail@ad5001.eu> * @author Ad5001 <mail@ad5001.eu>
@ -35,8 +35,9 @@ export class Interface {
* Throws an error if the implementation does not conform to the interface. * Throws an error if the implementation does not conform to the interface.
* @param {typeof Interface} interface_ * @param {typeof Interface} interface_
* @param {object} classToCheck * @param {object} classToCheck
* @return {boolean}
*/ */
static checkImplementation(interface_, classToCheck) { static check_implementation(interface_, classToCheck) {
const properties = new interface_() const properties = new interface_()
const interfaceName = interface_.name const interfaceName = interface_.name
const toCheckName = classToCheck.constructor.name const toCheckName = classToCheck.constructor.name
@ -51,7 +52,7 @@ export class Interface {
else if((typeof value) === "object") else if((typeof value) === "object")
// Test type of object. // Test type of object.
if(value instanceof Interface) if(value instanceof Interface)
Interface.checkImplementation(value, classToCheck[property]) Interface.check_implementation(value, classToCheck[property])
else if(value.prototype && !(classToCheck[property] instanceof value)) else if(value.prototype && !(classToCheck[property] instanceof value))
throw new Error(`Property '${property}' of ${interfaceName} implementation ${toCheckName} is not '${value.constructor.name}'.`) throw new Error(`Property '${property}' of ${interfaceName} implementation ${toCheckName} is not '${value.constructor.name}'.`)
} }
@ -59,13 +60,32 @@ export class Interface {
} }
export class CanvasInterface extends Interface { 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} */ /** @type {function(string): CanvasRenderingContext2D} */
getContext = FUNCTION getContext = FUNCTION
/** @type {function(rect)} */ /** @type {function(rect)} */
markDirty = FUNCTION markDirty = FUNCTION
/** @type {function(string): Promise} */ /** @type {function(string)} */
loadImageAsync = FUNCTION loadImage = FUNCTION
/** @type {function(string)} */ /** @type {function(string)} */
isImageLoading = FUNCTION isImageLoading = FUNCTION
/** @type {function(string)} */ /** @type {function(string)} */
@ -77,28 +97,30 @@ export class CanvasInterface extends Interface {
export class RootInterface extends Interface { export class RootInterface extends Interface {
width = NUMBER width = NUMBER
height = NUMBER height = NUMBER
updateObjectsLists = FUNCTION
} }
export class DialogInterface extends Interface { export class DialogInterface extends Interface {
show = FUNCTION show = FUNCTION
} }
export class HistoryInterface extends Interface {
undo = FUNCTION
redo = FUNCTION
clear = FUNCTION
addToHistory = FUNCTION
unserialize = FUNCTION
serialize = FUNCTION
}
export class LatexInterface extends Interface { export class LatexInterface extends Interface {
supportsAsyncRender = BOOLEAN
/** /**
* @param {string} markup - LaTeX markup to render * @param {string} markup - LaTeX markup to render
* @param {number} fontSize - Font size (in pt) to render * @param {number} fontSize - Font size (in pt) to render
* @param {string} color - Color of the text to render * @param {string} color - Color of the text to render
* @returns {string} - Comma separated data of the image (source, width, height) * @returns {string} - Comma separated data of the image (source, width, height)
*/ */
renderSync = FUNCTION 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 {Promise<string>} - Comma separated data of the image (source, width, height)
*/
renderAsync = FUNCTION
/** /**
* @param {string} markup - LaTeX markup to render * @param {string} markup - LaTeX markup to render
* @param {number} fontSize - Font size (in pt) to render * @param {number} fontSize - Font size (in pt) to render
@ -117,13 +139,37 @@ export class HelperInterface extends Interface {
/** /**
* Gets a setting from the config * Gets a setting from the config
* @param {string} settingName - Setting (and its dot-separated namespace) to get (e.g. "default_graph.xmin") * @param {string} settingName - Setting (and its dot-separated namespace) to get (e.g. "default_graph.xmin")
* @returns {string|number|boolean} Value of the setting * @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 getSetting = FUNCTION
/** /**
* Sets a setting in the config * Sets a setting in the config
* @param {string} settingName - Setting (and its dot-separated namespace) to set (e.g. "default_graph.xmin") * @param {string} settingName - Setting (and its dot-separated namespace) to set (e.g. "default_graph.xmin")
* @param {string|number|boolean} value * @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 setSetting = FUNCTION
/** /**

View file

@ -20,69 +20,35 @@ import { Module } from "./common.mjs"
import Objects from "./objects.mjs" import Objects from "./objects.mjs"
import History from "./history.mjs" import History from "./history.mjs"
import Canvas from "./canvas.mjs" import Canvas from "./canvas.mjs"
import Settings from "./settings.mjs" import { DialogInterface, RootInterface, SettingsInterface } from "./interface.mjs"
import { DialogInterface, RootInterface } from "./interface.mjs"
import { BaseEvent } from "../events.mjs"
class LoadedEvent extends BaseEvent {
constructor() {
super("loaded")
}
}
class SavedEvent extends BaseEvent {
constructor() {
super("saved")
}
}
class ModifiedEvent extends BaseEvent {
constructor() {
super("modified")
}
}
class IOAPI extends Module { class IOAPI extends Module {
static emits = ["loaded", "saved", "modified"]
/** @type {RootInterface} */
#rootElement
/** @type {{show: function(string)}} */
#alert
#saved = true
constructor() { constructor() {
super("IO", { super("IO", {
alert: DialogInterface, alert: DialogInterface,
root: RootInterface root: RootInterface,
settings: SettingsInterface
}) })
// Settings.on("changed", this.__emitModified.bind(this))
History.on("added undone redone", this.__emitModified.bind(this))
}
__emitModified() {
this.#saved = false
this.emit(new ModifiedEvent())
}
/** /**
* True if no changes have been made since last save, false otherwise. * Path of the currently opened file. Empty if no file is opened.
* @return {boolean} * @type {string}
*/ */
get saved() { return this.#saved } this.saveFileName = ""
}
/** /**
* Initializes module with QML elements. * Initializes module with QML elements.
* @param {RootInterface} root * @param {RootInterface} root
* @param {SettingsInterface} settings
* @param {{show: function(string)}} alert * @param {{show: function(string)}} alert
*/ */
initialize({ root, alert }) { initialize({ root, settings, alert }) {
super.initialize({ root, alert }) super.initialize({ root, settings, alert })
this.#rootElement = root this.rootElement = root
this.#alert = alert this.settings = settings
this.alert = alert
} }
/** /**
@ -94,7 +60,7 @@ class IOAPI extends Module {
// Add extension if necessary // Add extension if necessary
if(["lpf"].indexOf(filename.split(".")[filename.split(".").length - 1]) === -1) if(["lpf"].indexOf(filename.split(".")[filename.split(".").length - 1]) === -1)
filename += ".lpf" filename += ".lpf"
Settings.set("saveFilename", filename, false) this.saveFilename = filename
let objs = {} let objs = {}
for(let objType in Objects.currentObjects) { for(let objType in Objects.currentObjects) {
objs[objType] = [] objs[objType] = []
@ -103,29 +69,28 @@ class IOAPI extends Module {
} }
} }
let settings = { let settings = {
"xzoom": Settings.xzoom, "xzoom": this.settings.xzoom,
"yzoom": Settings.yzoom, "yzoom": this.settings.yzoom,
"xmin": Settings.xmin, "xmin": this.settings.xmin,
"ymax": Settings.ymax, "ymax": this.settings.ymax,
"xaxisstep": Settings.xaxisstep, "xaxisstep": this.settings.xaxisstep,
"yaxisstep": Settings.yaxisstep, "yaxisstep": this.settings.yaxisstep,
"xaxislabel": Settings.xlabel, "xaxislabel": this.settings.xlabel,
"yaxislabel": Settings.ylabel, "yaxislabel": this.settings.ylabel,
"logscalex": Settings.logscalex, "logscalex": this.settings.logscalex,
"linewidth": Settings.linewidth, "linewidth": this.settings.linewidth,
"showxgrad": Settings.showxgrad, "showxgrad": this.settings.showxgrad,
"showygrad": Settings.showygrad, "showygrad": this.settings.showygrad,
"textsize": Settings.textsize, "textsize": this.settings.textsize,
"history": History.serialize(), "history": History.serialize(),
"width": this.#rootElement.width, "width": this.rootElement.width,
"height": this.#rootElement.height, "height": this.rootElement.height,
"objects": objs, "objects": objs,
"type": "logplotv1" "type": "logplotv1"
} }
Helper.write(filename, JSON.stringify(settings)) Helper.write(filename, JSON.stringify(settings))
this.#alert.show(qsTranslate("io", "Saved plot to '%1'.").arg(filename.split("/").pop())) this.alert.show(qsTranslate("io", "Saved plot to '%1'.").arg(filename.split("/").pop()))
this.#saved = true History.history.saved = true
this.emit(new SavedEvent())
} }
/** /**
@ -136,32 +101,32 @@ class IOAPI extends Module {
if(!this.initialized) throw new Error("Attempting loadDiagram before initialize!") if(!this.initialized) throw new Error("Attempting loadDiagram before initialize!")
if(!History.initialized) throw new Error("Attempting loadDiagram before history is initialized!") if(!History.initialized) throw new Error("Attempting loadDiagram before history is initialized!")
let basename = filename.split("/").pop() let basename = filename.split("/").pop()
this.#alert.show(qsTranslate("io", "Loading file '%1'.").arg(basename)) this.alert.show(qsTranslate("io", "Loading file '%1'.").arg(basename))
let data = JSON.parse(Helper.load(filename)) let data = JSON.parse(Helper.load(filename))
let error = "" let error = ""
if(data.hasOwnProperty("type") && data["type"] === "logplotv1") { if(data.hasOwnProperty("type") && data["type"] === "logplotv1") {
History.clear() History.clear()
// Importing settings // Importing settings
Settings.set("saveFilename", filename, false) this.settings.saveFilename = filename
Settings.set("xzoom", parseFloat(data["xzoom"]) || 100, false) this.settings.xzoom = parseFloat(data["xzoom"]) || 100
Settings.set("yzoom", parseFloat(data["yzoom"]) || 10, false) this.settings.yzoom = parseFloat(data["yzoom"]) || 10
Settings.set("xmin", parseFloat(data["xmin"]) || 5 / 10, false) this.settings.xmin = parseFloat(data["xmin"]) || 5 / 10
Settings.set("ymax", parseFloat(data["ymax"]) || 24, false) this.settings.ymax = parseFloat(data["ymax"]) || 24
Settings.set("xaxisstep", data["xaxisstep"] || "4", false) this.settings.xaxisstep = data["xaxisstep"] || "4"
Settings.set("yaxisstep", data["yaxisstep"] || "4", false) this.settings.yaxisstep = data["yaxisstep"] || "4"
Settings.set("xlabel", data["xaxislabel"] || "", false) this.settings.xlabel = data["xaxislabel"] || ""
Settings.set("ylabel", data["yaxislabel"] || "", false) this.settings.ylabel = data["yaxislabel"] || ""
Settings.set("logscalex", data["logscalex"] === true, false) this.settings.logscalex = data["logscalex"] === true
if("showxgrad" in data) if("showxgrad" in data)
Settings.set("showxgrad", data["showxgrad"], false) this.settings.showxgrad = data["showxgrad"]
if("showygrad" in data) if("showygrad" in data)
Settings.set("showygrad", data["showygrad"], false) this.settings.textsize = data["showygrad"]
if("linewidth" in data) if("linewidth" in data)
Settings.set("linewidth", data["linewidth"], false) this.settings.linewidth = data["linewidth"]
if("textsize" in data) if("textsize" in data)
Settings.set("textsize", data["textsize"], false) this.settings.textsize = data["textsize"]
this.#rootElement.height = parseFloat(data["height"]) || 500 this.rootElement.height = parseFloat(data["height"]) || 500
this.#rootElement.width = parseFloat(data["width"]) || 1000 this.rootElement.width = parseFloat(data["width"]) || 1000
// Importing objects // Importing objects
Objects.currentObjects = {} Objects.currentObjects = {}
@ -192,18 +157,20 @@ class IOAPI extends Module {
if("history" in data) if("history" in data)
History.unserialize(...data["history"]) History.unserialize(...data["history"])
// Refreshing sidebar
this.rootElement.updateObjectsLists()
} else { } else {
error = qsTranslate("io", "Invalid file provided.") error = qsTranslate("io", "Invalid file provided.")
} }
if(error !== "") { if(error !== "") {
console.log(error) console.log(error)
this.#alert.show(qsTranslate("io", "Could not load file: ") + error) this.alert.show(qsTranslate("io", "Could not load file: ") + error)
// TODO: Error handling // TODO: Error handling
return return
} }
this.#alert.show(qsTranslate("io", "Loaded file '%1'.").arg(basename)) Canvas.redraw()
this.#saved = true this.alert.show(qsTranslate("io", "Loaded file '%1'.").arg(basename))
this.emit(new LoadedEvent()) History.history.saved = true
} }
} }

View file

@ -21,7 +21,7 @@ import * as Instruction from "../lib/expr-eval/instruction.mjs"
import { escapeValue } from "../lib/expr-eval/expression.mjs" import { escapeValue } from "../lib/expr-eval/expression.mjs"
import { HelperInterface, LatexInterface } from "./interface.mjs" import { HelperInterface, LatexInterface } from "./interface.mjs"
const unicodechars = ["pi", "∞", const unicodechars = [
"α", "β", "γ", "δ", "ε", "ζ", "η", "α", "β", "γ", "δ", "ε", "ζ", "η",
"π", "θ", "κ", "λ", "μ", "ξ", "ρ", "π", "θ", "κ", "λ", "μ", "ξ", "ρ",
"ς", "σ", "τ", "φ", "χ", "ψ", "ω", "ς", "σ", "τ", "φ", "χ", "ψ", "ω",
@ -30,9 +30,9 @@ const unicodechars = ["pi", "∞",
"ₕ", "ₖ", "ₗ", "ₘ", "ₙ", "ₚ", "ₛ", "ₕ", "ₖ", "ₗ", "ₘ", "ₙ", "ₚ", "ₛ",
"ₜ", "¹", "²", "³", "⁴", "⁵", "⁶", "ₜ", "¹", "²", "³", "⁴", "⁵", "⁶",
"⁷", "⁸", "⁹", "⁰", "₁", "₂", "₃", "⁷", "⁸", "⁹", "⁰", "₁", "₂", "₃",
"₄", "₅", "₆", "₇", "₈", "₉", "₀" "₄", "₅", "₆", "₇", "₈", "₉", "₀",
] "pi", "∞"]
const equivalchars = ["\\pi", "\\infty", const equivalchars = [
"\\alpha", "\\beta", "\\gamma", "\\delta", "\\epsilon", "\\zeta", "\\eta", "\\alpha", "\\beta", "\\gamma", "\\delta", "\\epsilon", "\\zeta", "\\eta",
"\\pi", "\\theta", "\\kappa", "\\lambda", "\\mu", "\\xi", "\\rho", "\\pi", "\\theta", "\\kappa", "\\lambda", "\\mu", "\\xi", "\\rho",
"\\sigma", "\\sigma", "\\tau", "\\phi", "\\chi", "\\psi", "\\omega", "\\sigma", "\\sigma", "\\tau", "\\phi", "\\chi", "\\psi", "\\omega",
@ -42,7 +42,7 @@ const equivalchars = ["\\pi", "\\infty",
"{}_{t}", "{}^{1}", "{}^{2}", "{}^{3}", "{}^{4}", "{}^{5}", "{}^{6}", "{}_{t}", "{}^{1}", "{}^{2}", "{}^{3}", "{}^{4}", "{}^{5}", "{}^{6}",
"{}^{7}", "{}^{8}", "{}^{9}", "{}^{0}", "{}_{1}", "{}_{2}", "{}_{3}", "{}^{7}", "{}^{8}", "{}^{9}", "{}^{0}", "{}_{1}", "{}_{2}", "{}_{3}",
"{}_{4}", "{}_{5}", "{}_{6}", "{}_{7}", "{}_{8}", "{}_{9}", "{}_{0}", "{}_{4}", "{}_{5}", "{}_{6}", "{}_{7}", "{}_{8}", "{}_{9}", "{}_{0}",
] "\\pi", "\\infty"]
/** /**
* Class containing the result of a LaTeX render. * Class containing the result of a LaTeX render.
@ -60,9 +60,6 @@ class LatexRenderResult {
} }
class LatexAPI extends Module { class LatexAPI extends Module {
/** @type {LatexInterface} */
#latex = null
constructor() { constructor() {
super("Latex", { super("Latex", {
latex: LatexInterface, latex: LatexInterface,
@ -80,8 +77,9 @@ class LatexAPI extends Module {
*/ */
initialize({ latex, helper }) { initialize({ latex, helper }) {
super.initialize({ latex, helper }) super.initialize({ latex, helper })
this.#latex = latex this.latex = latex
this.enabled = helper.getSetting("enable_latex") this.helper = helper
this.enabled = helper.getSettingBool("enable_latex")
} }
/** /**
@ -95,7 +93,7 @@ class LatexAPI extends Module {
*/ */
findPrerendered(markup, fontSize, color) { findPrerendered(markup, fontSize, color) {
if(!this.initialized) throw new Error("Attempting findPrerendered before initialize!") if(!this.initialized) throw new Error("Attempting findPrerendered before initialize!")
const data = this.#latex.findPrerendered(markup, fontSize, color) const data = this.latex.findPrerendered(markup, fontSize, color)
let ret = null let ret = null
if(data !== "") if(data !== "")
ret = new LatexRenderResult(...data.split(",")) ret = new LatexRenderResult(...data.split(","))
@ -112,12 +110,7 @@ class LatexAPI extends Module {
*/ */
async requestAsyncRender(markup, fontSize, color) { async requestAsyncRender(markup, fontSize, color) {
if(!this.initialized) throw new Error("Attempting requestAsyncRender before initialize!") if(!this.initialized) throw new Error("Attempting requestAsyncRender before initialize!")
let render let args = this.latex.render(markup, fontSize, color).split(",")
if(this.#latex.supportsAsyncRender)
render = await this.#latex.renderAsync(markup, fontSize, color)
else
render = this.#latex.renderSync(markup, fontSize, color)
const args = render.split(",")
return new LatexRenderResult(...args) return new LatexRenderResult(...args)
} }
@ -142,10 +135,9 @@ class LatexAPI extends Module {
*/ */
parif(elem, contents) { parif(elem, contents) {
elem = elem.toString() elem = elem.toString()
const contains = contents.some(x => elem.indexOf(x) > 0) if(elem[0] !== "(" && elem[elem.length - 1] !== ")" && contents.some(x => elem.indexOf(x) > 0))
if(contains && (elem[0] !== "(" || elem.at(-1) !== ")"))
return this.par(elem) return this.par(elem)
if(!contains && elem[0] === "(" && elem.at(-1) === ")") if(elem[0] === "(" && elem[elem.length - 1] === ")")
return elem.removeEnclosure() return elem.removeEnclosure()
return elem return elem
} }
@ -163,21 +155,20 @@ class LatexAPI extends Module {
if(args.length === 3) if(args.length === 3)
return `\\frac{d${args[0].removeEnclosure().replaceAll(args[1].removeEnclosure(), "x")}}{dx}` return `\\frac{d${args[0].removeEnclosure().replaceAll(args[1].removeEnclosure(), "x")}}{dx}`
else else
return `\\frac{d${args[0]}}{dx}(${args[1]})` return `\\frac{d${args[0]}}{dx}(x)`
case "integral": case "integral":
if(args.length === 4) if(args.length === 4)
return `\\int\\limits_{${args[0]}}^{${args[1]}}${args[2].removeEnclosure()} d${args[3].removeEnclosure()}` return `\\int\\limits_{${args[0]}}^{${args[1]}}${args[2].removeEnclosure()} d${args[3].removeEnclosure()}`
else else
return `\\int\\limits_{${args[0]}}^{${args[1]}}${args[2]}(t) dt` return `\\int\\limits_{${args[0]}}^{${args[1]}}${args[2]}(t) dt`
case "sqrt": case "sqrt":
const arg = this.parif(args.join(", "), []) return `\\sqrt\\left(${args.join(", ")}\\right)`
return `\\sqrt{${arg}}`
case "abs": case "abs":
return `\\left|${args.join(", ")}\\right|` return `\\left|${args.join(", ")}\\right|`
case "floor": case "floor":
return `\\left\\lfloor{${args.join(", ")}}\\right\\rfloor` return `\\left\\lfloor${args.join(", ")}\\right\\rfloor`
case "ceil": case "ceil":
return `\\left\\lceil{${args.join(", ")}}\\right\\rceil` return `\\left\\lceil${args.join(", ")}\\right\\rceil`
default: default:
return `\\mathrm{${f}}\\left(${args.join(", ")}\\right)` return `\\mathrm{${f}}\\left(${args.join(", ")}\\right)`
} }
@ -191,17 +182,16 @@ class LatexAPI extends Module {
* @returns {string} * @returns {string}
*/ */
variable(vari, wrapIn$ = false) { variable(vari, wrapIn$ = false) {
if(wrapIn$) { if(wrapIn$)
for(let i = 0; i < unicodechars.length; i++) { for(let i = 0; i < unicodechars.length; i++) {
if(vari.includes(unicodechars[i])) if(vari.includes(unicodechars[i]))
vari = vari.replaceAll(unicodechars[i], "$" + equivalchars[i] + "$") vari = vari.replaceAll(unicodechars[i], "$" + equivalchars[i] + "$")
} }
} else { else
for(let i = 0; i < unicodechars.length; i++) { for(let i = 0; i < unicodechars.length; i++) {
if(vari.includes(unicodechars[i])) if(vari.includes(unicodechars[i]))
vari = vari.replaceAll(unicodechars[i], equivalchars[i]) vari = vari.replaceAll(unicodechars[i], equivalchars[i])
} }
}
return vari return vari
} }
@ -269,7 +259,7 @@ class LatexAPI extends Module {
throw new EvalError("Unknown operator " + item.value + ".") throw new EvalError("Unknown operator " + item.value + ".")
} }
break break
case Instruction.IOP3: // Ternary operator case Instruction.IOP3: // Thirdiary operator
n3 = nstack.pop() n3 = nstack.pop()
n2 = nstack.pop() n2 = nstack.pop()
n1 = nstack.pop() n1 = nstack.pop()
@ -296,7 +286,7 @@ class LatexAPI extends Module {
nstack.push(this.parif(n1, ["+", "-", "*", "/", "^"]) + "!") nstack.push(this.parif(n1, ["+", "-", "*", "/", "^"]) + "!")
break break
default: default:
nstack.push(this.functionToLatex(f, [this.parif(n1, ["+", "-", "*", "/", "^"])])) nstack.push(f + this.parif(n1, ["+", "-", "*", "/", "^"]))
break break
} }
break break
@ -331,6 +321,9 @@ class LatexAPI extends Module {
throw new EvalError("invalid Expression") throw new EvalError("invalid Expression")
} }
} }
if(nstack.length > 1) {
nstack = [nstack.join(";")]
}
return String(nstack[0]) return String(nstack[0])
} }
} }

View file

@ -24,10 +24,6 @@ class ObjectsAPI extends Module {
constructor() { constructor() {
super("Objects") super("Objects")
/**
* List of object constructors.
* @type {Object.<string,typeof DrawableObject>}
*/
this.types = {} this.types = {}
/** /**
* List of objects for each type of object. * List of objects for each type of object.
@ -69,7 +65,7 @@ class ObjectsAPI extends Module {
* @param {string} newName - Name to rename the object to. * @param {string} newName - Name to rename the object to.
*/ */
renameObject(oldName, newName) { renameObject(oldName, newName) {
const obj = this.currentObjectsByName[oldName] let obj = this.currentObjectsByName[oldName]
delete this.currentObjectsByName[oldName] delete this.currentObjectsByName[oldName]
this.currentObjectsByName[newName] = obj this.currentObjectsByName[newName] = obj
obj.name = newName obj.name = newName
@ -80,7 +76,7 @@ class ObjectsAPI extends Module {
* @param {string} objName - Current name of the object. * @param {string} objName - Current name of the object.
*/ */
deleteObject(objName) { deleteObject(objName) {
const obj = this.currentObjectsByName[objName] let obj = this.currentObjectsByName[objName]
if(obj !== undefined) { if(obj !== undefined) {
this.currentObjects[obj.type].splice(this.currentObjects[obj.type].indexOf(obj), 1) this.currentObjects[obj.type].splice(this.currentObjects[obj.type].indexOf(obj), 1)
obj.delete() obj.delete()

View file

@ -20,9 +20,6 @@ import General from "../preferences/general.mjs"
import Editor from "../preferences/expression.mjs" import Editor from "../preferences/expression.mjs"
import DefaultGraph from "../preferences/default.mjs" import DefaultGraph from "../preferences/default.mjs"
/**
* Module for application wide settings.
*/
class PreferencesAPI extends Module { class PreferencesAPI extends Module {
constructor() { constructor() {
super("Preferences") super("Preferences")

View file

@ -1,186 +0,0 @@
/**
* 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 { BaseEvent } from "../events.mjs"
import { HelperInterface } from "./interface.mjs"
/**
* Base event for when a setting was changed.
*/
class ChangedEvent extends BaseEvent {
/**
*
* @param {string} property - Name of the property that was chagned
* @param {string|number|boolean} oldValue - Old value of the property
* @param {string|number|boolean} newValue - Current (new) value of the property
* @param {boolean} byUser - True if the user is at the source of the change in the setting.
*/
constructor(property, oldValue, newValue, byUser) {
super("changed")
this.property = property
this.oldValue = oldValue
this.newValue = newValue
this.byUser = byUser
}
}
/**
* Module for graph settings.
*/
class SettingsAPI extends Module {
static emits = ["changed"]
#nonConfigurable = ["saveFilename"]
/** @type {Map<string, string|number|boolean>} */
#properties = new Map([
["saveFilename", ""],
["xzoom", 100],
["yzoom", 10],
["xmin", .5],
["ymax", 25],
["xaxisstep", "4"],
["yaxisstep", "4"],
["xlabel", ""],
["ylabel", ""],
["linewidth", 1],
["textsize", 18],
["logscalex", true],
["showxgrad", true],
["showygrad", true]
])
constructor() {
super("Settings", {
helper: HelperInterface
})
}
/**
*
* @param {HelperInterface} helper
*/
initialize({ helper }) {
super.initialize({ helper })
// Initialize default values.
for(const key of this.#properties.keys())
if(!this.#nonConfigurable.includes(key))
this.set(key, helper.getSetting("default_graph."+key), false)
}
/**
* Sets a setting to a given value
*
* @param {string} property
* @param {string|number|boolean} value
* @param {boolean} byUser - Set to true if the user is at the origin of this change.
*/
set(property, value, byUser) {
if(!this.#properties.has(property))
throw new Error(`Property ${property} is not a setting.`)
const oldValue = this.#properties.get(property)
const propType = typeof oldValue
if(byUser)
console.debug("Setting", property, "from", oldValue, "to", value, `(${typeof value}, ${byUser})`)
if(propType !== typeof value)
throw new Error(`Value of ${property} must be a ${propType} (${typeof value} provided).`)
this.#properties.set(property, value)
const evt = new ChangedEvent(property, oldValue, value, byUser === true)
this.emit(evt)
}
/**
* Name of the currently opened file.
* @returns {string}
*/
get saveFilename() { return this.#properties.get("saveFilename") }
/**
* Zoom on the x axis of the diagram.
* @returns {number}
*/
get xzoom() { return this.#properties.get("xzoom") }
/**
* Zoom on the y axis of the diagram.
* @returns {number}
*/
get yzoom() { return this.#properties.get("yzoom") }
/**
* Minimum x of the diagram.
* @returns {number}
*/
get xmin() { return this.#properties.get("xmin") }
/**
* Maximum y of the diagram.
* @returns {number}
*/
get ymax() { return this.#properties.get("ymax") }
/**
* Step of the x axis graduation (expression).
* @note Only available in non-logarithmic mode.
* @returns {string}
*/
get xaxisstep() { return this.#properties.get("xaxisstep") }
/**
* Step of the y axis graduation (expression).
* @returns {string}
*/
get yaxisstep() { return this.#properties.get("yaxisstep") }
/**
* Label used on the x axis.
* @returns {string}
*/
get xlabel() { return this.#properties.get("xlabel") }
/**
* Label used on the y axis.
* @returns {string}
*/
get ylabel() { return this.#properties.get("ylabel") }
/**
* Width of lines that will be drawn into the canvas.
* @returns {number}
*/
get linewidth() { return this.#properties.get("linewidth") }
/**
* Font size of the text that will be drawn into the canvas.
* @returns {number}
*/
get textsize() { return this.#properties.get("textsize") }
/**
* true if the canvas should be in logarithmic mode, false otherwise.
* @returns {boolean}
*/
get logscalex() { return this.#properties.get("logscalex") }
/**
* true if the x graduation should be shown, false otherwise.
* @returns {boolean}
*/
get showxgrad() { return this.#properties.get("showxgrad") }
/**
* true if the y graduation should be shown, false otherwise.
* @returns {boolean}
*/
get showygrad() { return this.#properties.get("showygrad") }
}
Modules.Settings = Modules.Settings || new SettingsAPI()
export default Modules.Settings

View file

@ -63,7 +63,7 @@ export default class BodePhase extends ExecutableObject {
// Create new point // Create new point
om_0 = Objects.createNewRegisteredObject("Point", [Objects.getNewName("ω"), this.color, "name"]) om_0 = Objects.createNewRegisteredObject("Point", [Objects.getNewName("ω"), this.color, "name"])
om_0.labelPosition = this.phase.execute() >= 0 ? "above" : "below" om_0.labelPosition = this.phase.execute() >= 0 ? "above" : "below"
History.addToHistory(new CreateNewObject(om_0.name, "Point", om_0.export())) History.history.addToHistory(new CreateNewObject(om_0.name, "Point", om_0.export()))
labelPosition = "below" labelPosition = "below"
} }
om_0.requiredBy.push(this) om_0.requiredBy.push(this)

View file

@ -53,11 +53,11 @@ export class BoolSetting extends Setting {
} }
value() { value() {
return Helper.getSetting(this.nameInConfig) return Helper.getSettingBool(this.nameInConfig)
} }
set(value) { set(value) {
Helper.setSetting(this.nameInConfig, value === true) Helper.setSettingBool(this.nameInConfig, value)
} }
} }
@ -69,11 +69,11 @@ export class NumberSetting extends Setting {
} }
value() { value() {
return Helper.getSetting(this.nameInConfig) return Helper.getSettingInt(this.nameInConfig)
} }
set(value) { set(value) {
Helper.setSetting(this.nameInConfig, +value) Helper.setSettingInt(this.nameInConfig, value)
} }
} }
@ -84,11 +84,11 @@ export class EnumIntSetting extends Setting {
} }
value() { value() {
return Helper.getSetting(this.nameInConfig) return Helper.getSettingInt(this.nameInConfig)
} }
set(value) { set(value) {
Helper.setSetting(this.nameInConfig, +value) Helper.setSettingInt(this.nameInConfig, value)
} }
} }
@ -131,6 +131,6 @@ export class StringSetting extends Setting {
} }
set(value) { set(value) {
Helper.setSetting(this.nameInConfig, ""+value) Helper.setSetting(this.nameInConfig, value)
} }
} }

View file

@ -28,7 +28,7 @@ const XZOOM = new NumberSetting(
const YZOOM = new NumberSetting( const YZOOM = new NumberSetting(
qsTranslate("Settings", "Y Zoom"), qsTranslate("Settings", "Y Zoom"),
"default_graph.yzoom", "default_graph.xzoom",
"yzoom", "yzoom",
0.1 0.1
) )
@ -37,7 +37,7 @@ const XMIN = new NumberSetting(
qsTranslate("Settings", "Min X"), qsTranslate("Settings", "Min X"),
"default_graph.xmin", "default_graph.xmin",
"xmin", "xmin",
() => Helper.getSetting("default_graph.logscalex") ? 1e-100 : -Infinity () => Helper.getSettingBool("default_graph.logscalex") ? 1e-100 : -Infinity
) )
const YMAX = new NumberSetting( const YMAX = new NumberSetting(

View file

@ -46,15 +46,8 @@ class EnableLatex extends BoolSetting {
} }
} }
const ENABLE_LATEX_ASYNC = new BoolSetting(
qsTranslate("general", "Enable asynchronous LaTeX renderer"),
"enable_latex_async",
"new"
)
export default [ export default [
CHECK_FOR_UPDATES, CHECK_FOR_UPDATES,
RESET_REDO_STACK, RESET_REDO_STACK,
new EnableLatex(), new EnableLatex()
ENABLE_LATEX_ASYNC
] ]

View file

@ -21,150 +21,135 @@
* Replaces latin characters with their uppercase versions. * Replaces latin characters with their uppercase versions.
* @return {string} * @return {string}
*/ */
String.prototype.toLatinUppercase = function() { String.prototype.toLatinUppercase = String.prototype.toLatinUppercase || function() {
return this.replace(/[a-z]/g, function(match) { return this.replace(/[a-z]/g, function(match) {
return match.toUpperCase() return match.toUpperCase()
}) })
} }
/** /**
* Removes the first and last character of a string * Removes the 'enclosers' of a string (e.g. quotes, parentheses, brackets...)
* Used to remove enclosing characters like quotes, parentheses, brackets...
* @note Does NOT check for their existence ahead of time.
* @return {string} * @return {string}
*/ */
String.prototype.removeEnclosure = function() { String.prototype.removeEnclosure = function() {
return this.substring(1, this.length - 1) return this.substring(1, this.length - 1)
} }
/** const powerpos = {
* Rounds to a certain number of decimal places. "-": "⁻",
* From https://stackoverflow.com/a/48764436 "+": "⁺",
* "=": "⁼",
* @param {number} decimalPlaces " ": "",
* @return {number} "(": "⁽",
*/ ")": "⁾",
Number.prototype.toDecimalPrecision = function(decimalPlaces = 0) { "0": "⁰",
const p = Math.pow(10, decimalPlaces) "1": "¹",
const n = (this * p) * (1 + Number.EPSILON) "2": "²",
return Math.round(n) / p "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 CHARACTER_TO_POWER = new Map([ const exponents = [
["-", "⁻"], "⁰","¹","²","³","⁴","⁵","⁶","⁷","⁸","⁹"
["+", "⁺"],
["=", "⁼"],
[" ", ""],
["(", "⁽"],
[")", "⁾"],
["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 CHARACTER_TO_INDICE = new Map([
["-", "₋"],
["+", "₊"],
["=", "₌"],
["(", "₍"],
[")", "₎"],
[" ", ""],
["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", "ₓ"]
])
const EXPONENTS = [
"⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹"
] ]
const EXPONENTS_REG = new RegExp("([" + EXPONENTS.join("") + "]+)", "g") const exponentReg = new RegExp('(['+exponents.join('')+']+)', 'g')
/** const indicepos = {
* Put a text in sup position "-": "₋",
* @param {string} text "+": "₊",
* @return {string} "=": "₌",
*/ "(": "₍",
")": "₎",
" ": "",
"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) { export function textsup(text) {
let ret = "" let ret = ""
text = text.toString() text = text.toString()
for(let letter of text) for (let i = 0; i < text.length; i++) {
ret += CHARACTER_TO_POWER.has(letter) ? CHARACTER_TO_POWER.get(letter) : letter if(Object.keys(powerpos).indexOf(text[i]) >= 0) {
ret += powerpos[text[i]]
} else {
ret += text[i]
}
}
return ret return ret
} }
/** // Put a text in sub position
* Put a text in sub position
* @param {string} text
* @return {string}
*/
export function textsub(text) { export function textsub(text) {
let ret = "" let ret = ""
text = text.toString() text = text.toString()
for(let letter of text) for (let i = 0; i < text.length; i++) {
ret += CHARACTER_TO_INDICE.has(letter) ? CHARACTER_TO_INDICE.get(letter) : letter if(Object.keys(indicepos).indexOf(text[i]) >= 0) {
ret += indicepos[text[i]]
} else {
ret += text[i]
}
}
return ret return ret
} }
/** /**
* Simplifies (mathematically) a mathematical expression. * Simplifies (mathematically) a mathematical expression.
* @deprecated
* @param {string} str - Expression to parse * @param {string} str - Expression to parse
* @returns {string} * @returns {string}
*/ */
@ -187,43 +172,37 @@ export function simplifyExpression(str) {
// n1 & n3 are multiplied, opeM is the main operation (- or +). // n1 & n3 are multiplied, opeM is the main operation (- or +).
// Putting all n in form of number // Putting all n in form of number
//n2 = n2 == undefined ? 1 : parseFloat(n) //n2 = n2 == undefined ? 1 : parseFloat(n)
n1 = m1 === undefined ? 1 : eval(m1 + "1") n1 = m1 === undefined ? 1 : eval(m1 + '1')
n2 = m2 === undefined ? 1 : eval("1" + m2) n2 = m2 === undefined ? 1 : eval('1' + m2)
n3 = m3 === undefined ? 1 : eval(m3 + "1") n3 = m3 === undefined ? 1 : eval(m3 + '1')
n4 = m4 === undefined ? 1 : eval("1" + m4) n4 = m4 === undefined ? 1 : eval('1' + m4)
//let [n1, n2, n3, n4] = [n1, n2, n3, n4].map(n => n == undefined ? 1 : parseFloat(n)) //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) // Falling back to * in case it does not exist (the corresponding n would be 1)
[ope2, ope4] = [ope2, ope4].map(ope => ope === "/" ? "/" : "*") [ope2, ope4] = [ope2, ope4].map(ope => ope === '/' ? '/' : '*')
let coeff1 = n1 * n2 let coeff1 = n1*n2
let coeff2 = n3 * n4 let coeff2 = n3*n4
let coefficient = coeff1 + coeff2 - (opeM === "-" ? 2 * coeff2 : 0) let coefficient = coeff1+coeff2-(opeM === '-' ? 2*coeff2 : 0)
return `${coefficient} * π` return `${coefficient} * π`
} }
], ],
[ // Removing parenthesis when content is only added from both sides. [ // Removing parenthesis when content is only added from both sides.
/(^|[+-] |\()\(([^)(]+)\)($| [+-]|\))/g, /(^|[+-] |\()\(([^)(]+)\)($| [+-]|\))/g,
function(match, b4, middle, after) { function(match, b4, middle, after) {return `${b4}${middle}${after}`}
return `${b4}${middle}${after}`
}
], ],
[ // Removing parenthesis when content is only multiplied. [ // Removing parenthesis when content is only multiplied.
/(^|[*\/] |\()\(([^)(+-]+)\)($| [*\/+-]|\))/g, /(^|[*\/] |\()\(([^)(+-]+)\)($| [*\/+-]|\))/g,
function(match, b4, middle, after) { function(match, b4, middle, after) {return `${b4}${middle}${after}`}
return `${b4}${middle}${after}`
}
], ],
[ // Removing parenthesis when content is only multiplied. [ // Removing parenthesis when content is only multiplied.
/(^|[*\/+-] |\()\(([^)(+-]+)\)($| [*\/]|\))/g, /(^|[*\/+-] |\()\(([^)(+-]+)\)($| [*\/]|\))/g,
function(match, b4, middle, after) { function(match, b4, middle, after) {return `${b4}${middle}${after}`}
return `${b4}${middle}${after}`
}
], ],
[// Simplification additions/subtractions. [// Simplification additions/subtractions.
/(^|[^*\/] |\()([-.\d]+) [+-] (\([^)(]+\)|[^)(]+) [+-] ([-.\d]+)($| [^*\/]|\))/g, /(^|[^*\/] |\()([-.\d]+) [+-] (\([^)(]+\)|[^)(]+) [+-] ([-.\d]+)($| [^*\/]|\))/g,
function(match, b4, n1, op1, middle, op2, n2, after) { function(match, b4, n1, op1, middle, op2, n2, after) {
let total let total
if(op2 === "+") { if(op2 === '+') {
total = parseFloat(n1) + parseFloat(n2) total = parseFloat(n1) + parseFloat(n2)
} else { } else {
total = parseFloat(n1) - parseFloat(n2) total = parseFloat(n1) - parseFloat(n2)
@ -234,12 +213,12 @@ export function simplifyExpression(str) {
[// Simplification multiplications/divisions. [// Simplification multiplications/divisions.
/([-.\d]+) [*\/] (\([^)(]+\)|[^)(+-]+) [*\/] ([-.\d]+)/g, /([-.\d]+) [*\/] (\([^)(]+\)|[^)(+-]+) [*\/] ([-.\d]+)/g,
function(match, n1, op1, middle, op2, n2) { function(match, n1, op1, middle, op2, n2) {
if(parseInt(n1) === n1 && parseInt(n2) === n2 && op2 === "/" && if(parseInt(n1) === n1 && parseInt(n2) === n2 && op2 === '/' &&
(parseInt(n1) / parseInt(n2)) % 1 !== 0) { (parseInt(n1) / parseInt(n2)) % 1 !== 0) {
// Non int result for int division. // Non int result for int division.
return `(${n1} / ${n2}) ${op1} ${middle}` return `(${n1} / ${n2}) ${op1} ${middle}`
} else { } else {
if(op2 === "*") { if(op2 === '*') {
return `${parseFloat(n1) * parseFloat(n2)} ${op1} ${middle}` return `${parseFloat(n1) * parseFloat(n2)} ${op1} ${middle}`
} else { } else {
return `${parseFloat(n1) / parseFloat(n2)} ${op1} ${middle}` return `${parseFloat(n1) / parseFloat(n2)} ${op1} ${middle}`
@ -253,17 +232,17 @@ export function simplifyExpression(str) {
let str = middle let str = middle
// Replace all groups // Replace all groups
while(/\([^)(]+\)/g.test(str)) while(/\([^)(]+\)/g.test(str))
str = str.replace(/\([^)(]+\)/g, "") str = str.replace(/\([^)(]+\)/g, '')
// There shouldn't be any more parenthesis // There shouldn't be any more parenthesis
// If there is, that means the 2 parenthesis are needed. // If there is, that means the 2 parenthesis are needed.
if(!str.includes(")") && !str.includes("(")) { if(!str.includes(')') && !str.includes('(')) {
return middle return middle
} else { } else {
return `(${middle})` return `(${middle})`
} }
} }
] ],
// Simple simplifications // Simple simplifications
// [/(\s|^|\()0(\.0+)? \* (\([^)(]+\))/g, '$10'], // [/(\s|^|\()0(\.0+)? \* (\([^)(]+\))/g, '$10'],
// [/(\s|^|\()0(\.0+)? \* ([^)(+-]+)/g, '$10'], // [/(\s|^|\()0(\.0+)? \* ([^)(+-]+)/g, '$10'],
@ -294,35 +273,24 @@ export function simplifyExpression(str) {
/** /**
* Transforms a mathematical expression to make it readable by humans. * Transforms a mathematical expression to make it readable by humans.
* NOTE: Will break parsing of expression. * NOTE: Will break parsing of expression.
* @deprecated
* @param {string} str - Expression to parse. * @param {string} str - Expression to parse.
* @returns {string} * @returns {string}
*/ */
export function makeExpressionReadable(str) { export function makeExpressionReadable(str) {
let replacements = [ let replacements = [
// letiables // letiables
[/pi/g, "π"], [/pi/g, 'π'],
[/Infinity/g, "∞"], [/Infinity/g, '∞'],
[/inf/g, "∞"], [/inf/g, '∞'],
// Other // Other
[/ \* /g, "×"], [/ \* /g, '×'],
[/ \^ /g, "^"], [/ \^ /g, '^'],
[/\^\(([\d\w+-]+)\)/g, function(match, p1) { [/\^\(([\d\w+-]+)\)/g, function(match, p1) { return textsup(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) { [/_([\d\w+-]+)/g, function(match, p1) { return textsub(p1) }],
return textsup(p1) [/\[([^\[\]]+)\]/g, function(match, p1) { return textsub(p1) }],
}], [/(\d|\))×/g, '$1'],
[/_\(([\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) { [/integral\((.+),\s?(.+),\s?["'](.+)["'],\s?["'](.+)["']\)/g, function(match, a, b, p1, body, p2, p3, by, p4) {
if(a.length < b.length) { if(a.length < b.length) {
return `${textsub(a)}${textsup(b)} ${body} d${by}` return `${textsub(a)}${textsup(b)} ${body} d${by}`
@ -331,7 +299,7 @@ export function makeExpressionReadable(str) {
} }
}], }],
[/derivative\(?["'](.+)["'], ?["'](.+)["'], ?(.+)\)?/g, function(match, p1, body, p2, p3, by, p4, x) { [/derivative\(?["'](.+)["'], ?["'](.+)["'], ?(.+)\)?/g, function(match, p1, body, p2, p3, by, p4, x) {
return `d(${body.replace(new RegExp(by, "g"), "x")})/dx` return `d(${body.replace(new RegExp(by, 'g'), 'x')})/dx`
}] }]
] ]
@ -343,48 +311,6 @@ export function makeExpressionReadable(str) {
return str return str
} }
/** @type {[RegExp, string][]} */
const replacements = [
// Greek letters
[/(\W|^)al(pha)?(\W|$)/g, "$1α$3"],
[/(\W|^)be(ta)?(\W|$)/g, "$1β$3"],
[/(\W|^)ga(mma)?(\W|$)/g, "$1γ$3"],
[/(\W|^)de(lta)?(\W|$)/g, "$1δ$3"],
[/(\W|^)ep(silon)?(\W|$)/g, "$1ε$3"],
[/(\W|^)ze(ta)?(\W|$)/g, "$1ζ$3"],
[/(\W|^)et(a)?(\W|$)/g, "$1η$3"],
[/(\W|^)th(eta)?(\W|$)/g, "$1θ$3"],
[/(\W|^)io(ta)?(\W|$)/g, "$1ι$3"],
[/(\W|^)ka(ppa)?(\W|$)/g, "$1κ$3"],
[/(\W|^)la(mbda)?(\W|$)/g, "$1λ$3"],
[/(\W|^)mu(\W|$)/g, "$1μ$2"],
[/(\W|^)nu(\W|$)/g, "$1ν$2"],
[/(\W|^)xi(\W|$)/g, "$1ξ$2"],
[/(\W|^)rh(o)?(\W|$)/g, "$1ρ$3"],
[/(\W|^)si(gma)?(\W|$)/g, "$1σ$3"],
[/(\W|^)ta(u)?(\W|$)/g, "$1τ$3"],
[/(\W|^)up(silon)?(\W|$)/g, "$1υ$3"],
[/(\W|^)ph(i)?(\W|$)/g, "$1φ$3"],
[/(\W|^)ch(i)?(\W|$)/g, "$1χ$3"],
[/(\W|^)ps(i)?(\W|$)/g, "$1ψ$3"],
[/(\W|^)om(ega)?(\W|$)/g, "$1ω$3"],
// Capital greek letters
[/(\W|^)gga(mma)?(\W|$)/g, "$1Γ$3"],
[/(\W|^)gde(lta)?(\W|$)/g, "$1Δ$3"],
[/(\W|^)gth(eta)?(\W|$)/g, "$1Θ$3"],
[/(\W|^)gla(mbda)?(\W|$)/g, "$1Λ$3"],
[/(\W|^)gxi(\W|$)/g, "$1Ξ$2"],
[/(\W|^)gpi(\W|$)/g, "$1Π$2"],
[/(\W|^)gsi(gma)?(\W|$)/g, "$1Σ$3"],
[/(\W|^)gph(i)?(\W|$)/g, "$1Φ$3"],
[/(\W|^)gps(i)?(\W|$)/g, "$1Ψ$3"],
[/(\W|^)gom(ega)?(\W|$)/g, "$1Ω$3"],
// Array elements
[/\[([^\]\[]+)\]/g, function(match, p1) {
return textsub(p1)
}]
]
/** /**
* Parses a variable name to make it readable by humans. * Parses a variable name to make it readable by humans.
* *
@ -393,24 +319,65 @@ const replacements = [
* @returns {string} - The parsed name * @returns {string} - The parsed name
*/ */
export function parseName(str, removeUnallowed = true) { export function parseName(str, removeUnallowed = true) {
for(const replacement of replacements) 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]) str = str.replace(replacement[0], replacement[1])
if(removeUnallowed)
str = str.replace(/[xnπ\\∪∩\]\[ ()^/÷*×+=\d¹²³⁴⁵⁶⁷⁸⁹⁰-]/g, "")
return str return str
} }
/** /**
* Transforms camel case strings to a space separated one. * Transforms camel case strings to a space separated one.
* *
* @deprecated
* @param {string} label - Camel case to parse * @param {string} label - Camel case to parse
* @returns {string} Parsed label. * @returns {string} Parsed label.
*/ */
export function camelCase2readable(label) { export function camelCase2readable(label) {
let parsed = parseName(label, false) let parsed = parseName(label, false)
return parsed.charAt(0).toLatinUppercase() + parsed.slice(1).replace(/([A-Z])/g, " $1") return parsed.charAt(0).toLatinUppercase() + parsed.slice(1).replace(/([A-Z])/g," $1")
} }
/** /**
@ -418,12 +385,12 @@ export function camelCase2readable(label) {
* @returns {string} * @returns {string}
*/ */
export function getRandomColor() { export function getRandomColor() {
let clrs = "0123456789ABCDEF" let clrs = '0123456789ABCDEF';
let color = "#" let color = '#';
for(let i = 0; i < 6; i++) { for(let i = 0; i < 6; i++) {
color += clrs[Math.floor(Math.random() * (16 - 5 * (i % 2 === 0)))] color += clrs[Math.floor(Math.random() * (16-5*(i%2===0)))];
} }
return color return color;
} }
/** /**
@ -432,15 +399,16 @@ export function getRandomColor() {
* @returns {string} * @returns {string}
*/ */
export function escapeHTML(str) { export function escapeHTML(str) {
return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;") return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;') ;
} }
/** /**
* Parses exponents and replaces them with expression values * Parses exponents and replaces them with expression values
* @param {string} expression - The expression to replace in. * @param {string} expression - The expression to replace in.
* @return {string} The parsed expression * @return {string} The parsed expression
*/ */
export function exponentsToExpression(expression) { export function exponentsToExpression(expression) {
return expression.replace(EXPONENTS_REG, (m, exp) => "^" + exp.split("").map((x) => EXPONENTS.indexOf(x)).join("")) return expression.replace(exponentReg, (m, exp) => '^' + exp.split('').map((x) => exponents.indexOf(x)).join(''))
} }

View file

@ -1,118 +0,0 @@
/**
* 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"
const { spy } = chaiPlugins
import { BaseEventEmitter, BaseEvent } from "../../src/events.mjs"
class MockEmitter extends BaseEventEmitter {
static emits = ["example1", "example2"]
}
class MockEvent1 extends BaseEvent {
constructor() {
super("example1")
}
}
class MockEvent2 extends BaseEvent {
constructor(parameter) {
super("example2")
this.parameter = parameter
}
}
describe("Lib/EventsEmitters", function() {
it("sends events with unique and readonly names", function() {
const event = new MockEvent1()
expect(event.name).to.equal("example1")
expect(() => event.name = "not").to.throw()
})
it("forwards events to all of its listeners", function() {
const emitter = new MockEmitter()
const listener1 = spy()
const listener2 = spy()
emitter.on("example1", listener1)
emitter.on("example1", listener2)
emitter.emit(new MockEvent1())
expect(listener1).to.have.been.called.once
expect(listener2).to.have.been.called.once
})
it("forwards multiple events to a singular listener", function() {
const emitter = new MockEmitter()
const listener = spy()
const mockEvent1 = new MockEvent1()
const mockEvent2 = new MockEvent2(3)
emitter.on("example1 example2", listener)
emitter.emit(mockEvent1)
emitter.emit(mockEvent2)
expect(listener).to.have.been.called.twice
expect(listener).to.have.been.first.called.with.exactly(mockEvent1)
expect(listener).to.have.been.second.called.with.exactly(mockEvent2)
})
it("is able to have listeners removed", function() {
const emitter = new MockEmitter()
const listener = spy()
emitter.on("example1", listener)
const removedFromEventItDoesntListenTo = emitter.off("example2", listener)
const removedFromEventItListensTo = emitter.off("example1", listener)
const removedFromEventASecondTime = emitter.off("example1", listener)
expect(removedFromEventItDoesntListenTo).to.be.false
expect(removedFromEventItListensTo).to.be.true
expect(removedFromEventASecondTime).to.be.false
emitter.on("example1 example2", listener)
const removedFromBothEvents = emitter.off("example1 example2", listener)
expect(removedFromBothEvents).to.be.true
emitter.on("example1", listener)
const removedFromOneOfTheEvents = emitter.off("example1 example2", listener)
expect(removedFromOneOfTheEvents).to.be.true
})
it("is able to have one listener's listening to a single event removed when said listener listens to multiple", function() {
const emitter = new MockEmitter()
const listener = spy()
const mockEvent1 = new MockEvent1()
const mockEvent2 = new MockEvent2(3)
emitter.on("example1 example2", listener)
// Disable listener for example1
emitter.off("example1", listener)
emitter.emit(mockEvent1)
emitter.emit(mockEvent2)
expect(listener).to.have.been.called.once
expect(listener).to.also.have.been.called.with.exactly(mockEvent2)
})
it("is not able to emit or add/remove listeners for inexistant events", function() {
const emitter = new MockEmitter()
const listener = spy()
expect(() => emitter.on("inexistant", listener)).to.throw(Error)
expect(() => emitter.off("inexistant", listener)).to.throw(Error)
expect(() => emitter.emit(new BaseEvent("inexistant"))).to.throw(Error)
})
it("isn't able to emit non-events", function() {
const emitter = new MockEmitter()
expect(() => emitter.emit("not-an-event")).to.throw(Error)
})
})

View file

@ -1,58 +0,0 @@
/**
* 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 { MockLatex } from "../mock/latex.mjs"
import { MockHelper } from "../mock/helper.mjs"
import {
CanvasInterface,
DialogInterface,
HelperInterface,
Interface,
LatexInterface,
RootInterface
} from "../../src/module/interface.mjs"
import { MockDialog } from "../mock/dialog.mjs"
import { MockRootElement } from "../mock/root.mjs"
import { MockCanvas } from "../mock/canvas.mjs"
describe("Interfaces", function() {
describe("#interface methods", function() {
it("throws an error when called directly", function() {
const obj = new CanvasInterface()
expect(() => obj.markDirty()).to.throw(Error)
})
})
describe("#checkImplementation", function() {
it("validates the implementation of mocks", function() {
const checkMockLatex = () => Interface.checkImplementation(LatexInterface, new MockLatex())
const checkMockHelper = () => Interface.checkImplementation(HelperInterface, new MockHelper())
const checkMockDialog = () => Interface.checkImplementation(DialogInterface, new MockDialog())
const checkMockRoot = () => Interface.checkImplementation(RootInterface, new MockRootElement())
const checkMockCanvas = () => Interface.checkImplementation(CanvasInterface, new MockCanvas())
expect(checkMockLatex).to.not.throw()
expect(checkMockHelper).to.not.throw()
expect(checkMockDialog).to.not.throw()
expect(checkMockRoot).to.not.throw()
expect(checkMockCanvas).to.not.throw()
})
})
})

View file

@ -1,231 +0,0 @@
/**
* 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/>.
*/
// Load prior tests
import { describe, it } from "mocha"
import { expect } from "chai"
import * as Polyfill from "../../src/lib/expr-eval/polyfill.mjs"
import {
andOperator,
cbrt,
equal,
expm1,
hypot,
lessThan,
log1p,
log2,
notEqual
} from "../../src/lib/expr-eval/polyfill.mjs"
describe("Math/Polyfill", () => {
describe("#AADDDD", function() {
it("should add two numbers", function() {
expect(Polyfill.add(2, 3)).to.equal(5)
expect(Polyfill.add("2", "3")).to.equal(5)
})
})
describe("#sub", function() {
it("should subtract two numbers", function() {
expect(Polyfill.sub(2, 1)).to.equal(1)
expect(Polyfill.sub("2", "1")).to.equal(1)
})
})
describe("#mul", function() {
it("should multiply two numbers", function() {
expect(Polyfill.mul(2, 3)).to.equal(6)
expect(Polyfill.mul("2", "3")).to.equal(6)
})
})
describe("#div", function() {
it("should divide two numbers", function() {
expect(Polyfill.div(10, 2)).to.equal(5)
expect(Polyfill.div("10", "2")).to.equal(5)
})
})
describe("#mod", function() {
it("should return the modulo of two numbers", function() {
expect(Polyfill.mod(10, 3)).to.equal(1)
expect(Polyfill.mod("10", "3")).to.equal(1)
})
})
describe("#concat", function() {
it("should return the concatenation of two strings", function() {
expect(Polyfill.concat(10, 3)).to.equal("103")
expect(Polyfill.concat("abc", "def")).to.equal("abcdef")
})
})
describe("#equal", function() {
it("should return whether its two arguments are equal", function() {
expect(Polyfill.equal(10, 3)).to.be.false
expect(Polyfill.equal(10, 10)).to.be.true
expect(Polyfill.equal("abc", "def")).to.be.false
expect(Polyfill.equal("abc", "abc")).to.be.true
})
})
describe("#notEqual", function() {
it("should return whether its two arguments are not equal", function() {
expect(Polyfill.notEqual(10, 3)).to.be.true
expect(Polyfill.notEqual(10, 10)).to.be.false
expect(Polyfill.notEqual("abc", "def")).to.be.true
expect(Polyfill.notEqual("abc", "abc")).to.be.false
})
})
describe("#greaterThan", function() {
it("should return whether its first argument is strictly greater than its second", function() {
expect(Polyfill.greaterThan(10, 3)).to.be.true
expect(Polyfill.greaterThan(10, 10)).to.be.false
expect(Polyfill.greaterThan(10, 30)).to.be.false
})
})
describe("#lessThan", function() {
it("should return whether its first argument is strictly less than its second", function() {
expect(Polyfill.lessThan(10, 3)).to.be.false
expect(Polyfill.lessThan(10, 10)).to.be.false
expect(Polyfill.lessThan(10, 30)).to.be.true
})
})
describe("#greaterThanEqual", function() {
it("should return whether its first argument is greater or equal to its second", function() {
expect(Polyfill.greaterThanEqual(10, 3)).to.be.true
expect(Polyfill.greaterThanEqual(10, 10)).to.be.true
expect(Polyfill.greaterThanEqual(10, 30)).to.be.false
})
})
describe("#lessThanEqual", function() {
it("should return whether its first argument is strictly less than its second", function() {
expect(Polyfill.lessThanEqual(10, 3)).to.be.false
expect(Polyfill.lessThanEqual(10, 10)).to.be.true
expect(Polyfill.lessThanEqual(10, 30)).to.be.true
})
})
describe("#andOperator", function() {
it("should return whether its arguments are both true", function() {
expect(Polyfill.andOperator(true, true)).to.be.true
expect(Polyfill.andOperator(true, false)).to.be.false
expect(Polyfill.andOperator(false, true)).to.be.false
expect(Polyfill.andOperator(false, false)).to.be.false
expect(Polyfill.andOperator(10, 3)).to.be.true
expect(Polyfill.andOperator(10, 0)).to.be.false
expect(Polyfill.andOperator(0, 0)).to.be.false
})
})
describe("#orOperator", function() {
it("should return whether one of its arguments is true", function() {
expect(Polyfill.orOperator(true, true)).to.be.true
expect(Polyfill.orOperator(true, false)).to.be.true
expect(Polyfill.orOperator(false, true)).to.be.true
expect(Polyfill.orOperator(false, false)).to.be.false
expect(Polyfill.orOperator(10, 3)).to.be.true
expect(Polyfill.orOperator(10, 0)).to.be.true
expect(Polyfill.orOperator(0, 0)).to.be.false
})
})
describe("#inOperator", function() {
it("should check if second argument contains first", function() {
expect(Polyfill.inOperator("a", ["a", "b", "c"])).to.be.true
expect(Polyfill.inOperator(3, [0, 1, 2])).to.be.false
expect(Polyfill.inOperator(3, [0, 1, 3, 2])).to.be.true
expect(Polyfill.inOperator("a", "abcdef")).to.be.true
expect(Polyfill.inOperator("a", "bcdefg")).to.be.false
})
})
describe("#sinh, #cosh, #tanh, #asinh, #acosh, #atanh", function() {
const EPSILON = 1e-12
for(let x = -.9; x < 1; x += 0.1) {
expect(Polyfill.sinh(x)).to.be.approximately(Math.sinh(x), EPSILON)
expect(Polyfill.cosh(x)).to.be.approximately(Math.cosh(x), EPSILON)
expect(Polyfill.tanh(x)).to.be.approximately(Math.tanh(x), EPSILON)
expect(Polyfill.asinh(x)).to.be.approximately(Math.asinh(x), EPSILON)
expect(Polyfill.atanh(x)).to.be.approximately(Math.atanh(x), EPSILON)
}
for(let x = 1.1; x < 10; x += 0.1) {
expect(Polyfill.sinh(x)).to.be.approximately(Math.sinh(x), EPSILON)
expect(Polyfill.cosh(x)).to.be.approximately(Math.cosh(x), EPSILON)
expect(Polyfill.tanh(x)).to.be.approximately(Math.tanh(x), EPSILON)
expect(Polyfill.asinh(x)).to.be.approximately(Math.asinh(x), EPSILON)
expect(Polyfill.acosh(x)).to.be.approximately(Math.acosh(x), EPSILON)
expect(Polyfill.log10(x)).to.be.approximately(Math.log10(x), EPSILON)
}
})
describe("#trunc", function() {
it("should return the decimal part of floats", function() {
for(let x = -10; x < 10; x += 0.1)
expect(Polyfill.trunc(x)).to.equal(Math.trunc(x))
})
})
describe("#gamma", function() {
it("should return the product of factorial(x - 1)", function() {
expect(Polyfill.gamma(0)).to.equal(Infinity)
expect(Polyfill.gamma(1)).to.equal(1)
expect(Polyfill.gamma(2)).to.equal(1)
expect(Polyfill.gamma(3)).to.equal(2)
expect(Polyfill.gamma(4)).to.equal(6)
expect(Polyfill.gamma(5)).to.equal(24)
expect(Polyfill.gamma(172)).to.equal(Infinity)
expect(Polyfill.gamma(172.3)).to.equal(Infinity)
expect(Polyfill.gamma(.2)).to.approximately(4.590_843_712, 1e-8)
expect(Polyfill.gamma(5.5)).to.be.approximately(52.34277778, 1e-8)
expect(Polyfill.gamma(90.2)).to.equal(4.0565358202825355e+136)
})
})
describe("#hypot", function() {
it("should return the hypothenus length of a triangle whose length are provided in arguments", function() {
for(let x = 0; x < 10; x += 0.3) {
expect(Polyfill.hypot(x)).to.be.approximately(Math.hypot(x), Number.EPSILON)
for(let y = 0; y < 10; y += 0.3) {
expect(Polyfill.hypot(x, y)).to.be.approximately(Math.hypot(x, y), Number.EPSILON)
}
}
})
})
describe("#sign, #cbrt, #exmp1", function() {
for(let x = -10; x < 10; x += 0.3) {
expect(Polyfill.sign(x)).to.approximately(Math.sign(x), 1e-12)
expect(Polyfill.cbrt(x)).to.approximately(Math.cbrt(x), 1e-12)
expect(Polyfill.expm1(x)).to.approximately(Math.expm1(x), 1e-12)
}
})
describe("#log1p, #log2", function() {
for(let x = 1; x < 10; x += 0.3) {
expect(Polyfill.log1p(x)).to.be.approximately(Math.log1p(x), 1e-12)
expect(Polyfill.log2(x)).to.be.approximately(Math.log2(x), 1e-12)
}
})
})

View file

@ -1,183 +0,0 @@
/**
* 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 { textsup, textsub, parseName, getRandomColor, escapeHTML, exponentsToExpression } from "../../src/utils.mjs"
import { describe, it } from "mocha"
import { expect } from "chai"
describe("Lib/PrototypeExtensions", function() {
describe("#String.toLatinUppercase", function() {
it("transforms latin characters from strings to their uppercase version", function() {
expect("abc".toLatinUppercase()).to.equal("ABC")
expect("abCd".toLatinUppercase()).to.equal("ABCD")
expect("ab123cd456".toLatinUppercase()).to.equal("AB123CD456")
expect("ABC".toLatinUppercase()).to.equal("ABC")
})
it("does not transform non latin characters to their uppercase version", function() {
expect("abαπ".toLatinUppercase()).to.equal("ABαπ")
expect("abαπ".toLatinUppercase()).to.not.equal("abαπ".toUpperCase())
})
})
describe("#String.removeEnclosure", function() {
it("is able to remove the first and last characters", function() {
expect("[1+t]".removeEnclosure()).to.equal("1+t")
expect('"a+b+c*d"'.removeEnclosure()).to.equal("a+b+c*d")
expect("(pi/2)".removeEnclosure()).to.equal("pi/2")
})
})
describe("#Number.toDecimalPrecision", function() {
it("rounds a number to a fixed decimal precision", function() {
expect(123.456789.toDecimalPrecision()).to.equal(123)
expect(123.456789.toDecimalPrecision(1)).to.equal(123.5)
expect(123.456789.toDecimalPrecision(2)).to.equal(123.46)
expect(123.456789.toDecimalPrecision(3)).to.equal(123.457)
expect(123.456789.toDecimalPrecision(4)).to.equal(123.4568)
expect(123.456789.toDecimalPrecision(5)).to.equal(123.45679)
expect(123.456789.toDecimalPrecision(6)).to.equal(123.456789)
expect(123.111111.toDecimalPrecision(5)).to.equal(123.11111)
})
})
})
describe("Lib/Utils", function() {
describe("#textsup", function() {
it("transforms characters which have a sup unicode equivalent", function() {
expect(textsup("-+=()")).to.equal("⁻⁺⁼⁽⁾")
expect(textsup("0123456789")).to.equal("⁰¹²³⁴⁵⁶⁷⁸⁹")
expect(textsup("abcdefghijklmnoprstuvwxyz")).to.equal("ᵃᵇᶜᵈᵉᶠᵍʰⁱʲᵏˡᵐⁿᵒᵖʳˢᵗᵘᵛʷˣʸᶻ")
})
it("does not transform characters without a sup equivalent", function() {
expect(textsup("ABCDEFGHIJKLMNOPQRSTUVWXYZq")).to.equal("ABCDEFGHIJKLMNOPQRSTUVWXYZq")
})
it("partially transforms strings which only have a few characters with a sup equivalent", function() {
expect(textsup("ABCabcABC")).to.equal("ABCᵃᵇᶜABC")
})
})
describe("#textsub", function() {
it("transforms characters which have a sub unicode equivalent", function() {
expect(textsub("-+=()")).to.equal("₋₊₌₍₎")
expect(textsub("0123456789")).to.equal("₀₁₂₃₄₅₆₇₈₉")
expect(textsub("aehijklmnoprstuvx")).to.equal("ₐₑₕᵢⱼₖₗₘₙₒₚᵣₛₜᵤᵥₓ")
})
it("does not transform characters without a sub equivalent", function() {
expect(textsub("ABCDEFGHIJKLMNOPQRSTUVWXYZ")).to.equal("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
expect(textsub("bcdfgqyz")).to.equal("bcdfgqyz")
})
it("partially transforms strings which only have a few characters with a sub equivalent", function() {
expect(textsub("ABC123ABC")).to.equal("ABC₁₂₃ABC")
})
})
describe("#parseName", function() {
it("parses greek letter names", function() {
const shorthands = {
"α": ["al", "alpha"],
"β": ["be", "beta"],
"γ": ["ga", "gamma"],
"δ": ["de", "delta"],
"ε": ["ep", "epsilon"],
"ζ": ["ze", "zeta"],
"η": ["et", "eta"],
"θ": ["th", "theta"],
"κ": ["ka", "kappa"],
"λ": ["la", "lambda"],
"μ": ["mu"],
"ν": ["nu"],
"ξ": ["xi"],
"ρ": ["rh", "rho"],
"σ": ["si", "sigma"],
"τ": ["ta", "tau"],
"υ": ["up", "upsilon"],
"φ": ["ph", "phi"],
"χ": ["ch", "chi"],
"ψ": ["ps", "psi"],
"ω": ["om", "omega"],
"Γ": ["gga", "ggamma"],
"Δ": ["gde", "gdelta"],
"Θ": ["gth", "gtheta"],
"Λ": ["gla", "glambda"],
"Ξ": ["gxi"],
"Π": ["gpi"],
"Σ": ["gsi", "gsigma"],
"Φ": ["gph", "gphi"],
"Ψ": ["gps", "gpsi"],
"Ω": ["gom", "gomega"],
}
for(const [char, shorts] of Object.entries(shorthands)) {
expect(parseName(char)).to.equal(char)
for(const short of shorts)
expect(parseName(short)).to.equal(char)
}
})
it("parses array elements into sub", function() {
expect(parseName("u[n+1]")).to.equal("uₙ₊₁")
expect(parseName("u[(n+x)]")).to.equal("u₍ₙ₊ₓ₎")
expect(parseName("u[A]")).to.equal("uA")
})
it("removes disallowed characters when indicated", function() {
const disallowed = "xnπ\\∪∩[] ()^/^/÷*×+=1234567890¹²³⁴⁵⁶⁷⁸⁹⁰-"
expect(parseName(disallowed)).to.equal("")
expect(parseName("AA" + disallowed)).to.equal("AA")
expect(parseName(disallowed, false)).to.equal(disallowed)
})
it("is able to do all three at once", function() {
expect(parseName("al[n+1]+n")).to.equal("αₙ₊₁")
expect(parseName("al[n+1]+n", false)).to.equal("αₙ₊₁+n")
})
})
describe("#getRandomColor", function() {
it("provides a valid color", function() {
const colorReg = /^#[A-F\d]{6}$/
for(let i = 0; i < 50; i++)
expect(getRandomColor()).to.match(colorReg)
})
})
describe("#escapeHTML", function() {
it("escapes ampersands", function() {
expect(escapeHTML("&")).to.equal("&amp;")
expect(escapeHTML("High & Mighty")).to.equal("High &amp; Mighty")
})
it("escapes injected HTML tags", function() {
expect(escapeHTML("<script>alert('Injected!')</script>")).to.equal("&lt;script&gt;alert('Injected!')&lt;/script&gt;")
expect(escapeHTML('<a href="javascript:alert()">Link</a>')).to.equal('&lt;a href="javascript:alert()"&gt;Link&lt;/a&gt;')
})
})
describe("#exponentsToExpression", function() {
it("transforms exponents to power expression", function() {
expect(exponentsToExpression("x¹²³⁴⁵⁶⁷⁸⁹⁰")).to.equal("x^1234567890")
expect(exponentsToExpression("x¹²+y³⁴")).to.equal("x^12+y^34")
})
})
})

View file

@ -15,22 +15,16 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as fs from "./mock/fs.mjs" import * as fs from "./mock/fs.mjs";
import Qt from "./mock/qt.mjs" import Qt from "./mock/qt.mjs";
import { MockHelper } from "./mock/helper.mjs" import { MockHelper } from "./mock/helper.mjs";
import { MockLatex } from "./mock/latex.mjs" import { MockLatex } from "./mock/latex.mjs";
import Modules from "../src/module/index.mjs";
import { use } from "chai"
import spies from "chai-spies"
import promised from "chai-as-promised"
function setup() { function setup() {
use(promised)
const { spy } = use(spies)
globalThis.Helper = new MockHelper() globalThis.Helper = new MockHelper()
globalThis.Latex = new MockLatex() globalThis.Latex = new MockLatex()
globalThis.chaiPlugins = { spy } Modules.Latex.initialize({ latex: Latex, helper: Helper })
} }
setup() setup()

View file

@ -19,46 +19,46 @@
import { describe, it } from "mocha" import { describe, it } from "mocha"
import { expect } from "chai" import { expect } from "chai"
// import { Domain, parseDomainSimple } from "../../src/math/domain.mjs" import { Domain, parseDomainSimple } from "../../src/math/domain.mjs"
//
// describe("math.domain", function() { describe("math.domain", function() {
// describe("#parseDomainSimple", function() { describe("#parseDomainSimple", function() {
// it("returns predefined domains", function() { it("returns predefined domains", function() {
// const predefinedToCheck = [ const predefinedToCheck = [
// // Real domains // Real domains
// { domain: Domain.R, shortcuts: ["R", ""] }, { domain: Domain.R, shortcuts: ["R", ""] },
// // Zero exclusive real domains // Zero exclusive real domains
// { domain: Domain.RE, shortcuts: ["RE", "R*", "*"] }, { domain: Domain.RE, shortcuts: ["RE", "R*", "*"] },
// // Real positive domains // Real positive domains
// { domain: Domain.RP, shortcuts: ["RP", "R+", "ℝ⁺", "+"] }, { domain: Domain.RP, shortcuts: ["RP", "R+", "ℝ⁺", "+"] },
// // Zero-exclusive real positive domains // Zero-exclusive real positive domains
// { domain: Domain.RPE, shortcuts: ["RPE", "REP", "R+*", "R*+", "*⁺", "ℝ⁺*", "*+", "+*"] }, { domain: Domain.RPE, shortcuts: ["RPE", "REP", "R+*", "R*+", "*⁺", "ℝ⁺*", "*+", "+*"] },
// // Real negative domain // Real negative domain
// { domain: Domain.RM, shortcuts: ["RM", "R-", "ℝ⁻", "-"] }, { domain: Domain.RM, shortcuts: ["RM", "R-", "ℝ⁻", "-"] },
// // Zero-exclusive real negative domains // Zero-exclusive real negative domains
// { domain: Domain.RME, shortcuts: ["RME", "REM", "R-*", "R*-", "ℝ⁻*", "*⁻", "-*", "*-"] }, { domain: Domain.RME, shortcuts: ["RME", "REM", "R-*", "R*-", "ℝ⁻*", "*⁻", "-*", "*-"] },
// // Natural integers domain // Natural integers domain
// { domain: Domain.N, shortcuts: ["", "N", "ZP", "Z+", "ℤ⁺", "+"] }, { domain: Domain.N, shortcuts: ["", "N", "ZP", "Z+", "ℤ⁺", "+"] },
// // Zero-exclusive natural integers domain // Zero-exclusive natural integers domain
// { domain: Domain.NE, shortcuts: ["NE", "NP", "N*", "N+", "*", "ℕ⁺", "+", "ZPE", "ZEP", "Z+*", "Z*+", "ℤ⁺*", "*⁺", "+*", "*+"] }, { domain: Domain.NE, shortcuts: ["NE", "NP", "N*", "N+", "*", "ℕ⁺", "+", "ZPE", "ZEP", "Z+*", "Z*+", "ℤ⁺*", "*⁺", "+*", "*+"] },
// // Logarithmic natural domains // Logarithmic natural domains
// { domain: Domain.NLog, shortcuts: ["NLOG", "ℕˡᵒᵍ", "LOG"] }, { domain: Domain.NLog, shortcuts: ["NLOG", "ℕˡᵒᵍ", "LOG"] },
// // All integers domains // All integers domains
// { domain: Domain.Z, shortcuts: ["Z", ""] }, { domain: Domain.Z, shortcuts: ["Z", ""] },
// // Zero-exclusive all integers domain // Zero-exclusive all integers domain
// { domain: Domain.ZE, shortcuts: ["ZE", "Z*", "*"] }, { domain: Domain.ZE, shortcuts: ["ZE", "Z*", "*"] },
// // Negative integers domain // Negative integers domain
// { domain: Domain.ZM, shortcuts: ["ZM", "Z-", "ℤ⁻", "-"] }, { domain: Domain.ZM, shortcuts: ["ZM", "Z-", "ℤ⁻", "-"] },
// // Zero-exclusive negative integers domain // Zero-exclusive negative integers domain
// { domain: Domain.ZME, shortcuts: ["ZME", "ZEM", "Z-*", "Z*-", "ℤ⁻*", "*⁻", "-*", "*-"] }, { domain: Domain.ZME, shortcuts: ["ZME", "ZEM", "Z-*", "Z*-", "ℤ⁻*", "*⁻", "-*", "*-"] },
// ] ]
//
// // Real domains // Real domains
// for(const { domain, shortcuts } of predefinedToCheck) for(const { domain, shortcuts } of predefinedToCheck)
// for(const shortcut of shortcuts) for(const shortcut of shortcuts)
// expect(parseDomainSimple(shortcut)).to.be.equal(domain) expect(parseDomainSimple(shortcut)).to.be.equal(domain)
// }) })
//
// it("") it("")
// }) })
// }) })

View file

@ -1,59 +0,0 @@
/**
* 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 class MockCanvas {
constructor(mockLoading = false) {
this.mockLoading = mockLoading
}
getContext(context) {
throw new Error("MockCanvas.getContext not implemented")
}
markDirty(rect) {
this.requestPaint()
}
loadImageAsync(image) {
return new Promise((resolve, reject) => {
resolve()
})
}
/**
* Image loading is instantaneous.
* @param {string} image
* @return {boolean}
*/
isImageLoading(image) {
return this.mockLoading
}
/**
* Image loading is instantaneous.
* @param {string} image
* @return {boolean}
*/
isImageLoaded(image) {
return !this.mockLoading
}
requestPaint() {
}
}

View file

@ -1,23 +0,0 @@
/**
* 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 class MockDialog {
constructor() {}
show() {}
}

View file

@ -22,8 +22,7 @@ const DEFAULT_SETTINGS = {
"check_for_updates": true, "check_for_updates": true,
"reset_redo_stack": true, "reset_redo_stack": true,
"last_install_greet": "0", "last_install_greet": "0",
"enable_latex": true, "enable_latex": false,
"enable_latex_async": true,
"expression_editor": { "expression_editor": {
"autoclose": true, "autoclose": true,
"colorize": true, "colorize": true,
@ -54,13 +53,7 @@ export class MockHelper {
this.__settings = { ...DEFAULT_SETTINGS } this.__settings = { ...DEFAULT_SETTINGS }
} }
__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|number|boolean} Value of the setting
*/
getSetting(settingName) {
const namespace = settingName.split(".") const namespace = settingName.split(".")
let data = this.__settings let data = this.__settings
for(const name of namespace) for(const name of namespace)
@ -71,12 +64,7 @@ export class MockHelper {
return data return data
} }
/** __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|number|boolean} value
*/
setSetting(settingName, value) {
const namespace = settingName.split(".") const namespace = settingName.split(".")
const finalName = namespace.pop() const finalName = namespace.pop()
let data = this.__settings let data = this.__settings
@ -88,6 +76,60 @@ export class MockHelper {
data[finalName] = value 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 * Sends data to be written
* @param {string} file * @param {string} file

View file

@ -22,7 +22,6 @@ const PIXEL = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAACklEQVR4AWNgAAAAA
export class MockLatex { export class MockLatex {
constructor() { constructor() {
this.supportsAsyncRender = true
} }
/** /**
@ -56,23 +55,13 @@ export class MockLatex {
return `${TMP}/${name}.png` 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 {Promise<string>} - Comma separated data of the image (source, width, height)
*/
async renderAsync(markup, fontSize, color) {
return this.renderSync(markup, fontSize, color)
}
/** /**
* @param {string} markup - LaTeX markup to render * @param {string} markup - LaTeX markup to render
* @param {number} fontSize - Font size (in pt) to render * @param {number} fontSize - Font size (in pt) to render
* @param {string} color - Color of the text to render * @param {string} color - Color of the text to render
* @returns {string} - Comma separated data of the image (source, width, height) * @returns {string} - Comma separated data of the image (source, width, height)
*/ */
renderSync(markup, fontSize, color) { render(markup, fontSize, color) {
const file = this.__getFileName(markup, fontSize, color) const file = this.__getFileName(markup, fontSize, color)
writeFileSync(file, PIXEL, "base64") writeFileSync(file, PIXEL, "base64")
return `${file},1,1` return `${file},1,1`

View file

@ -1,61 +0,0 @@
/**
* 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 for root element with width and height property.
* setWidth, setHeight, getWidth, and getHeight methods can be spied on to check
* when the accessor is called.
*/
export class MockRootElement {
#width = 0
#height = 0
constructor() {}
setWidth(width) {
this.#width = width;
}
getWidth() {
return this.#width
}
setHeight(height) {
this.#height = height;
}
getHeight() {
return this.#height
}
get width() {
return this.getWidth()
}
set width(value) {
this.setWidth(value)
}
get height() {
return this.getHeight()
}
set height(value) {
this.setHeight(value)
}
}

View file

@ -1,89 +0,0 @@
/**
* 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/>.
*/
// Load prior tests
import "../basics/events.mjs"
import "../basics/interface.mjs"
import { describe, it } from "mocha"
import { expect } from "chai"
import { MockDialog } from "../mock/dialog.mjs"
import { BOOLEAN, DialogInterface, FUNCTION, NUMBER, STRING } from "../../src/module/interface.mjs"
import { Module } from "../../src/module/common.mjs"
class MockModule extends Module {
constructor() {
super("mock", {
number: NUMBER,
bool: BOOLEAN,
str: STRING,
func: FUNCTION,
dialog: DialogInterface
})
}
}
describe("Module/Base", function() {
it("defined a Modules global", function() {
expect(globalThis.Modules).to.not.be.undefined
})
it("is not be initialized upon construction", function() {
const module = new MockModule()
expect(module.name).to.equal("mock")
expect(module.initialized).to.be.false
})
it("is only be initialized with the right arguments", function() {
const module = new MockModule()
const initializeWithNoArg = () => module.initialize({})
const initializeWithSomeArg = () => module.initialize({ number: 0, str: "" })
const initializeWithWrongType = () => module.initialize({
number: () => {},
str: 0,
bool: "",
func: false,
dialog: null
})
const initializeProperly = () => module.initialize({
number: 0,
str: "",
bool: true,
func: FUNCTION,
dialog: new MockDialog()
})
expect(initializeWithNoArg).to.throw(Error)
expect(initializeWithSomeArg).to.throw(Error)
expect(initializeWithWrongType).to.throw(Error)
expect(initializeProperly).to.not.throw(Error)
expect(module.initialized).to.be.true
})
it("cannot be initialized twice", function() {
const module = new MockModule()
const initialize = () => module.initialize({
number: 0,
str: "",
bool: true,
func: FUNCTION,
dialog: new MockDialog()
})
expect(initialize).to.not.throw(Error)
expect(initialize).to.throw(Error)
})
})

View file

@ -1,389 +0,0 @@
/**
* 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/>.
*/
// Load prior tests
import "./base.mjs"
import { describe, it } from "mocha"
import { expect } from "chai"
import ExprEval from "../../src/module/expreval.mjs"
describe("Module/ExprEval", function() {
describe("#parse.evaluate", function() {
const evaluate = (expr, vals = {}) => ExprEval.parse(expr).evaluate(vals)
it("parses simple mathematical expressions", function() {
expect(evaluate(`"\\'\\"\\\\\\/\\b\\f\\n\\r\\t\\u3509"`)).to.equal(`'"\\/\b\f\n\r\t\u3509`)
expect(evaluate("1")).to.equal(1)
expect(evaluate(" 1 ")).to.equal(1)
expect(evaluate("0xFF")).to.equal(255)
expect(evaluate("0b11")).to.equal(3)
expect(evaluate("-1")).to.equal(-1)
expect(evaluate("-(-1)")).to.equal(1)
expect(evaluate("+(-1)")).to.equal(-1)
expect(evaluate("3!")).to.equal(6)
expect(evaluate("1+1")).to.equal(2)
expect(evaluate("4*3")).to.equal(12)
expect(evaluate("4•3")).to.equal(12)
expect(evaluate("64/4")).to.equal(16)
expect(evaluate("2^10")).to.equal(1024)
expect(evaluate("10%3")).to.equal(1)
expect(evaluate("10%3")).to.equal(1)
// Test priorities
expect(evaluate("10*10+10*10")).to.equal(200)
expect(evaluate("10/10+10/10")).to.equal(2)
expect(evaluate("10/10+10/10")).to.equal(2)
expect(evaluate("2^2-2^2")).to.equal(0)
expect(evaluate("(2^2-2)^2")).to.equal(4)
})
it("parses equality and test statements", function() {
expect(evaluate("10%3 == 1 ? 2 : 1")).to.equal(2)
expect(evaluate("not(10%3 == 1) ? 2 : 1")).to.equal(1)
expect(evaluate("10%3 != 1 ? 2 : 1")).to.equal(1)
expect(evaluate("10 < 3 ? 2 : 1")).to.equal(1)
expect(evaluate("10 > 3 ? (2+1) : 1")).to.equal(3)
expect(evaluate("10 <= 3 ? 4 : 1")).to.equal(1)
expect(evaluate("10 >= 3 ? 4 : 1")).to.equal(4)
// Check equality
expect(evaluate("10 < 10 ? 2 : 1")).to.equal(1)
expect(evaluate("10 > 10 ? 2 : 1")).to.equal(1)
expect(evaluate("10 <= 10 ? 4 : 1")).to.equal(4)
expect(evaluate("10 >= 10 ? 4 : 1")).to.equal(4)
// Check 'and' and 'or'
expect(evaluate("10 <= 3 and 10 < 10 ? 4 : 1")).to.equal(1)
expect(evaluate("10 <= 10 and 10 < 10 ? 4 : 1")).to.equal(1)
expect(evaluate("10 <= 10 and 10 < 20 ? 4 : 1")).to.equal(4)
expect(evaluate("10 <= 3 or 10 < 10 ? 4 : 1")).to.equal(1)
expect(evaluate("10 <= 10 or 10 < 10 ? 4 : 1")).to.equal(4)
expect(evaluate("10 <= 10 or 10 < 20 ? 4 : 1")).to.equal(4)
})
it("parses singular function operators (functions with one arguments and no parenthesis)", function() {
// Trigonometric functions
expect(evaluate("sin 0")).to.be.approximately(0, Number.EPSILON)
expect(evaluate("cos 0")).to.be.approximately(1, Number.EPSILON)
expect(evaluate("tan 0")).to.be.approximately(0, Number.EPSILON)
expect(evaluate("asin 1")).to.be.approximately(Math.PI / 2, Number.EPSILON)
expect(evaluate("acos 1")).to.be.approximately(0, Number.EPSILON)
expect(evaluate("atan 1")).to.be.approximately(Math.PI / 4, Number.EPSILON)
expect(evaluate("sinh 1")).to.be.approximately(Math.sinh(1), Number.EPSILON)
expect(evaluate("cosh 1")).to.be.approximately(Math.cosh(1), Number.EPSILON)
expect(evaluate("tanh 1")).to.be.approximately(Math.tanh(1), Number.EPSILON)
expect(evaluate("asinh 1")).to.be.approximately(Math.asinh(1), Number.EPSILON)
expect(evaluate("acosh 1")).to.be.approximately(Math.acosh(1), Number.EPSILON)
expect(evaluate("atanh 0.5")).to.be.approximately(Math.atanh(0.5), Number.EPSILON)
// Reverse trigonometric
expect(evaluate("asin sin 1")).to.be.approximately(1, Number.EPSILON)
expect(evaluate("acos cos 1")).to.be.approximately(1, Number.EPSILON)
expect(evaluate("atan tan 1")).to.be.approximately(1, Number.EPSILON)
expect(evaluate("asinh sinh 1")).to.be.approximately(1, Number.EPSILON)
expect(evaluate("acosh cosh 1")).to.be.approximately(1, Number.EPSILON)
expect(evaluate("atanh tanh 1")).to.be.approximately(1, Number.EPSILON)
// Other functions
expect(evaluate("sqrt 4")).to.be.approximately(2, Number.EPSILON)
expect(evaluate("sqrt 2")).to.be.approximately(Math.sqrt(2), Number.EPSILON)
expect(evaluate("cbrt 27")).to.be.approximately(3, Number.EPSILON)
expect(evaluate("cbrt 14")).to.be.approximately(Math.cbrt(14), Number.EPSILON)
expect(evaluate("log 1")).to.be.approximately(Math.log(1), Number.EPSILON)
expect(evaluate("ln 1")).to.be.approximately(Math.log(1), Number.EPSILON)
expect(evaluate("log2 8")).to.be.approximately(3, Number.EPSILON)
expect(evaluate("log10 100")).to.be.approximately(2, Number.EPSILON)
expect(evaluate("lg 100")).to.be.approximately(2, Number.EPSILON)
expect(evaluate("expm1 0")).to.be.approximately(0, Number.EPSILON)
expect(evaluate("expm1 10")).to.be.approximately(Math.expm1(10), Number.EPSILON)
expect(evaluate("log1p 0")).to.be.approximately(0, Number.EPSILON)
expect(evaluate("log1p 10")).to.be.approximately(Math.log1p(10), Number.EPSILON)
// Roundings/Sign transformations
expect(evaluate("abs -12.34")).to.equal(12.34)
expect(evaluate("abs 12.45")).to.equal(12.45)
expect(evaluate("ceil 12.45")).to.equal(13)
expect(evaluate("ceil 12.75")).to.equal(13)
expect(evaluate("ceil 12.0")).to.equal(12)
expect(evaluate("ceil -12.6")).to.equal(-12)
expect(evaluate("floor 12.45")).to.equal(12)
expect(evaluate("floor 12.75")).to.equal(12)
expect(evaluate("floor 12.0")).to.equal(12)
expect(evaluate("floor -12.2")).to.equal(-13)
expect(evaluate("round 12.45")).to.equal(12)
expect(evaluate("round 12.75")).to.equal(13)
expect(evaluate("round 12.0")).to.equal(12)
expect(evaluate("round -12.2")).to.equal(-12)
expect(evaluate("round -12.6")).to.equal(-13)
expect(evaluate("trunc 12.45")).to.equal(12)
expect(evaluate("trunc 12.75")).to.equal(12)
expect(evaluate("trunc 12.0")).to.equal(12)
expect(evaluate("trunc -12.2")).to.equal(-12)
expect(evaluate("exp 1")).to.be.approximately(Math.E, Number.EPSILON)
expect(evaluate("exp 10")).to.be.approximately(Math.pow(Math.E, 10), 1e-8)
expect(evaluate("length \"string\"")).to.equal(6)
expect(evaluate("sign 0")).to.equal(0)
expect(evaluate("sign -0")).to.equal(0)
expect(evaluate("sign -10")).to.equal(-1)
expect(evaluate("sign 80")).to.equal(1)
})
it("parses regular functions", function() {
for(let i = 0; i < 1000; i++) {
expect(evaluate("random()")).to.be.within(0, 1)
expect(evaluate("random(100)")).to.be.within(0, 100)
}
expect(evaluate("fac(3)")).to.equal(6)
expect(evaluate("fac(10)")).to.equal(3628800)
expect(evaluate("min(10, 20)")).to.equal(10)
expect(evaluate("min(-10, -20)")).to.equal(-20)
expect(evaluate("max(10, 20)")).to.equal(20)
expect(evaluate("max(-10, -20)")).to.equal(-10)
expect(evaluate("hypot(3, 4)")).to.equal(5)
expect(evaluate("pyt(30, 40)")).to.equal(50)
expect(evaluate("atan2(1, 1)")).to.be.approximately(Math.PI / 4, Number.EPSILON)
expect(evaluate("atan2(1, 0)")).to.be.approximately(Math.PI / 2, Number.EPSILON)
expect(evaluate("atan2(0, 1)")).to.be.approximately(0, Number.EPSILON)
expect(evaluate("if(10 == 10, 1, 0)")).to.be.approximately(1, Number.EPSILON)
expect(evaluate("if(10 != 10, 1, 0)")).to.be.approximately(0, Number.EPSILON)
expect(evaluate("gamma(10) == 9!")).to.be.true
expect(evaluate("Γ(30) == 29!")).to.be.true
expect(evaluate("Γ(25) == 23!")).to.be.false
expect(evaluate("roundTo(26.04)")).to.equal(26)
expect(evaluate("roundTo(26.04, 2)")).to.equal(26.04)
expect(evaluate("roundTo(26.04836432123, 5)")).to.equal(26.04836)
expect(evaluate("roundTo(26.04836432123, 5)")).to.equal(26.04836)
})
it("parses arrays and access their members", function() {
expect(evaluate("[6, 7, 9]")).to.have.lengthOf(3)
expect(evaluate("[6, 7, 9]")).to.deep.equal([6, 7, 9])
expect(evaluate("[6, \"8\", 9]")).to.have.lengthOf(3)
expect(evaluate("[6, 7%2]")).to.deep.equal([6, 1])
// Access array indices
expect(evaluate("[6, 7][1]")).to.equal(7)
expect(evaluate("[6, 7, 8, 9, 10][2*2-1]")).to.equal(9)
})
it("can apply functions to arrays", function() {
expect(evaluate("length [6, 7, 9]")).to.equal(3)
expect(evaluate("length [6, 7, 8, 9]")).to.equal(4)
expect(evaluate("[6, 7, 9]||[10,11,12]")).to.deep.equal([6, 7, 9, 10, 11, 12])
expect(evaluate("6 in [6, 7, 9]")).to.be.true
expect(evaluate("2 in [6, 7, 9]")).to.be.false
expect(evaluate("min([10, 6, 7, 8, 9])")).to.equal(6)
expect(evaluate("max([6, 7, 8, 9, 2])")).to.equal(9)
})
it("throws errors when invalid function parameters are provided", function() {
expect(() => evaluate("max()")).to.throw()
expect(() => evaluate("min()")).to.throw()
})
it("parses constants", function() {
expect(evaluate("pi")).to.equal(Math.PI)
expect(evaluate("PI")).to.equal(Math.PI)
expect(evaluate("π")).to.equal(Math.PI)
expect(evaluate("e")).to.equal(Math.E)
expect(evaluate("E")).to.equal(Math.E)
expect(evaluate("true")).to.be.true
expect(evaluate("false")).to.be.false
// expect(evaluate("∞")).to.equal(Math.Infinity)
// expect(evaluate("infinity")).to.equal(Math.Infinity)
// expect(evaluate("Infinity")).to.equal(Math.Infinity)
})
it("can be provided variables", function() {
const u = [1, 2, 3, 4]
const x = 10
const s_ = "string"
const f = (x) => x * 2
expect(evaluate("u", { u })).to.deep.equal([...u])
expect(evaluate("x", { x })).to.equal(x)
expect(evaluate("s_", { s_ })).to.equal(s_)
expect(evaluate("f", { f })).to.equal(f)
expect(evaluate("b", { b: true })).to.equal(true)
expect(evaluate("u[1]", { u })).to.equal(u[1])
expect(evaluate("x/2", { x })).to.equal(x / 2)
expect(evaluate("f(2)", { f })).to.equal(f(2))
expect(evaluate("if(x == f(2), u[0], s_)", { x, u, s_, f })).to.equal(s_)
})
it("can be provided objects", function() {
const obj = { execute: (x) => x * 3, x: 10, y: { cached: true, execute: () => 20 } }
expect(evaluate("O(3)+O(2)", { O: obj })).to.equal(9 + 6)
expect(evaluate("O.x+O.y", { O: obj })).to.equal(30)
})
it("throws errors when trying to use variables wrongly", function() {
const obj = { execute: (x) => x * 3 }
expect(() => evaluate("O()", { O: obj })).to.throw()
expect(() => evaluate("O.x", { O: obj })).to.throw()
expect(() => evaluate("x()", { x: 10 })).to.throw()
expect(() => evaluate("x")).to.throw()
expect(() => evaluate("n")).to.throw()
})
it("can do it all at once", function() {
const obj = { execute: (x) => x * 3, x: 20 }
const u = [1, 2, 3, 4]
const x = 10
const s = "string"
const expr = "random(e) <= e ? fac(x)+u[2]+O(pi) : O.x+length s"
expect(evaluate(expr, { x, u, s, O: obj })).to.equal(3628803 + obj.execute(Math.PI))
})
it("cannot parse invalid expressions", function() {
expect(() => evaluate("1+")).to.throw()
expect(() => evaluate("@")).to.throw()
expect(() => evaluate("]")).to.throw()
expect(() => evaluate("")).to.throw()
expect(() => evaluate(`"\\u35P2"`)).to.throw()
expect(() => evaluate(`"\\x"`)).to.throw()
})
})
describe("#parse.toString", function() {
it("can be converted back into a string without changes", function() {
const expressions = ["pi+2*(e+2)^4", "sin(1+2!+pi+cos -3)^2", "[2,3,4][(2-1)*2]", "true ? false : true"]
for(const ogString of expressions) {
const expr = ExprEval.parse(ogString)
const convertedString = expr.toString()
expect(ExprEval.parse(convertedString)).to.deep.equal(expr) // Can be reparsed just the same
}
})
})
describe("#parse.substitute", function() {
const parsed = ExprEval.parse("if(x == 0, 1, 2+x)")
it("can substitute a variable for a number", function() {
expect(parsed.substitute("x", 10).evaluate({})).to.equal(12)
expect(parsed.substitute("x", 0).evaluate({})).to.equal(1)
})
it("can substitute a variable for another", function() {
expect(parsed.substitute("x", "b").evaluate({ b: 10 })).to.equal(12)
expect(parsed.substitute("x", "b").evaluate({ b: 0 })).to.equal(1)
})
it("can substitute a variable for an expression", function() {
expect(parsed.substitute("x", "sin α").evaluate({ "α": Math.PI / 2 })).to.be.approximately(3, Number.EPSILON)
expect(parsed.substitute("x", "sin α").evaluate({ "α": 0 })).to.equal(1)
expect(parsed.substitute("x", "α == 1 ? 0 : 1").evaluate({ "α": 1 })).to.equal(1)
})
})
describe("#parse.variables", function() {
it("can list all parsed undefined variables", function() {
expect(ExprEval.parse("a+b+x+pi+sin(b)").variables()).to.deep.equal(["a", "b", "x"])
})
})
describe("#parse.toJSFunction", function() {
const func = ExprEval.parse("not(false) ? a+b+x+1/x : x!+random()+A.x+[][0]").toJSFunction("x", { a: "10", b: "0" })
expect(func(10)).to.equal(20.1)
expect(func(20)).to.equal(30.05)
})
describe("#integral", function() {
it("returns the integral value between two integers", function() {
expect(ExprEval.integral(0, 1, "1", "t")).to.be.approximately(1, Number.EPSILON)
expect(ExprEval.integral(0, 1, "t", "t")).to.be.approximately(1 / 2, Number.EPSILON)
expect(ExprEval.integral(0, 1, "t^2", "t")).to.be.approximately(1 / 3, Number.EPSILON)
expect(ExprEval.integral(0, 1, "t^3", "t")).to.be.approximately(1 / 4, 0.01)
expect(ExprEval.integral(0, 1, "t^4", "t")).to.be.approximately(1 / 5, 0.01)
expect(ExprEval.integral(10, 40, "1", "t")).to.equal(30)
expect(ExprEval.integral(20, 40, "1", "t")).to.equal(20)
expect(ExprEval.integral(0, 10, { execute: (x) => 1 })).to.equal(10)
expect(ExprEval.integral(0, 10, { execute: (x) => x })).to.equal(50)
expect(ExprEval.integral(0, 1, { execute: (x) => Math.pow(x, 2) })).to.equal(1 / 3)
})
it("throws error when provided with invalid arguments", function() {
const noArg1 = () => ExprEval.integral()
const noArg2 = () => ExprEval.integral(0)
const noFunction = () => ExprEval.integral(0, 1)
const invalidObjectProvided = () => ExprEval.integral(0, 1, { a: 2 })
const notAnObjectProvided = () => ExprEval.integral(0, 1, "string")
const invalidFromProvided = () => ExprEval.integral("ze", 1, "t^2", "t")
const invalidToProvided = () => ExprEval.integral(0, "ze", "t^2", "t")
const notStringProvided1 = () => ExprEval.integral(0, 1, { a: 2 }, { b: 1 })
const notStringProvided2 = () => ExprEval.integral(0, 1, { a: 2 }, "t")
const notStringProvided3 = () => ExprEval.integral(0, 1, "t^2", { b: 1 })
const invalidVariableProvided = () => ExprEval.integral(0, 1, "t^2", "93IO74")
const invalidExpressionProvided = () => ExprEval.integral(0, 1, "t^2t", "t")
const invalidVariableInExpression = () => ExprEval.integral(0, 1, "t^2+x", "t")
expect(noArg1).to.throw()
expect(noArg2).to.throw()
expect(noFunction).to.throw()
expect(invalidObjectProvided).to.throw()
expect(invalidFromProvided).to.throw()
expect(invalidToProvided).to.throw()
expect(notAnObjectProvided).to.throw()
expect(notStringProvided1).to.throw()
expect(notStringProvided2).to.throw()
expect(notStringProvided3).to.throw()
expect(invalidVariableProvided).to.throw()
expect(invalidExpressionProvided).to.throw()
expect(invalidVariableInExpression).to.throw()
})
})
describe("#derivative", function() {
const DELTA = 1e-5
it("returns the derivative value of a function at a given number", function() {
expect(ExprEval.derivative("1", "t", 2)).to.be.approximately(0, DELTA)
expect(ExprEval.derivative("t", "t", 2)).to.be.approximately(1, DELTA)
expect(ExprEval.derivative("t^2", "t", 2)).to.be.approximately(4, DELTA)
expect(ExprEval.derivative("t^3", "t", 2)).to.be.approximately(12, DELTA)
expect(ExprEval.derivative("t^4", "t", 2)).to.be.approximately(32, DELTA)
expect(ExprEval.derivative({ execute: (x) => 1 }, 10)).to.equal(0)
expect(ExprEval.derivative({ execute: (x) => x }, 10)).to.be.approximately(1, DELTA)
expect(ExprEval.derivative({ execute: (x) => Math.pow(x, 2) }, 10)).to.be.approximately(20, DELTA)
})
it("throws error when provided with invalid arguments", function() {
const noArg1 = () => ExprEval.derivative()
const noArg2 = () => ExprEval.derivative("1")
const noValue1 = () => ExprEval.derivative("0", "1")
const noValue2 = () => ExprEval.derivative({ execute: (x) => 1 })
const invalidObjectProvided = () => ExprEval.derivative({ a: 2 }, 1)
const notAnObjectProvided = () => ExprEval.derivative("string", 1)
const invalidXProvided = () => ExprEval.derivative("t^2+x", "t", "ze")
const notStringProvided1 = () => ExprEval.derivative({ a: 2 }, { b: 1 }, 1)
const notStringProvided2 = () => ExprEval.derivative({ a: 2 }, "t", 1)
const notStringProvided3 = () => ExprEval.derivative("t^2", { b: 1 }, 1)
const invalidVariableProvided = () => ExprEval.derivative("t^2", "93IO74", 1)
const invalidExpressionProvided = () => ExprEval.derivative("t^2t", "t", 1)
const invalidVariableInExpression = () => ExprEval.derivative("t^2+x", "t", 1)
expect(noArg1).to.throw()
expect(noArg2).to.throw()
expect(noValue1).to.throw()
expect(noValue2).to.throw()
expect(invalidObjectProvided).to.throw()
expect(invalidXProvided).to.throw()
expect(notAnObjectProvided).to.throw()
expect(notStringProvided1).to.throw()
expect(notStringProvided2).to.throw()
expect(notStringProvided3).to.throw()
expect(invalidVariableProvided).to.throw()
expect(invalidExpressionProvided).to.throw()
expect(invalidVariableInExpression).to.throw()
})
})
})

View file

@ -1,231 +0,0 @@
/**
* 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/>.
*/
// Load prior tests
import "../basics/utils.mjs"
import "./base.mjs"
import "./expreval.mjs"
import { describe, it } from "mocha"
import { expect } from "chai"
import { existsSync } from "../mock/fs.mjs"
const { spy } = chaiPlugins
import ExprEval from "../../src/module/expreval.mjs"
import LatexAPI from "../../src/module/latex.mjs"
describe("Module/Latex", function() {
it("is defined as a global", function() {
expect(globalThis.Modules.Latex).to.equal(LatexAPI)
})
describe("#initialize", function() {
it("isn't enabled before initialization", function() {
expect(LatexAPI.enabled).to.be.false
})
it("is enabled after initialization", function() {
LatexAPI.initialize({ latex: Latex, helper: Helper })
expect(LatexAPI.enabled).to.equal(Helper.getSetting("enable_latex"))
expect(LatexAPI.initialized).to.be.true
})
})
describe("#requestAsyncRender", function() {
it("should return a render result with a valid source, a width, and a height", async function() {
const data = await LatexAPI.requestAsyncRender("\\frac{x}{3}", 13, "#AA0033")
expect(data).to.be.an("object")
expect(data.source).to.be.a("string")
expect(data.source).to.satisfy(existsSync)
expect(data.height).to.be.a("number")
expect(data.width).to.be.a("number")
})
it("should call functions from the LaTeX module", async function() {
const renderSyncSpy = spy.on(Latex, "renderSync")
const renderAsyncSpy = spy.on(Latex, "renderAsync")
Latex.supportsAsyncRender = true
await LatexAPI.requestAsyncRender("\\frac{x}{3}", 13, "#AA0033")
expect(renderAsyncSpy).to.have.been.called.once
expect(renderSyncSpy).to.have.been.called.once // Called async
Latex.supportsAsyncRender = false
await LatexAPI.requestAsyncRender("\\frac{x}{3}", 13, "#AA0033")
expect(renderAsyncSpy).to.have.been.called.once // From the time before
expect(renderSyncSpy).to.have.been.called.twice
Latex.supportsAsyncRender = true
})
it("should not reply with the same source for different markup, font size, or color.", async function() {
const datas = [
await LatexAPI.requestAsyncRender("\\frac{x}{3}", 13, "#AA0033"),
await LatexAPI.requestAsyncRender("\\frac{x}{4}", 13, "#AA0033"),
await LatexAPI.requestAsyncRender("\\frac{x}{3}", 14, "#AA0033"),
await LatexAPI.requestAsyncRender("\\frac{x}{3}", 13, "#0033AA")
]
const sources = datas.map(x => x.source)
expect(new Set(sources)).to.have.a.lengthOf(4)
})
})
describe("#findPrerendered", function() {
it("should return the same data as async render for the same markup, font size, and color", async function() {
const data = await LatexAPI.requestAsyncRender("\\frac{x}{3}", 13, "#AA0033")
const found = LatexAPI.findPrerendered("\\frac{x}{3}", 13, "#AA0033")
expect(found).to.not.be.null
expect(found.source).to.equal(data.source)
expect(found.width).to.equal(data.width)
})
it("should return null if the markup hasn't been prerendered with the same markup, font size, and color", async function() {
await LatexAPI.requestAsyncRender("\\frac{x}{3}", 13, "#AA0033")
expect(LatexAPI.findPrerendered("\\frac{y}{3}", 13, "#AA0033")).to.be.null
expect(LatexAPI.findPrerendered("\\frac{x}{3}", 12, "#AA0033")).to.be.null
expect(LatexAPI.findPrerendered("\\frac{x}{3}", 13, "#3300AA")).to.be.null
})
})
describe("#par", function() {
it("should add parentheses to strings", function() {
expect(LatexAPI.par("string")).to.equal("(string)")
expect(LatexAPI.par("aaaa")).to.equal("(aaaa)")
expect(LatexAPI.par("")).to.equal("()")
expect(LatexAPI.par("(example)")).to.equal("((example))")
})
})
describe("#parif", function() {
it("should add parentheses to strings that contain one of the ones in the list", function() {
expect(LatexAPI.parif("string", ["+"])).to.equal("string")
expect(LatexAPI.parif("string+assert", ["+"])).to.equal("(string+assert)")
expect(LatexAPI.parif("string+assert", ["+", "-"])).to.equal("(string+assert)")
expect(LatexAPI.parif("string-assert", ["+", "-"])).to.equal("(string-assert)")
})
it("shouldn't add new parentheses to strings that contains one of the ones in the list if they already have one", function() {
expect(LatexAPI.parif("(string+assert", ["+"])).to.equal("((string+assert)")
expect(LatexAPI.parif("string+assert)", ["+"])).to.equal("(string+assert))")
expect(LatexAPI.parif("(string+assert)", ["+"])).to.equal("(string+assert)")
expect(LatexAPI.parif("(string+assert)", ["+", "-"])).to.equal("(string+assert)")
expect(LatexAPI.parif("(string-assert)", ["+", "-"])).to.equal("(string-assert)")
})
it("shouldn't add parentheses to strings that does not contains one of the ones in the list", function() {
expect(LatexAPI.parif("string", ["+"])).to.equal("string")
expect(LatexAPI.parif("string+assert", ["-"])).to.equal("string+assert")
expect(LatexAPI.parif("(string*assert", ["+", "-"])).to.equal("(string*assert")
expect(LatexAPI.parif("string/assert)", ["+", "-"])).to.equal("string/assert)")
})
it("should remove parentheses from strings that does not contains one of the ones in the list", function() {
expect(LatexAPI.parif("(string)", ["+"])).to.equal("string")
expect(LatexAPI.parif("(string+assert)", ["-"])).to.equal("string+assert")
expect(LatexAPI.parif("((string*assert)", ["+", "-"])).to.equal("(string*assert")
expect(LatexAPI.parif("(string/assert))", ["+", "-"])).to.equal("string/assert)")
})
})
describe("#variable", function() {
const from = [
"α", "β", "γ", "δ", "ε", "ζ", "η",
"π", "θ", "κ", "λ", "μ", "ξ", "ρ",
"ς", "σ", "τ", "φ", "χ", "ψ", "ω",
"Γ", "Δ", "Θ", "Λ", "Ξ", "Π", "Σ",
"Φ", "Ψ", "Ω", "ₐ", "ₑ", "ₒ", "ₓ",
"ₕ", "ₖ", "ₗ", "ₘ", "ₙ", "ₚ", "ₛ",
"ₜ", "¹", "²", "³", "⁴", "⁵", "⁶",
"⁷", "⁸", "⁹", "⁰", "₁", "₂", "₃",
"₄", "₅", "₆", "₇", "₈", "₉", "₀",
"pi", "∞"]
const to = [
"\\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"]
it("should convert unicode characters to their latex equivalent", function() {
for(let i = 0; i < from.length; i++)
expect(LatexAPI.variable(from[i])).to.include(to[i])
})
it("should wrap within dollar signs when the option is included", function() {
for(let i = 0; i < from.length; i++) {
expect(LatexAPI.variable(from[i], false)).to.equal(to[i])
expect(LatexAPI.variable(from[i], true)).to.equal(`$${to[i]}$`)
}
})
it("should be able to convert multiple of them", function() {
expect(LatexAPI.variable("α₂", false)).to.equal("\\alpha{}_{2}")
expect(LatexAPI.variable("∞piΠ", false)).to.equal("\\infty\\pi\\Pi")
})
})
describe("#functionToLatex", function() {
it("should transform derivatives into latex fractions", function() {
const d1 = LatexAPI.functionToLatex("derivative", ["'3t'", "'t'", "x+2"])
const d2 = LatexAPI.functionToLatex("derivative", ["f", "x+2"])
expect(d1).to.equal("\\frac{d3x}{dx}")
expect(d2).to.equal("\\frac{df}{dx}(x+2)")
})
it("should transform integrals into latex limits", function() {
const i1 = LatexAPI.functionToLatex("integral", ["0", "x", "'3y'", "'y'"])
const i2 = LatexAPI.functionToLatex("integral", ["1", "2", "f"])
expect(i1).to.equal("\\int\\limits_{0}^{x}3y dy")
expect(i2).to.equal("\\int\\limits_{1}^{2}f(t) dt")
})
it("should transform sqrt functions to sqrt latex", function() {
const sqrt1 = LatexAPI.functionToLatex("sqrt", ["(x+2)"])
const sqrt2 = LatexAPI.functionToLatex("sqrt", ["\\frac{x}{2}"])
expect(sqrt1).to.equal("\\sqrt{x+2}")
expect(sqrt2).to.equal("\\sqrt{\\frac{x}{2}}")
})
it("should transform abs, floor and ceil", function() {
const abs = LatexAPI.functionToLatex("abs", ["x+3"])
const floor = LatexAPI.functionToLatex("floor", ["x+3"])
const ceil = LatexAPI.functionToLatex("ceil", ["x+3"])
expect(abs).to.equal("\\left|x+3\\right|")
expect(floor).to.equal("\\left\\lfloor{x+3}\\right\\rfloor")
expect(ceil).to.equal("\\left\\lceil{x+3}\\right\\rceil")
})
it("should transform regular functions into latex", function() {
const f1 = LatexAPI.functionToLatex("f", ["x+3", true])
const f2 = LatexAPI.functionToLatex("h_1", ["10"])
expect(f1).to.equal("\\mathrm{f}\\left(x+3, true\\right)")
expect(f2).to.equal("\\mathrm{h_1}\\left(10\\right)")
})
})
describe("#expression", function() {
it("should transform parsed expressions", function() {
const expr = ExprEval.parse("(+1! == 2/2 ? sin [-2.2][0] : f(t)^(1+1-1) + sqrt(A.t)) * 3 % 1")
const expected = "((((+1!))==(\\frac{2}{2}) ? (\\mathrm{sin}\\left(([(-2.2)][0])\\right)) : (\\mathrm{f}\\left(t\\right)^{1+1-1}+\\sqrt{A.t})) \\times 3) \\mathrm{mod} 1"
expect(LatexAPI.expression(expr.tokens)).to.equal(expected)
})
})
})

View file

@ -1,30 +0,0 @@
/**
* 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/>.
*/
// Load prior tests
import "./base.mjs"
import "../basics/utils.mjs"
import { describe, it } from "mocha"
import { expect } from "chai"
// import Objects from "../../src/module/objects.mjs"
//
// describe("Module/Objects", function() {
//
// })

View file

@ -1,101 +0,0 @@
/**
* 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/>.
*/
// Load prior tests
import "./base.mjs"
import "../basics/utils.mjs"
import { describe, it } from "mocha"
import { expect } from "chai"
const { spy } = chaiPlugins
import Settings from "../../src/module/settings.mjs"
import { BaseEvent } from "../../src/events.mjs"
describe("Module/Settings", function() {
it("is defined as a global", function() {
expect(globalThis.Modules.Settings).to.equal(Settings)
})
it("has base defined properties even before initialization", function() {
expect(Settings.saveFilename).to.be.a("string")
expect(Settings.xzoom).to.be.a("number")
expect(Settings.yzoom).to.be.a("number")
expect(Settings.xmin).to.be.a("number")
expect(Settings.ymax).to.be.a("number")
expect(Settings.xaxisstep).to.be.a("string")
expect(Settings.yaxisstep).to.be.a("string")
expect(Settings.xlabel).to.be.a("string")
expect(Settings.ylabel).to.be.a("string")
expect(Settings.linewidth).to.be.a("number")
expect(Settings.textsize).to.be.a("number")
expect(Settings.logscalex).to.be.a("boolean")
expect(Settings.showxgrad).to.be.a("boolean")
expect(Settings.showygrad).to.be.a("boolean")
})
it("can be set values, but only of the right type", function() {
expect(() => Settings.set("xzoom", "", false)).to.throw()
expect(() => Settings.set("xlabel", true, false)).to.throw()
expect(() => Settings.set("showxgrad", 2, false)).to.throw()
expect(() => Settings.set("xzoom", 200, false)).to.not.throw()
expect(() => Settings.set("xlabel", "x", false)).to.not.throw()
expect(() => Settings.set("showxgrad", false, false)).to.not.throw()
})
it("cannot be set unknown settings", function() {
expect(() => Settings.set("unknown", "", false)).to.throw()
})
it("sends an event when a value is set", function() {
const listener = spy((e) => {
expect(e).to.be.an.instanceof(BaseEvent)
expect(e.name).to.equal("changed")
expect(e.property).to.equal("xzoom")
expect(e.newValue).to.equal(300)
expect(e.byUser).to.be.true
})
Settings.on("changed", listener)
Settings.set("xzoom", 300, true)
expect(listener).to.have.been.called.once
Settings.off("changed", listener)
})
it("requires a helper to set default values", function() {
spy.on(Settings, "set")
expect(() => Settings.initialize({})).to.throw()
expect(() => Settings.initialize({ helper: globalThis.Helper })).to.not.throw()
expect(Settings.set).to.have.been.called.exactly(13)
expect(Settings.set).to.not.have.been.called.with("saveFilename")
expect(Settings.set).to.have.been.called.with("xzoom")
expect(Settings.set).to.have.been.called.with("yzoom")
expect(Settings.set).to.have.been.called.with("xmin")
expect(Settings.set).to.have.been.called.with("ymax")
expect(Settings.set).to.have.been.called.with("xaxisstep")
expect(Settings.set).to.have.been.called.with("yaxisstep")
expect(Settings.set).to.have.been.called.with("xlabel")
expect(Settings.set).to.have.been.called.with("ylabel")
expect(Settings.set).to.have.been.called.with("linewidth")
expect(Settings.set).to.have.been.called.with("textsize")
expect(Settings.set).to.have.been.called.with("logscalex")
expect(Settings.set).to.have.been.called.with("showxgrad")
expect(Settings.set).to.have.been.called.with("showygrad")
})
})

View file

@ -29,7 +29,7 @@ if not is_release and which('git') is not None:
# Command to check date of latest git commit # Command to check date of latest git commit
cmd = ['git', 'log', '--format=%ci', '-n 1'] cmd = ['git', 'log', '--format=%ci', '-n 1']
cwd = realpath(join(dirname(__file__), '..', '..', '..')) # Root LogarithmPlotter directory. cwd = realpath(join(dirname(__file__), '..')) # Root AccountFree directory.
if exists(join(cwd, '.git')): if exists(join(cwd, '.git')):
date_str = check_output(cmd, cwd=cwd).decode('utf-8').split(' ')[0] date_str = check_output(cmd, cwd=cwd).decode('utf-8').split(' ')[0]
try: try:

View file

@ -17,9 +17,9 @@
""" """
from os import getcwd, chdir, environ, path from os import getcwd, chdir, environ, path
from platform import system as os_name, release as OS_RELEASE from platform import release as os_release
from sys import path as sys_path from sys import path as sys_path
from sys import argv, exit from sys import platform, argv, exit
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from time import time from time import time
@ -49,18 +49,6 @@ from LogarithmPlotter.util.helper import Helper
from LogarithmPlotter.util.latex import Latex from LogarithmPlotter.util.latex import Latex
from LogarithmPlotter.util.js import PyJSValue from LogarithmPlotter.util.js import PyJSValue
OS_NAME = os_name()
CACHE_PATH = {
"Linux": path.join(environ["XDG_CONFIG_HOME"], "LogarithmPlotter")
if "XDG_CONFIG_HOME" in environ else
path.join(path.expanduser("~"), ".cache", "LogarithmPlotter"),
"Windows": path.join(path.expandvars('%APPDATA%'), "LogarithmPlotter", "cache"),
"Darwin": path.join(path.expanduser("~"), "Library", "Caches", "LogarithmPlotter"),
}[OS_NAME]
LINUX_THEMES = { # See https://specifications.freedesktop.org/menu-spec/latest/onlyshowin-registry.html LINUX_THEMES = { # See https://specifications.freedesktop.org/menu-spec/latest/onlyshowin-registry.html
"COSMIC": "Basic", "COSMIC": "Basic",
"GNOME": "Basic", "GNOME": "Basic",
@ -94,10 +82,11 @@ def get_linux_theme() -> str:
def get_platform_qt_style(os) -> str: def get_platform_qt_style(os) -> str:
return { return {
"Linux": get_linux_theme(), "linux": get_linux_theme(),
"Windows": "Universal" if OS_RELEASE() in ["10", "11", "12", "13", "14"] else "Windows", "freebsd": get_linux_theme(),
"Darwin": "macOS", "win32": "Universal" if os_release() in ["10", "11", "12", "13", "14"] else "Windows",
"Android": "Material" "cygwin": "Fusion",
"darwin": "macOS"
}[os] }[os]
@ -158,7 +147,7 @@ def run():
config.init() config.init()
if not 'QT_QUICK_CONTROLS_STYLE' in environ: if not 'QT_QUICK_CONTROLS_STYLE' in environ:
QQuickStyle.setStyle(get_platform_qt_style(OS_NAME)) QQuickStyle.setStyle(get_platform_qt_style(platform))
dep_time = time() dep_time = time()
print("Loaded dependencies in " + str((dep_time - start_time) * 1000) + "ms.") print("Loaded dependencies in " + str((dep_time - start_time) * 1000) + "ms.")
@ -170,12 +159,12 @@ def run():
# Installing macOS file handler. # Installing macOS file handler.
macos_file_open_handler = None macos_file_open_handler = None
if OS_NAME == "Darwin": if platform == "darwin":
macos_file_open_handler = native.MacOSFileOpenHandler() macos_file_open_handler = native.MacOSFileOpenHandler()
app.installEventFilter(macos_file_open_handler) app.installEventFilter(macos_file_open_handler)
helper = Helper(pwd, tmpfile) helper = Helper(pwd, tmpfile)
latex = Latex(CACHE_PATH) latex = Latex(tempdir)
engine, js_globals = create_engine(helper, latex, dep_time) engine, js_globals = create_engine(helper, latex, dep_time)
if len(engine.rootObjects()) == 0: # No root objects loaded if len(engine.rootObjects()) == 0: # No root objects loaded
@ -188,7 +177,7 @@ def run():
js_globals.Modules.IO.loadDiagram(argv[-1]) js_globals.Modules.IO.loadDiagram(argv[-1])
chdir(path.dirname(path.realpath(__file__))) chdir(path.dirname(path.realpath(__file__)))
if OS_NAME == "Darwin": if platform == "darwin":
macos_file_open_handler.init_io(js_globals.Modules.IO) macos_file_open_handler.init_io(js_globals.Modules.IO)
# Check for LaTeX installation if LaTeX support is enabled # Check for LaTeX installation if LaTeX support is enabled

View file

@ -76,16 +76,18 @@ MenuBar {
Action { Action {
text: qsTr("&Undo") text: qsTr("&Undo")
shortcut: StandardKey.Undo shortcut: StandardKey.Undo
onTriggered: Modules.History.undo() onTriggered: history.undo()
icon.name: 'edit-undo' icon.name: 'edit-undo'
icon.color: enabled ? sysPalette.windowText : sysPaletteIn.windowText icon.color: enabled ? sysPalette.windowText : sysPaletteIn.windowText
enabled: history.undoCount > 0
} }
Action { Action {
text: qsTr("&Redo") text: qsTr("&Redo")
shortcut: StandardKey.Redo shortcut: StandardKey.Redo
onTriggered: Modules.History.redo() onTriggered: history.redo()
icon.name: 'edit-redo' icon.name: 'edit-redo'
icon.color: enabled ? sysPalette.windowText : sysPaletteIn.windowText icon.color: enabled ? sysPalette.windowText : sysPaletteIn.windowText
enabled: history.redoCount > 0
} }
Action { Action {
text: qsTr("&Copy plot") text: qsTr("&Copy plot")
@ -117,7 +119,7 @@ MenuBar {
icon.color: sysPalette.buttonText icon.color: sysPalette.buttonText
onTriggered: { onTriggered: {
var newObj = Modules.Objects.createNewRegisteredObject(modelData) var newObj = Modules.Objects.createNewRegisteredObject(modelData)
Modules.History.addToHistory(new JS.HistoryLib.CreateNewObject(newObj.name, modelData, newObj.export())) history.addToHistory(new JS.HistoryLib.CreateNewObject(newObj.name, modelData, newObj.export()))
objectLists.update() objectLists.update()
} }
} }

View file

@ -0,0 +1,222 @@
/**
* 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 QtQuick
import QtQml
import QtQuick.Window
import "../js/index.mjs" as JS
/*!
\qmltype History
\inqmlmodule eu.ad5001.LogarithmPlotter.History
\brief QObject holding persistantly for undo & redo stacks.
\sa HistoryBrowser, HistoryLib
*/
Item {
// Using a QtObject is necessary in order to have proper property propagation in QML
id: historyObj
/*!
\qmlproperty int History::undoCount
Count of undo actions.
*/
property int undoCount: 0
/*!
\qmlproperty int History::redoCount
Count of redo actions.
*/
property int redoCount: 0
/*!
\qmlproperty var History::undoStack
Stack of undo actions.
*/
property var undoStack: []
/*!
\qmlproperty var History::redoStack
Stack of redo actions.
*/
property var redoStack: []
/*!
\qmlproperty bool History::saved
true when no modification was done to the current working file, false otherwise.
*/
property bool saved: true
/*!
\qmlmethod void History::clear()
Clears both undo and redo stacks completly.
*/
function clear() {
undoCount = 0
redoCount = 0
undoStack = []
redoStack = []
}
/*!
\qmlmethod var History::serialize()
Serializes history into JSON-able content.
*/
function serialize() {
let undoSt = [], redoSt = [];
for(let i = 0; i < undoCount; i++)
undoSt.push([
undoStack[i].type(),
undoStack[i].export()
]);
for(let i = 0; i < redoCount; i++)
redoSt.push([
redoStack[i].type(),
redoStack[i].export()
]);
return [undoSt, redoSt]
}
/*!
\qmlmethod void History::unserialize(var undoSt, var redoSt)
Unserializes both \c undoSt stack and \c redoSt stack from serialized content.
*/
function unserialize(undoSt, redoSt) {
clear();
for(let i = 0; i < undoSt.length; i++)
undoStack.push(new JS.HistoryLib.Actions[undoSt[i][0]](...undoSt[i][1]))
for(let i = 0; i < redoSt.length; i++)
redoStack.push(new JS.HistoryLib.Actions[redoSt[i][0]](...redoSt[i][1]))
undoCount = undoSt.length;
redoCount = redoSt.length;
objectLists.update()
}
/*!
\qmlmethod void History::addToHistory(var action)
Adds an instance of HistoryLib.Action to history.
*/
function addToHistory(action) {
if(action instanceof JS.HistoryLib.Action) {
console.log("Added new entry to history: " + action.getReadableString())
undoStack.push(action)
undoCount++;
if(Helper.getSettingBool("reset_redo_stack")) {
redoStack = []
redoCount = 0
}
saved = false
}
}
/*!
\qmlmethod void History::undo(bool updateObjectList = true)
Undoes the HistoryLib.Action at the top of the undo stack and pushes it to the top of the redo stack.
By default, will update the graph and the object list. This behavior can be disabled by setting the \c updateObjectList to false.
*/
function undo(updateObjectList = true) {
if(undoStack.length > 0) {
var action = undoStack.pop()
action.undo()
if(updateObjectList)
objectLists.update()
redoStack.push(action)
undoCount--;
redoCount++;
saved = false
}
}
/*!
\qmlmethod void History::redo(bool updateObjectList = true)
Redoes the HistoryLib.Action at the top of the redo stack and pushes it to the top of the undo stack.
By default, will update the graph and the object list. This behavior can be disabled by setting the \c updateObjectList to false.
*/
function redo(updateObjectList = true) {
if(redoStack.length > 0) {
var action = redoStack.pop()
action.redo()
if(updateObjectList)
objectLists.update()
undoStack.push(action)
undoCount++;
redoCount--;
saved = false
}
}
/*!
\qmlmethod void History::undoMultipleDefered(int toUndoCount)
Undoes several HistoryLib.Action at the top of the undo stack and pushes them to the top of the redo stack.
It undoes them deferedly to avoid overwhelming the computer while creating a cool short accelerated summary of all changes.
*/
function undoMultipleDefered(toUndoCount) {
undoTimer.toUndoCount = toUndoCount;
undoTimer.start()
if(toUndoCount > 0)
saved = false
}
/*!
\qmlmethod void History::redoMultipleDefered(int toRedoCount)
Redoes several HistoryLib.Action at the top of the redo stack and pushes them to the top of the undo stack.
It redoes them deferedly to avoid overwhelming the computer while creating a cool short accelerated summary of all changes.
*/
function redoMultipleDefered(toRedoCount) {
redoTimer.toRedoCount = toRedoCount;
redoTimer.start()
if(toRedoCount > 0)
saved = false
}
Timer {
id: undoTimer
interval: 5; running: false; repeat: true
property int toUndoCount: 0
onTriggered: {
if(toUndoCount > 0) {
historyObj.undo(toUndoCount % 4 == 1) // Only redraw once every 4 changes.
toUndoCount--;
} else {
running = false;
}
}
}
Timer {
id: redoTimer
interval: 5; running: false; repeat: true
property int toRedoCount: 0
onTriggered: {
if(toRedoCount > 0) {
historyObj.redo(toRedoCount % 4 == 1) // Only redraw once every 4 changes.
toRedoCount--;
} else {
running = false;
}
}
}
Component.onCompleted: {
Modules.History.initialize({
historyObj,
themeTextColor: sysPalette.windowText.toString(),
imageDepth: Screen.devicePixelRatio,
fontSize: 14
})
}
}

View file

@ -16,8 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
pragma ComponentBehavior: Bound
import QtQuick.Controls import QtQuick.Controls
import QtQuick import QtQuick
import eu.ad5001.LogarithmPlotter.Setting 1.0 as Setting import eu.ad5001.LogarithmPlotter.Setting 1.0 as Setting
@ -49,18 +47,6 @@ Item {
*/ */
property bool darkTheme: isDarkTheme() property bool darkTheme: isDarkTheme()
/*!
\qmlproperty int HistoryBrowser::undoCount
Number of actions in the undo stack.
*/
property int undoCount: 0
/*!
\qmlproperty int HistoryBrowser::redoCount
Number of actions in the redo stack.
*/
property int redoCount: 0
Setting.TextSetting { Setting.TextSetting {
id: filterInput id: filterInput
anchors.left: parent.left anchors.left: parent.left
@ -90,22 +76,19 @@ Item {
id: redoColumn id: redoColumn
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
width: historyBrowser.actionWidth width: actionWidth
Repeater { Repeater {
model: historyBrowser.redoCount model: history.redoCount
HistoryItem { HistoryItem {
id: redoButton id: redoButton
width: historyBrowser.actionWidth width: actionWidth
//height: actionHeight //height: actionHeight
isRedo: true isRedo: true
idx: index
darkTheme: historyBrowser.darkTheme darkTheme: historyBrowser.darkTheme
hidden: !(filterInput.value == "" || content.includes(filterInput.value)) hidden: !(filterInput.value == "" || content.includes(filterInput.value))
onClicked: {
redoTimer.toRedoCount = Modules.History.redoStack.length-index
redoTimer.start()
}
} }
} }
} }
@ -118,14 +101,14 @@ Item {
transform: Rotation { origin.x: 30; origin.y: 30; angle: 270} transform: Rotation { origin.x: 30; origin.y: 30; angle: 270}
height: 70 height: 70
width: 20 width: 20
visible: historyBrowser.redoCount > 0 visible: history.redoCount > 0
} }
Rectangle { Rectangle {
id: nowRect id: nowRect
anchors.right: parent.right anchors.right: parent.right
anchors.top: redoColumn.bottom anchors.top: redoColumn.bottom
width: historyBrowser.actionWidth width: actionWidth
height: 40 height: 40
color: sysPalette.highlight color: sysPalette.highlight
Text { Text {
@ -141,24 +124,20 @@ Item {
id: undoColumn id: undoColumn
anchors.right: parent.right anchors.right: parent.right
anchors.top: nowRect.bottom anchors.top: nowRect.bottom
width: historyBrowser.actionWidth width: actionWidth
Repeater { Repeater {
model: historyBrowser.undoCount model: history.undoCount
HistoryItem { HistoryItem {
id: undoButton id: undoButton
width: historyBrowser.actionWidth width: actionWidth
//height: actionHeight //height: actionHeight
isRedo: false isRedo: false
idx: index
darkTheme: historyBrowser.darkTheme darkTheme: historyBrowser.darkTheme
hidden: !(filterInput.value == "" || content.includes(filterInput.value)) hidden: !(filterInput.value == "" || content.includes(filterInput.value))
onClicked: {
undoTimer.toUndoCount = +index+1
undoTimer.start()
}
} }
} }
} }
@ -171,39 +150,7 @@ Item {
transform: Rotation { origin.x: 30; origin.y: 30; angle: 270} transform: Rotation { origin.x: 30; origin.y: 30; angle: 270}
height: 60 height: 60
width: 20 width: 20
visible: historyBrowser.undoCount > 0 visible: history.undoCount > 0
}
}
}
Timer {
id: undoTimer
interval: 5; running: false; repeat: true
property int toUndoCount: 0
onTriggered: {
if(toUndoCount > 0) {
Modules.History.undo()
if(toUndoCount % 3 === 1)
Modules.Canvas.requestPaint()
toUndoCount--;
} else {
running = false;
}
}
}
Timer {
id: redoTimer
interval: 5; running: false; repeat: true
property int toRedoCount: 0
onTriggered: {
if(toRedoCount > 0) {
Modules.History.redo()
if(toRedoCount % 3 === 1)
Modules.Canvas.requestPaint()
toRedoCount--;
} else {
running = false;
} }
} }
} }
@ -216,18 +163,6 @@ Item {
let hex = sysPalette.windowText.toString() let hex = sysPalette.windowText.toString()
// We only check the first parameter, as on all normal OSes, text color is grayscale. // We only check the first parameter, as on all normal OSes, text color is grayscale.
return parseInt(hex.substr(1,2), 16) > 128 return parseInt(hex.substr(1,2), 16) > 128
}
Component.onCompleted: {
Modules.History.initialize({
helper: Helper,
themeTextColor: sysPalette.windowText.toString(),
imageDepth: Screen.devicePixelRatio,
fontSize: 14
})
Modules.History.on("cleared loaded added undone redone", () => {
undoCount = Modules.History.undoStack.length
redoCount = Modules.History.redoStack.length
})
} }
} }

View file

@ -16,8 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick
import Qt5Compat.GraphicalEffects
import eu.ad5001.LogarithmPlotter.Setting 1.0 as Setting import eu.ad5001.LogarithmPlotter.Setting 1.0 as Setting
@ -40,17 +41,17 @@ Button {
\qmlproperty bool HistoryItem::isRedo \qmlproperty bool HistoryItem::isRedo
true if the action is in the redo stack, false othewise. true if the action is in the redo stack, false othewise.
*/ */
required property bool isRedo property bool isRedo
/*! /*!
\qmlproperty int HistoryItem::index \qmlproperty int HistoryItem::idx
Index of the item within the HistoryBrowser list. Index of the item within the HistoryBrowser list.
*/ */
required property int index property int idx
/*! /*!
\qmlproperty bool HistoryItem::darkTheme \qmlproperty bool HistoryItem::darkTheme
true when the system is running with a dark theme, false otherwise. true when the system is running with a dark theme, false otherwise.
*/ */
required property bool darkTheme property bool darkTheme
/*! /*!
\qmlproperty bool HistoryItem::hidden \qmlproperty bool HistoryItem::hidden
true when the item is filtered out, false otherwise. true when the item is filtered out, false otherwise.
@ -60,7 +61,7 @@ Button {
\qmlproperty int HistoryItem::historyAction \qmlproperty int HistoryItem::historyAction
Associated history action. Associated history action.
*/ */
readonly property var historyAction: isRedo ? Modules.History.redoStack.at(index) : Modules.History.undoStack.at(-index-1) readonly property var historyAction: isRedo ? history.redoStack[idx] : history.undoStack[history.undoCount-idx-1]
/*! /*!
\qmlproperty int HistoryItem::actionHeight \qmlproperty int HistoryItem::actionHeight
@ -81,11 +82,12 @@ Button {
height: hidden ? 8 : Math.max(actionHeight, label.height + 15) height: hidden ? 8 : Math.max(actionHeight, label.height + 15)
Rectangle { LinearGradient {
anchors.fill: parent anchors.fill: parent
//opacity: hidden ? 0.6 : 1 //opacity: hidden ? 0.6 : 1
start: Qt.point(0, 0)
end: Qt.point(parent.width, 0)
gradient: Gradient { gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.1; color: "transparent" } GradientStop { position: 0.1; color: "transparent" }
GradientStop { position: 1.5; color: clr } GradientStop { position: 1.5; color: clr }
} }
@ -145,6 +147,13 @@ Button {
ToolTip.visible: hovered ToolTip.visible: hovered
ToolTip.delay: 200 ToolTip.delay: 200
ToolTip.text: content ToolTip.text: content
onClicked: {
if(isRedo)
history.redoMultipleDefered(history.redoCount-idx)
else
history.undoMultipleDefered(+idx+1)
}
} }

View file

@ -1,4 +1,5 @@
module eu.ad5001.LogarithmPlotter.History module eu.ad5001.LogarithmPlotter.History
History 1.0 History.qml
HistoryBrowser 1.0 HistoryBrowser.qml HistoryBrowser 1.0 HistoryBrowser.qml
HistoryItem 1.0 HistoryItem.qml HistoryItem 1.0 HistoryItem.qml

View file

@ -30,15 +30,104 @@ import Qt.labs.platform as Native
*/ */
Canvas { Canvas {
id: canvas id: canvas
anchors.top: parent.top anchors.top: separator.bottom
anchors.left: parent.left anchors.left: parent.left
height: parent.height - 90 height: parent.height - 90
width: parent.width width: parent.width
/*!
\qmlproperty double LogGraphCanvas::xmin
Minimum x of the diagram, provided from settings.
\sa Settings
*/
property double xmin: 0
/*!
\qmlproperty double LogGraphCanvas::ymax
Maximum y of the diagram, provided from settings.
\sa Settings
*/
property double ymax: 0
/*!
\qmlproperty double LogGraphCanvas::xzoom
Zoom on the x axis of the diagram, provided from settings.
\sa Settings
*/
property double xzoom: 10
/*!
\qmlproperty double LogGraphCanvas::yzoom
Zoom on the y axis of the diagram, provided from settings.
\sa Settings
*/
property double yzoom: 10
/*!
\qmlproperty string LogGraphCanvas::xaxisstep
Step of the x axis graduation, provided from settings.
\note: Only available in non-logarithmic mode.
\sa Settings
*/
property string xaxisstep: "4"
/*!
\qmlproperty string LogGraphCanvas::yaxisstep
Step of the y axis graduation, provided from settings.
\sa Settings
*/
property string yaxisstep: "4"
/*!
\qmlproperty string LogGraphCanvas::xlabel
Label used on the x axis, provided from settings.
\sa Settings
*/
property string xlabel: ""
/*!
\qmlproperty string LogGraphCanvas::ylabel
Label used on the y axis, provided from settings.
\sa Settings
*/
property string ylabel: ""
/*!
\qmlproperty double LogGraphCanvas::linewidth
Width of lines that will be drawn into the canvas, provided from settings.
\sa Settings
*/
property double linewidth: 1
/*!
\qmlproperty double LogGraphCanvas::textsize
Font size of the text that will be drawn into the canvas, provided from settings.
\sa Settings
*/
property double textsize: 14
/*!
\qmlproperty bool LogGraphCanvas::logscalex
true if the canvas should be in logarithmic mode, false otherwise.
Provided from settings.
\sa Settings
*/
property bool logscalex: false
/*!
\qmlproperty bool LogGraphCanvas::showxgrad
true if the x graduation should be shown, false otherwise.
Provided from settings.
\sa Settings
*/
property bool showxgrad: false
/*!
\qmlproperty bool LogGraphCanvas::showygrad
true if the y graduation should be shown, false otherwise.
Provided from settings.
\sa Settings
*/
property bool showygrad: false
/*! /*!
\qmlproperty var LogGraphCanvas::imageLoaders \qmlproperty var LogGraphCanvas::imageLoaders
Dictionary of format {image: callback} containing data for deferred image loading. Dictionary of format {image: [callback.image data]} containing data for defered image loading.
*/ */
property var imageLoaders: {} property var imageLoaders: {}
/*!
\qmlproperty var LogGraphCanvas::ctx
Cache for the 2D context so that it may be used asynchronously.
*/
property var ctx
Component.onCompleted: { Component.onCompleted: {
imageLoaders = {} imageLoaders = {}
@ -66,21 +155,9 @@ Canvas {
Object.keys(imageLoaders).forEach((key) => { Object.keys(imageLoaders).forEach((key) => {
if(isImageLoaded(key)) { if(isImageLoaded(key)) {
// Calling callback // Calling callback
imageLoaders[key]() imageLoaders[key][0](canvas, ctx, imageLoaders[key][1])
delete imageLoaders[key] delete imageLoaders[key]
} }
}) })
} }
/*!
\qmlmethod void LogGraphCanvas::loadImageAsync(string imageSource)
Loads an image data onto the canvas asynchronously.
Returns a Promise that is resolved when the image is loaded.
*/
function loadImageAsync(imageSource) {
return new Promise((resolve) => {
this.loadImage(imageSource)
this.imageLoaders[imageSource] = resolve
})
}
} }

View file

@ -42,7 +42,7 @@ ApplicationWindow {
width: 1000 width: 1000
height: 500 height: 500
color: sysPalette.window color: sysPalette.window
title: qsTr("untitled") title: "LogarithmPlotter " + (settings.saveFilename != "" ? " - " + settings.saveFilename.split('/').pop() : "") + (history.saved ? "" : "*")
SystemPalette { id: sysPalette; colorGroup: SystemPalette.Active } SystemPalette { id: sysPalette; colorGroup: SystemPalette.Active }
SystemPalette { id: sysPaletteIn; colorGroup: SystemPalette.Disabled } SystemPalette { id: sysPaletteIn; colorGroup: SystemPalette.Disabled }
@ -51,6 +51,8 @@ ApplicationWindow {
AppMenuBar {id: appMenu} AppMenuBar {id: appMenu}
History { id: history }
Popup.GreetScreen {} Popup.GreetScreen {}
Popup.Preferences {id: preferences} Popup.Preferences {id: preferences}
@ -143,6 +145,20 @@ ApplicationWindow {
width: sidebar.inPortrait ? parent.width : parent.width - sidebar.width//*sidebar.position width: sidebar.inPortrait ? parent.width : parent.width - sidebar.width//*sidebar.position
x: sidebar.width//*sidebar.position x: sidebar.width//*sidebar.position
xmin: settings.xmin
ymax: settings.ymax
xzoom: settings.xzoom
yzoom: settings.yzoom
xlabel: settings.xlabel
ylabel: settings.ylabel
yaxisstep: settings.yaxisstep
xaxisstep: settings.xaxisstep
logscalex: settings.logscalex
linewidth: settings.linewidth
textsize: settings.textsize
showxgrad: settings.showxgrad
showygrad: settings.showygrad
property bool firstDrawDone: false property bool firstDrawDone: false
onPainted: if(!firstDrawDone) { onPainted: if(!firstDrawDone) {
@ -183,7 +199,7 @@ ApplicationWindow {
} }
onClosing: function(close) { onClosing: function(close) {
if(!Modules.IO.saved) { if(!history.saved) {
close.accepted = false close.accepted = false
appMenu.openSaveUnsavedChangesDialog() appMenu.openSaveUnsavedChangesDialog()
} }
@ -248,22 +264,5 @@ ApplicationWindow {
Component.onCompleted: { Component.onCompleted: {
Modules.IO.initialize({ root, settings, alert }) Modules.IO.initialize({ root, settings, alert })
Modules.Latex.initialize({ latex: Latex, helper: Helper }) Modules.Latex.initialize({ latex: Latex, helper: Helper })
Modules.Settings.on("changed", (evt) => {
if(evt.property === "saveFilename") {
const fileName = evt.newValue.split('/').pop().split('\\').pop()
if(fileName !== "")
title = fileName
}
})
Modules.IO.on("saved loaded", (evt) => {
// Refreshing sidebar
updateObjectsLists()
if(title.endsWith("*"))
title = title.substring(0, title.length-1)
})
Modules.IO.on("modified", () => {
if(!title.endsWith("*"))
title = title+"*"
})
} }
} }

View file

@ -80,7 +80,7 @@ Repeater {
variables: propertyType.variables variables: propertyType.variables
onChanged: function(newExpr) { onChanged: function(newExpr) {
if(obj[propertyName].toString() != newExpr.toString()) { if(obj[propertyName].toString() != newExpr.toString()) {
Modules.History.addToHistory(new JS.HistoryLib.EditedProperty( history.addToHistory(new JS.HistoryLib.EditedProperty(
obj.name, objType, propertyName, obj.name, objType, propertyName,
obj[propertyName], newExpr obj[propertyName], newExpr
)) ))
@ -123,7 +123,7 @@ Repeater {
// Ensuring old and new values are different to prevent useless adding to history. // Ensuring old and new values are different to prevent useless adding to history.
if(obj[propertyName] != newValueParsed) { if(obj[propertyName] != newValueParsed) {
Modules.History.addToHistory(new JS.HistoryLib.EditedProperty( history.addToHistory(new JS.HistoryLib.EditedProperty(
obj.name, objType, propertyName, obj.name, objType, propertyName,
obj[propertyName], newValueParsed obj[propertyName], newValueParsed
)) ))
@ -168,7 +168,7 @@ Repeater {
return obj[propertyName] return obj[propertyName]
} }
onClicked: { onClicked: {
Modules.History.addToHistory(new JS.HistoryLib.EditedProperty( history.addToHistory(new JS.HistoryLib.EditedProperty(
obj.name, objType, propertyName, obj.name, objType, propertyName,
obj[propertyName], this.checked obj[propertyName], this.checked
)) ))
@ -209,9 +209,7 @@ Repeater {
if(selectedObj == null) { if(selectedObj == null) {
// Creating new object. // Creating new object.
selectedObj = Modules.Objects.createNewRegisteredObject(propertyType.objType) selectedObj = Modules.Objects.createNewRegisteredObject(propertyType.objType)
Modules.History.addToHistory( history.addToHistory(new JS.HistoryLib.CreateNewObject(selectedObj.name, propertyType.objType, selectedObj.export()))
new JS.HistoryLib.CreateNewObject(selectedObj.name, propertyType.objType, selectedObj.export())
)
baseModel = Modules.Objects.getObjectsName(propertyType.objType).concat( baseModel = Modules.Objects.getObjectsName(propertyType.objType).concat(
isRealObject ? [qsTr("+ Create new %1").arg(Modules.Objects.types[propertyType.objType].displayType())] : isRealObject ? [qsTr("+ Create new %1").arg(Modules.Objects.types[propertyType.objType].displayType())] :
[]) [])
@ -221,14 +219,14 @@ Repeater {
//Modules.Objects.currentObjects[objType][objIndex].requiredBy = obj[propertyName].filter((obj) => obj.name != obj.name) //Modules.Objects.currentObjects[objType][objIndex].requiredBy = obj[propertyName].filter((obj) => obj.name != obj.name)
} }
obj.requiredBy = obj.requiredBy.filter((obj) => obj.name != obj.name) obj.requiredBy = obj.requiredBy.filter((obj) => obj.name != obj.name)
Modules.History.addToHistory(new JS.HistoryLib.EditedProperty( history.addToHistory(new JS.HistoryLib.EditedProperty(
obj.name, objType, propertyName, obj.name, objType, propertyName,
obj[propertyName], selectedObj obj[propertyName], selectedObj
)) ))
obj[propertyName] = selectedObj obj[propertyName] = selectedObj
} else if(baseModel[newIndex] != obj[propertyName]) { } else if(baseModel[newIndex] != obj[propertyName]) {
// Ensuring new property is different to not add useless history entries. // Ensuring new property is different to not add useless history entries.
Modules.History.addToHistory(new JS.HistoryLib.EditedProperty( history.addToHistory(new JS.HistoryLib.EditedProperty(
obj.name, objType, propertyName, obj.name, objType, propertyName,
obj[propertyName], baseModel[newIndex] obj[propertyName], baseModel[newIndex]
)) ))
@ -258,7 +256,7 @@ Repeater {
onChanged: { onChanged: {
var exported = exportModel() var exported = exportModel()
Modules.History.addToHistory(new JS.HistoryLib.EditedProperty( history.addToHistory(new JS.HistoryLib.EditedProperty(
obj.name, objType, propertyName, obj.name, objType, propertyName,
obj[propertyName], exported obj[propertyName], exported
)) ))

View file

@ -112,7 +112,7 @@ Popup.BaseDialog {
if(newName in Modules.Objects.currentObjectsByName) { if(newName in Modules.Objects.currentObjectsByName) {
invalidNameDialog.showDialog(newName) invalidNameDialog.showDialog(newName)
} else { } else {
Modules.History.addToHistory(new JS.HistoryLib.NameChanged( history.addToHistory(new JS.HistoryLib.NameChanged(
objEditor.obj.name, objEditor.objType, newName objEditor.obj.name, objEditor.objType, newName
)) ))
Modules.Objects.renameObject(obj.name, newName) Modules.Objects.renameObject(obj.name, newName)
@ -127,17 +127,13 @@ Popup.BaseDialog {
id: labelContentProperty id: labelContentProperty
height: 30 height: 30
width: dlgProperties.width width: dlgProperties.width
label: qsTranslate("prop", "labelContent") label: qsTr("Label content")
model: [qsTr("null"), qsTr("name"), qsTr("name + value")] model: [qsTr("null"), qsTr("name"), qsTr("name + value")]
property var idModel: ["null", "name", "name + value"] property var idModel: ["null", "name", "name + value"]
icon: "common/label.svg" icon: "common/label.svg"
currentIndex: idModel.indexOf(objEditor.obj.labelContent) currentIndex: idModel.indexOf(objEditor.obj.labelContent)
onActivated: function(newIndex) { onActivated: function(newIndex) {
if(idModel[newIndex] != objEditor.obj.labelContent) { if(idModel[newIndex] != objEditor.obj.labelContent) {
Modules.History.addToHistory(new JS.HistoryLib.EditedProperty(
obj.name, objType, "labelContent",
objEditor.obj.labelContent, idModel[newIndex]
))
objEditor.obj.labelContent = idModel[newIndex] objEditor.obj.labelContent = idModel[newIndex]
objEditor.obj.update() objEditor.obj.update()
objectListList.update() objectListList.update()

View file

@ -104,9 +104,7 @@ Column {
onClicked: { onClicked: {
let newObj = Modules.Objects.createNewRegisteredObject(modelData) let newObj = Modules.Objects.createNewRegisteredObject(modelData)
Modules.History.addToHistory(new JS.HistoryLib.CreateNewObject( history.addToHistory(new JS.HistoryLib.CreateNewObject(newObj.name, modelData, newObj.export()))
newObj.name, modelData, newObj.export()
))
objectLists.update() objectLists.update()
let hasXProp = newObj.constructor.properties().hasOwnProperty('x') let hasXProp = newObj.constructor.properties().hasOwnProperty('x')

View file

@ -71,8 +71,8 @@ ScrollView {
id: typeVisibilityCheckBox id: typeVisibilityCheckBox
checked: Modules.Objects.currentObjects[objType] != undefined ? Modules.Objects.currentObjects[objType].every(obj => obj.visible) : true checked: Modules.Objects.currentObjects[objType] != undefined ? Modules.Objects.currentObjects[objType].every(obj => obj.visible) : true
onClicked: { onClicked: {
for(const obj of Modules.Objects.currentObjects[objType]) obj.visible = this.checked for(var obj of Modules.Objects.currentObjects[objType]) obj.visible = this.checked
for(const obj of objTypeList.editingRows) obj.objVisible = this.checked for(var obj of objTypeList.editingRows) obj.objVisible = this.checked
objectListList.changed() objectListList.changed()
} }

View file

@ -72,7 +72,7 @@ Item {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 5 anchors.leftMargin: 5
onClicked: { onClicked: {
Modules.History.addToHistory(new JS.HistoryLib.EditedVisibility( history.addToHistory(new JS.HistoryLib.EditedVisibility(
obj.name, obj.type, this.checked obj.name, obj.type, this.checked
)) ))
obj.visible = this.checked obj.visible = this.checked
@ -212,7 +212,7 @@ Item {
selectedColor: obj.color selectedColor: obj.color
title: qsTr("Pick new color for %1 %2").arg(obj.constructor.displayType()).arg(obj.name) title: qsTr("Pick new color for %1 %2").arg(obj.constructor.displayType()).arg(obj.name)
onAccepted: { onAccepted: {
Modules.History.addToHistory(new JS.HistoryLib.ColorChanged( history.addToHistory(new JS.HistoryLib.ColorChanged(
obj.name, obj.type, obj.color, selectedColor.toString() obj.name, obj.type, obj.color, selectedColor.toString()
)) ))
obj.color = selectedColor.toString() obj.color = selectedColor.toString()
@ -227,11 +227,11 @@ Item {
function deleteRecursively(object) { function deleteRecursively(object) {
for(let toRemove of object.requiredBy) for(let toRemove of object.requiredBy)
deleteRecursively(toRemove) deleteRecursively(toRemove)
if(Modules.Objects.currentObjectsByName[object.name] !== undefined) { if(Modules.Objects.currentObjectsByName[object.name] != undefined) {
// Object still exists // Object still exists
// Temporary fix for objects require not being propertly updated. // Temporary fix for objects require not being propertly updated.
object.requiredBy = [] object.requiredBy = []
Modules.History.addToHistory(new JS.HistoryLib.DeleteObject( history.addToHistory(new JS.HistoryLib.DeleteObject(
object.name, object.type, object.export() object.name, object.type, object.export()
)) ))
Modules.Objects.deleteObject(object.name) Modules.Objects.deleteObject(object.name)

View file

@ -115,7 +115,7 @@ Item {
let obj = Modules.Objects.currentObjectsByName[objName] let obj = Modules.Objects.currentObjectsByName[objName]
// Set values // Set values
if(parent.userPickX && parent.userPickY) { if(parent.userPickX && parent.userPickY) {
Modules.History.addToHistory(new JS.HistoryLib.EditedPosition( history.addToHistory(new JS.HistoryLib.EditedPosition(
objName, objType, obj[propertyX], newValueX, obj[propertyY], newValueY objName, objType, obj[propertyX], newValueX, obj[propertyY], newValueY
)) ))
obj[propertyX] = newValueX obj[propertyX] = newValueX
@ -124,7 +124,7 @@ Item {
objectLists.update() objectLists.update()
pickerRoot.picked(obj) pickerRoot.picked(obj)
} else if(parent.userPickX) { } else if(parent.userPickX) {
Modules.History.addToHistory(new JS.HistoryLib.EditedProperty( history.addToHistory(new JS.HistoryLib.EditedProperty(
objName, objType, propertyX, obj[propertyX], newValueX objName, objType, propertyX, obj[propertyX], newValueX
)) ))
obj[propertyX] = newValueX obj[propertyX] = newValueX
@ -132,7 +132,7 @@ Item {
objectLists.update() objectLists.update()
pickerRoot.picked(obj) pickerRoot.picked(obj)
} else if(parent.userPickY) { } else if(parent.userPickY) {
Modules.History.addToHistory(new JS.HistoryLib.EditedProperty( history.addToHistory(new JS.HistoryLib.EditedProperty(
objName, objType, propertyY, obj[propertyY], newValueY objName, objType, propertyY, obj[propertyY], newValueY
)) ))
obj[propertyY] = newValueY obj[propertyY] = newValueY
@ -285,7 +285,7 @@ Item {
const axisX = Modules.Canvas.axesSteps.x.value const axisX = Modules.Canvas.axesSteps.x.value
const xpos = Modules.Canvas.px2x(picker.mouseX) const xpos = Modules.Canvas.px2x(picker.mouseX)
if(snapToGridCheckbox.checked) { if(snapToGridCheckbox.checked) {
if(Modules.Settings.logscalex) { if(canvas.logscalex) {
// Calculate the logged power // Calculate the logged power
let pow = Math.pow(10, Math.floor(Math.log10(xpos))) let pow = Math.pow(10, Math.floor(Math.log10(xpos)))
return pow*Math.round(xpos/pow) return pow*Math.round(xpos/pow)

View file

@ -45,17 +45,17 @@ Popup {
property bool changelogNeedsFetching: true property bool changelogNeedsFetching: true
onAboutToShow: if(changelogNeedsFetching) { onAboutToShow: if(changelogNeedsFetching) {
Helper.fetchChangelog().then((fetchedText) => { Helper.fetchChangelog()
changelogNeedsFetching = false }
changelog.text = fetchedText
Connections {
target: Helper
function onChangelogFetched(chl) {
changelogNeedsFetching = false;
changelog.text = chl
changelogView.contentItem.implicitHeight = changelog.height changelogView.contentItem.implicitHeight = changelog.height
}, (error) => { // console.log(changelog.height, changelogView.contentItem.implicitHeight)
const e = qsTranslate("changelog", "Could not fetch update: {}.").replace('{}', error) }
console.error(e)
changelogNeedsFetching = false
changelog.text = e
changelogView.contentItem.implicitHeight = changelog.height
})
} }
ScrollView { ScrollView {

View file

@ -141,7 +141,7 @@ Popup {
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
font.pixelSize: 14 font.pixelSize: 14
text: modelData.name text: modelData.name
wrapMode: Text.Wrap wrapMode: Text.WordWrap
clip: true clip: true
} }
} }

View file

@ -1,11 +1,11 @@
module eu.ad5001.LogarithmPlotter.Popup module eu.ad5001.LogarithmPlotter.Popup
Alert 1.0 Alert.qml
About 1.0 About.qml
BaseDialog 1.0 BaseDialog.qml BaseDialog 1.0 BaseDialog.qml
Changelog 1.0 Changelog.qml About 1.0 About.qml
Alert 1.0 Alert.qml
FileDialog 1.0 FileDialog.qml FileDialog 1.0 FileDialog.qml
GreetScreen 1.0 GreetScreen.qml GreetScreen 1.0 GreetScreen.qml
Changelog 1.0 Changelog.qml
ThanksTo 1.0 ThanksTo.qml
InsertCharacter 1.0 InsertCharacter.qml InsertCharacter 1.0 InsertCharacter.qml
Preferences 1.0 Preferences.qml Preferences 1.0 Preferences.qml
ThanksTo 1.0 ThanksTo.qml

View file

@ -175,17 +175,17 @@ Item {
Icon { Icon {
id: iconLabel id: iconLabel
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: parent.icon == "" ? 0 : 3 anchors.topMargin: icon == "" ? 0 : 3
source: control.visible && parent.icon != "" ? "../icons/" + control.icon : "" source: control.visible && icon != "" ? "../icons/" + control.icon : ""
width: height width: height
height: parent.icon == "" || !visible ? 0 : 24 height: icon == "" || !visible ? 0 : 24
color: sysPalette.windowText color: sysPalette.windowText
} }
Label { Label {
id: labelItem id: labelItem
anchors.left: iconLabel.right anchors.left: iconLabel.right
anchors.leftMargin: parent.icon == "" ? 0 : 5 anchors.leftMargin: icon == "" ? 0 : 5
anchors.top: parent.top anchors.top: parent.top
height: parent.height height: parent.height
width: Math.max(85, implicitWidth) width: Math.max(85, implicitWidth)
@ -221,9 +221,9 @@ Item {
focus: true focus: true
selectByMouse: true selectByMouse: true
property bool autocompleteEnabled: Helper.getSetting("autocompletion.enabled") property bool autocompleteEnabled: Helper.getSettingBool("autocompletion.enabled")
property bool syntaxHighlightingEnabled: Helper.getSetting("expression_editor.colorize") property bool syntaxHighlightingEnabled: Helper.getSettingBool("expression_editor.colorize")
property bool autoClosing: Helper.getSetting("expression_editor.autoclose") property bool autoClosing: Helper.getSettingBool("expression_editor.autoclose")
property var tokens: autocompleteEnabled || syntaxHighlightingEnabled ? parent.tokens(text) : [] property var tokens: autocompleteEnabled || syntaxHighlightingEnabled ? parent.tokens(text) : []
Keys.priority: Keys.BeforeItem // Required for knowing which key the user presses. Keys.priority: Keys.BeforeItem // Required for knowing which key the user presses.
@ -231,8 +231,8 @@ Item {
onEditingFinished: { onEditingFinished: {
if(insertButton.focus || insertPopup.focus) return if(insertButton.focus || insertPopup.focus) return
let value = text let value = text
if(value != "" && value.toString() != parent.defValue) { if(value != "" && value.toString() != defValue) {
let expr = parent.parse(value) let expr = parse(value)
if(expr != null) { if(expr != null) {
control.changed(expr) control.changed(expr)
defValue = expr.toEditableString() defValue = expr.toEditableString()
@ -280,10 +280,10 @@ Item {
acPopupContent.itemSelected = 0 acPopupContent.itemSelected = 0
if(event.text in parent.openAndCloseMatches && autoClosing) { if(event.text in openAndCloseMatches && autoClosing) {
let start = selectionStart let start = selectionStart
insert(selectionStart, event.text) insert(selectionStart, event.text)
insert(selectionEnd, parent.openAndCloseMatches[event.text]) insert(selectionEnd, openAndCloseMatches[event.text])
cursorPosition = start+1 cursorPosition = start+1
event.accepted = true event.accepted = true
} }
@ -600,7 +600,7 @@ Item {
*/ */
function colorize(tokenList) { function colorize(tokenList) {
let parsedText = "" let parsedText = ""
let scheme = colorSchemes[Helper.getSetting("expression_editor.color_scheme")] let scheme = colorSchemes[Helper.getSettingInt("expression_editor.color_scheme")]
for(let token of tokenList) { for(let token of tokenList) {
switch(token.type) { switch(token.type) {
case JS.Parsing.TokenType.VARIABLE: case JS.Parsing.TokenType.VARIABLE:

View file

@ -37,7 +37,7 @@ Item {
Emitted when the value of the text has been changed. Emitted when the value of the text has been changed.
The corresponding handler is \c onChanged. The corresponding handler is \c onChanged.
*/ */
signal changed(var newValue) signal changed(string newValue)
/*! /*!
\qmlproperty bool TextSetting::isInt \qmlproperty bool TextSetting::isInt

View file

@ -1,8 +1,8 @@
module eu.ad5001.LogarithmPlotter.Setting module eu.ad5001.LogarithmPlotter.Setting
AutocompletionCategory 1.0 AutocompletionCategory.qml
ComboBoxSetting 1.0 ComboBoxSetting.qml ComboBoxSetting 1.0 ComboBoxSetting.qml
ExpressionEditor 1.0 ExpressionEditor.qml
Icon 1.0 Icon.qml Icon 1.0 Icon.qml
ListSetting 1.0 ListSetting.qml ListSetting 1.0 ListSetting.qml
TextSetting 1.0 TextSetting.qml TextSetting 1.0 TextSetting.qml
ExpressionEditor 1.0 ExpressionEditor.qml
AutocompletionCategory 1.0 AutocompletionCategory.qml

View file

@ -44,25 +44,25 @@ ScrollView {
Zoom on the x axis of the diagram, provided from settings. Zoom on the x axis of the diagram, provided from settings.
\sa Settings \sa Settings
*/ */
property double xzoom: Helper.getSetting('default_graph.xzoom') property double xzoom: Helper.getSettingInt('default_graph.xzoom')
/*! /*!
\qmlproperty double Settings::yzoom \qmlproperty double Settings::yzoom
Zoom on the y axis of the diagram, provided from settings. Zoom on the y axis of the diagram, provided from settings.
\sa Settings \sa Settings
*/ */
property double yzoom: Helper.getSetting('default_graph.yzoom') property double yzoom: Helper.getSettingInt('default_graph.yzoom')
/*! /*!
\qmlproperty double Settings::xmin \qmlproperty double Settings::xmin
Minimum x of the diagram, provided from settings. Minimum x of the diagram, provided from settings.
\sa Settings \sa Settings
*/ */
property double xmin: Helper.getSetting('default_graph.xmin') property double xmin: Helper.getSettingInt('default_graph.xmin')
/*! /*!
\qmlproperty double Settings::ymax \qmlproperty double Settings::ymax
Maximum y of the diagram, provided from settings. Maximum y of the diagram, provided from settings.
\sa Settings \sa Settings
*/ */
property double ymax: Helper.getSetting('default_graph.ymax') property double ymax: Helper.getSettingInt('default_graph.ymax')
/*! /*!
\qmlproperty string Settings::xaxisstep \qmlproperty string Settings::xaxisstep
Step of the x axis graduation, provided from settings. Step of the x axis graduation, provided from settings.
@ -93,34 +93,39 @@ ScrollView {
Width of lines that will be drawn into the canvas, provided from settings. Width of lines that will be drawn into the canvas, provided from settings.
\sa Settings \sa Settings
*/ */
property double linewidth: Helper.getSetting('default_graph.linewidth') property double linewidth: Helper.getSettingInt('default_graph.linewidth')
/*! /*!
\qmlproperty double Settings::textsize \qmlproperty double Settings::textsize
Font size of the text that will be drawn into the canvas, provided from settings. Font size of the text that will be drawn into the canvas, provided from settings.
\sa Settings \sa Settings
*/ */
property double textsize: Helper.getSetting('default_graph.textsize') property double textsize: Helper.getSettingInt('default_graph.textsize')
/*! /*!
\qmlproperty bool Settings::logscalex \qmlproperty bool Settings::logscalex
true if the canvas should be in logarithmic mode, false otherwise. true if the canvas should be in logarithmic mode, false otherwise.
Provided from settings. Provided from settings.
\sa Settings \sa Settings
*/ */
property bool logscalex: Helper.getSetting('default_graph.logscalex') property bool logscalex: Helper.getSettingBool('default_graph.logscalex')
/*! /*!
\qmlproperty bool Settings::showxgrad \qmlproperty bool Settings::showxgrad
true if the x graduation should be shown, false otherwise. true if the x graduation should be shown, false otherwise.
Provided from settings. Provided from settings.
\sa Settings \sa Settings
*/ */
property bool showxgrad: Helper.getSetting('default_graph.showxgrad') property bool showxgrad: Helper.getSettingBool('default_graph.showxgrad')
/*! /*!
\qmlproperty bool Settings::showygrad \qmlproperty bool Settings::showygrad
true if the y graduation should be shown, false otherwise. true if the y graduation should be shown, false otherwise.
Provided from settings. Provided from settings.
\sa Settings \sa Settings
*/ */
property bool showygrad: Helper.getSetting('default_graph.showygrad') property bool showygrad: Helper.getSettingBool('default_graph.showygrad')
/*!
\qmlproperty bool Settings::saveFilename
Path of the currently opened file. Empty if no file is opened.
*/
property string saveFilename: ""
Column { Column {
spacing: 10 spacing: 10
@ -131,18 +136,15 @@ ScrollView {
id: fdiag id: fdiag
onAccepted: { onAccepted: {
var filePath = fdiag.currentFile.toString().substr(7) var filePath = fdiag.currentFile.toString().substr(7)
Modules.Settings.set("saveFilename", filePath) settings.saveFilename = filePath
if(exportMode) { if(exportMode) {
Modules.IO.saveDiagram(filePath) Modules.IO.saveDiagram(filePath)
} else { } else {
Modules.IO.loadDiagram(filePath) Modules.IO.loadDiagram(filePath)
// Adding labels. if(xAxisLabel.find(settings.xlabel) == -1) xAxisLabel.model.append({text: settings.xlabel})
if(xAxisLabel.find(Modules.Settings.xlabel) === -1) xAxisLabel.editText = settings.xlabel
xAxisLabel.model.append({text: Modules.Settings.xlabel}) if(yAxisLabel.find(settings.ylabel) == -1) yAxisLabel.model.append({text: settings.ylabel})
xAxisLabel.editText = Modules.Settings.xlabel yAxisLabel.editText = settings.ylabel
if(yAxisLabel.find(Modules.Settings.ylabel) === -1)
yAxisLabel.model.append({text: Modules.Settings.ylabel})
yAxisLabel.editText = Modules.Settings.ylabel
} }
} }
} }
@ -156,16 +158,11 @@ ScrollView {
min: 0.1 min: 0.1
icon: "settings/xzoom.svg" icon: "settings/xzoom.svg"
width: settings.settingWidth width: settings.settingWidth
value: settings.xzoom.toFixed(2)
onChanged: function(newValue) { onChanged: function(newValue) {
Modules.Settings.set("xzoom", newValue, true) settings.xzoom = newValue
settings.changed() settings.changed()
} }
function update(newValue) {
value = Modules.Settings.xzoom.toFixed(2)
maxX.update()
}
} }
Setting.TextSetting { Setting.TextSetting {
@ -176,16 +173,11 @@ ScrollView {
label: qsTr("Y Zoom") label: qsTr("Y Zoom")
icon: "settings/yzoom.svg" icon: "settings/yzoom.svg"
width: settings.settingWidth width: settings.settingWidth
value: settings.yzoom.toFixed(2)
onChanged: function(newValue) { onChanged: function(newValue) {
Modules.Settings.set("yzoom", newValue, true) settings.yzoom = newValue
settings.changed() settings.changed()
} }
function update(newValue) {
value = Modules.Settings.yzoom.toFixed(2)
minY.update()
}
} }
// Positioning the graph // Positioning the graph
@ -197,18 +189,14 @@ ScrollView {
label: qsTr("Min X") label: qsTr("Min X")
icon: "settings/xmin.svg" icon: "settings/xmin.svg"
width: settings.settingWidth width: settings.settingWidth
defValue: settings.xmin
onChanged: function(newValue) { onChanged: function(newValue) {
Modules.Settings.set("xmin", newValue, true) if(parseFloat(maxX.value) > newValue) {
settings.xmin = newValue
settings.changed() settings.changed()
} else {
alert.show("Minimum x value must be inferior to maximum.")
} }
function update(newValue) {
let newVal = Modules.Settings.xmin
if(newVal > 1e-5)
newVal = newVal.toDecimalPrecision(8)
value = newVal
maxX.update()
} }
} }
@ -220,16 +208,11 @@ ScrollView {
label: qsTr("Max Y") label: qsTr("Max Y")
icon: "settings/ymax.svg" icon: "settings/ymax.svg"
width: settings.settingWidth width: settings.settingWidth
defValue: settings.ymax
onChanged: function(newValue) { onChanged: function(newValue) {
Modules.Settings.set("ymax", newValue, true) settings.ymax = newValue
settings.changed() settings.changed()
} }
function update() {
value = Modules.Settings.ymax
minY.update()
}
} }
Setting.TextSetting { Setting.TextSetting {
@ -240,24 +223,15 @@ ScrollView {
label: qsTr("Max X") label: qsTr("Max X")
icon: "settings/xmax.svg" icon: "settings/xmax.svg"
width: settings.settingWidth width: settings.settingWidth
defValue: Modules.Canvas.px2x(canvas.width).toFixed(2)
onChanged: function(xvaluemax) { onChanged: function(xvaluemax) {
if(xvaluemax > Modules.Settings.xmin) { if(xvaluemax > settings.xmin) {
const newXZoom = Modules.Settings.xzoom * canvas.width/(Modules.Canvas.x2px(xvaluemax)) // Adjusting zoom to fit. = (end)/(px of current point) settings.xzoom = settings.xzoom * canvas.width/(Modules.Canvas.x2px(xvaluemax)) // Adjusting zoom to fit. = (end)/(px of current point)
Modules.Settings.set("xzoom", newXZoom, true)
zoomX.update()
settings.changed() settings.changed()
} else { } else {
alert.show("Maximum x value must be superior to minimum.") alert.show("Maximum x value must be superior to minimum.")
} }
} }
function update() {
let newVal = Modules.Canvas.px2x(canvas.width)
if(newVal > 1e-5)
newVal = newVal.toDecimalPrecision(8)
value = newVal
}
} }
Setting.TextSetting { Setting.TextSetting {
@ -268,21 +242,15 @@ ScrollView {
label: qsTr("Min Y") label: qsTr("Min Y")
icon: "settings/ymin.svg" icon: "settings/ymin.svg"
width: settings.settingWidth width: settings.settingWidth
defValue: Modules.Canvas.px2y(canvas.height).toFixed(2)
onChanged: function(yvaluemin) { onChanged: function(yvaluemin) {
if(yvaluemin < settings.ymax) { if(yvaluemin < settings.ymax) {
const newYZoom = Modules.Settings.yzoom * canvas.height/(Modules.Canvas.y2px(yvaluemin)) // Adjusting zoom to fit. = (end)/(px of current point) settings.yzoom = settings.yzoom * canvas.height/(Modules.Canvas.y2px(yvaluemin)) // Adjusting zoom to fit. = (end)/(px of current point)
Modules.Settings.set("yzoom", newYZoom, true)
zoomY.update()
settings.changed() settings.changed()
} else { } else {
alert.show("Minimum y value must be inferior to maximum.") alert.show("Minimum y value must be inferior to maximum.")
} }
} }
function update() {
value = Modules.Canvas.px2y(canvas.height).toDecimalPrecision(8)
}
} }
Setting.TextSetting { Setting.TextSetting {
@ -292,16 +260,12 @@ ScrollView {
label: qsTr("X Axis Step") label: qsTr("X Axis Step")
icon: "settings/xaxisstep.svg" icon: "settings/xaxisstep.svg"
width: settings.settingWidth width: settings.settingWidth
defValue: settings.xaxisstep
visible: !settings.logscalex
onChanged: function(newValue) { onChanged: function(newValue) {
Modules.Settings.set("xaxisstep", newValue, true) settings.xaxisstep = newValue
settings.changed() settings.changed()
} }
function update() {
value = Modules.Settings.xaxisstep
visible = !Modules.Settings.logscalex
}
} }
Setting.TextSetting { Setting.TextSetting {
@ -311,13 +275,11 @@ ScrollView {
label: qsTr("Y Axis Step") label: qsTr("Y Axis Step")
icon: "settings/yaxisstep.svg" icon: "settings/yaxisstep.svg"
width: settings.settingWidth width: settings.settingWidth
defValue: settings.yaxisstep
onChanged: function(newValue) { onChanged: function(newValue) {
Modules.Settings.set("yaxisstep", newValue, true) settings.yaxisstep = newValue
settings.changed() settings.changed()
} }
function update() { value = Modules.Settings.yaxisstep }
} }
Setting.TextSetting { Setting.TextSetting {
@ -328,13 +290,11 @@ ScrollView {
min: 1 min: 1
icon: "settings/linewidth.svg" icon: "settings/linewidth.svg"
width: settings.settingWidth width: settings.settingWidth
defValue: settings.linewidth
onChanged: function(newValue) { onChanged: function(newValue) {
Modules.Settings.set("linewidth", newValue, true) settings.linewidth = newValue
settings.changed() settings.changed()
} }
function update() { value = Modules.Settings.linewidth }
} }
Setting.TextSetting { Setting.TextSetting {
@ -345,13 +305,11 @@ ScrollView {
min: 1 min: 1
icon: "settings/textsize.svg" icon: "settings/textsize.svg"
width: settings.settingWidth width: settings.settingWidth
defValue: settings.textsize
onChanged: function(newValue) { onChanged: function(newValue) {
Modules.Settings.set("textsize", newValue, true) settings.textsize = newValue
settings.changed() settings.changed()
} }
function update() { value = Modules.Settings.textsize }
} }
Setting.ComboBoxSetting { Setting.ComboBoxSetting {
@ -360,31 +318,24 @@ ScrollView {
width: settings.settingWidth width: settings.settingWidth
label: qsTr('X Label') label: qsTr('X Label')
icon: "settings/xlabel.svg" icon: "settings/xlabel.svg"
editable: true
model: ListModel { model: ListModel {
ListElement { text: "" } ListElement { text: "" }
ListElement { text: "x" } ListElement { text: "x" }
ListElement { text: "ω (rad/s)" } ListElement { text: "ω (rad/s)" }
} }
currentIndex: find(settings.xlabel)
editable: true
onAccepted: function(){ onAccepted: function(){
editText = JS.Utils.parseName(editText, false) editText = JS.Utils.parseName(editText, false)
if(find(editText) === -1) model.append({text: editText}) if (find(editText) === -1) model.append({text: editText})
currentIndex = find(editText) settings.xlabel = editText
Modules.Settings.set("xlabel", editText, true)
settings.changed() settings.changed()
} }
onActivated: function(selectedId) { onActivated: function(selectedId) {
Modules.Settings.set("xlabel", model.get(selectedId).text, true) settings.xlabel = model.get(selectedId).text
settings.changed() settings.changed()
} }
Component.onCompleted: editText = settings.xlabel
function update() {
editText = Modules.Settings.xlabel
if(find(editText) === -1) model.append({text: editText})
currentIndex = find(editText)
}
} }
Setting.ComboBoxSetting { Setting.ComboBoxSetting {
@ -393,7 +344,6 @@ ScrollView {
width: settings.settingWidth width: settings.settingWidth
label: qsTr('Y Label') label: qsTr('Y Label')
icon: "settings/ylabel.svg" icon: "settings/ylabel.svg"
editable: true
model: ListModel { model: ListModel {
ListElement { text: "" } ListElement { text: "" }
ListElement { text: "y" } ListElement { text: "y" }
@ -402,52 +352,39 @@ ScrollView {
ListElement { text: "φ (deg)" } ListElement { text: "φ (deg)" }
ListElement { text: "φ (rad)" } ListElement { text: "φ (rad)" }
} }
currentIndex: find(settings.ylabel)
editable: true
onAccepted: function(){ onAccepted: function(){
editText = JS.Utils.parseName(editText, false) editText = JS.Utils.parseName(editText, false)
if(find(editText) === -1) model.append({text: editText}) if (find(editText) === -1) model.append({text: editText, yaxisstep: root.yaxisstep})
currentIndex = find(editText) settings.ylabel = editText
Modules.Settings.set("ylabel", editText, true)
settings.changed() settings.changed()
} }
onActivated: function(selectedId) { onActivated: function(selectedId) {
Modules.Settings.set("ylabel", model.get(selectedId).text, true) settings.ylabel = model.get(selectedId).text
settings.changed() settings.changed()
} }
Component.onCompleted: editText = settings.ylabel
function update() {
editText = Modules.Settings.ylabel
if(find(editText) === -1) model.append({text: editText})
currentIndex = find(editText)
}
} }
CheckBox { CheckBox {
id: logScaleX id: logScaleX
checked: settings.logscalex
text: qsTr('X Log scale') text: qsTr('X Log scale')
onClicked: { onClicked: {
Modules.Settings.set("logscalex", checked, true) settings.logscalex = checked
if(Modules.Settings.xmin <= 0) // Reset xmin to prevent crash.
Modules.Settings.set("xmin", .5)
settings.changed() settings.changed()
} }
function update() {
checked = Modules.Settings.logscalex
xAxisStep.update()
}
} }
CheckBox { CheckBox {
id: showXGrad id: showXGrad
checked: settings.showxgrad
text: qsTr('Show X graduation') text: qsTr('Show X graduation')
onClicked: { onClicked: {
Modules.Settings.set("showxgrad", checked, true) settings.showxgrad = checked
settings.changed() settings.changed()
} }
function update() { checked = Modules.Settings.showxgrad }
} }
CheckBox { CheckBox {
@ -455,10 +392,9 @@ ScrollView {
checked: settings.showygrad checked: settings.showygrad
text: qsTr('Show Y graduation') text: qsTr('Show Y graduation')
onClicked: { onClicked: {
Modules.Settings.set("showygrad", checked, true) settings.showygrad = checked
settings.changed() settings.changed()
} }
function update() { checked = Modules.Settings.showygrad }
} }
Button { Button {
@ -504,10 +440,10 @@ ScrollView {
Saves the current canvas in the opened file. If no file is currently opened, prompts to pick a save location. Saves the current canvas in the opened file. If no file is currently opened, prompts to pick a save location.
*/ */
function save() { function save() {
if(Modules.Settings.saveFilename == "") { if(settings.saveFilename == "") {
saveAs() saveAs()
} else { } else {
Modules.IO.saveDiagram(Modules.Settings.saveFilename) Modules.IO.saveDiagram(settings.saveFilename)
} }
} }
@ -528,30 +464,4 @@ ScrollView {
fdiag.exportMode = false fdiag.exportMode = false
fdiag.open() fdiag.open()
} }
/**
* Initializing the settings
*/
Component.onCompleted: function() {
const matchedElements = new Map([
["xzoom", zoomX],
["yzoom", zoomY],
["xmin", minX],
["ymax", maxY],
["xaxisstep", xAxisStep],
["yaxisstep", yAxisStep],
["xlabel", xAxisLabel],
["ylabel", yAxisLabel],
["linewidth", lineWidth],
["textsize", textSize],
["logscalex", logScaleX],
["showxgrad", showXGrad],
["showygrad", showYGrad]
])
Modules.Settings.on("changed", (evt) => {
if(matchedElements.has(evt.property))
matchedElements.get(evt.property).update()
})
Modules.Settings.initialize({ helper: Helper })
}
} }

View file

@ -17,6 +17,8 @@
*/ */
import QtQuick import QtQuick
import QtQuick.Controls
import eu.ad5001.LogarithmPlotter.Setting 1.0 as Setting
/*! /*!
\qmltype ViewPositionChangeOverlay \qmltype ViewPositionChangeOverlay
@ -79,7 +81,7 @@ Item {
property int prevY property int prevY
/*! /*!
\qmlproperty double ViewPositionChangeOverlay::baseZoomMultiplier \qmlproperty double ViewPositionChangeOverlay::baseZoomMultiplier
How much should the zoom be multiplied/scrolled by for one scroll step (120° on the mouse wheel). How much should the zoom be mutliplied/scrolled by for one scroll step (120° on the mouse wheel).
*/ */
property double baseZoomMultiplier: 0.1 property double baseZoomMultiplier: 0.1
@ -89,15 +91,15 @@ Item {
cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor
property int positionChangeTimer: 0 property int positionChangeTimer: 0
function updatePosition(deltaX, deltaY, isEnd) { function updatePosition(deltaX, deltaY) {
const unauthorized = [NaN, Infinity, -Infinity] const unauthorized = [NaN, Infinity, -Infinity]
const xmin = (Modules.Canvas.px2x(Modules.Canvas.x2px(Modules.Settings.xmin)-deltaX)) const xmin = (Modules.Canvas.px2x(Modules.Canvas.x2px(settingsInstance.xmin)-deltaX))
const ymax = Modules.Settings.ymax + deltaY/Modules.Settings.yzoom const ymax = settingsInstance.ymax + deltaY/canvas.yzoom
if(!unauthorized.includes(xmin)) if(!unauthorized.includes(xmin))
Modules.Settings.set("xmin", xmin, isEnd) settingsInstance.xmin = xmin
if(!unauthorized.includes(ymax)) if(!unauthorized.includes(ymax))
Modules.Settings.set("ymax", ymax.toDecimalPrecision(6), isEnd) settingsInstance.ymax = ymax.toFixed(4)
Modules.Canvas.requestPaint() settingsInstance.changed()
parent.positionChanged(deltaX, deltaY) parent.positionChanged(deltaX, deltaY)
} }
@ -111,9 +113,9 @@ Item {
onPositionChanged: function(mouse) { onPositionChanged: function(mouse) {
positionChangeTimer++ positionChangeTimer++
if(positionChangeTimer == 3) { if(positionChangeTimer == 3) {
let deltaX = mouse.x - parent.prevX let deltaX = mouse.x - prevX
let deltaY = mouse.y - parent.prevY let deltaY = mouse.y - prevY
updatePosition(deltaX, deltaY, false) updatePosition(deltaX, deltaY)
prevX = mouse.x prevX = mouse.x
prevY = mouse.y prevY = mouse.y
positionChangeTimer = 0 positionChangeTimer = 0
@ -121,35 +123,35 @@ Item {
} }
onReleased: function(mouse) { onReleased: function(mouse) {
let deltaX = mouse.x - parent.prevX let deltaX = mouse.x - prevX
let deltaY = mouse.y - parent.prevY let deltaY = mouse.y - prevY
updatePosition(deltaX, deltaY, true) updatePosition(deltaX, deltaY)
parent.endPositionChange(deltaX, deltaY) parent.endPositionChange(deltaX, deltaY)
} }
onWheel: function(wheel) { onWheel: function(wheel) {
// Scrolling // Scrolling
let scrollSteps = Math.round(wheel.angleDelta.y / 120) let scrollSteps = Math.round(wheel.angleDelta.y / 120)
let zoomMultiplier = Math.pow(1+parent.baseZoomMultiplier, Math.abs(scrollSteps)) let zoomMultiplier = Math.pow(1+baseZoomMultiplier, Math.abs(scrollSteps))
// Avoid floating-point rounding errors by removing the zoom *after* // Avoid floating-point rounding errors by removing the zoom *after*
let xZoomDelta = (Modules.Settings.xzoom*zoomMultiplier - Modules.Settings.xzoom) let xZoomDelta = (settingsInstance.xzoom*zoomMultiplier - settingsInstance.xzoom)
let yZoomDelta = (Modules.Settings.yzoom*zoomMultiplier - Modules.Settings.yzoom) let yZoomDelta = (settingsInstance.yzoom*zoomMultiplier - settingsInstance.yzoom)
if(scrollSteps < 0) { // Negative scroll if(scrollSteps < 0) { // Negative scroll
xZoomDelta *= -1 xZoomDelta *= -1
yZoomDelta *= -1 yZoomDelta *= -1
} }
let newXZoom = (Modules.Settings.xzoom+xZoomDelta).toDecimalPrecision(0) let newXZoom = (settingsInstance.xzoom+xZoomDelta).toFixed(0)
let newYZoom = (Modules.Settings.yzoom+yZoomDelta).toDecimalPrecision(0) let newYZoom = (settingsInstance.yzoom+yZoomDelta).toFixed(0)
// Check if we need to have more precision // Check if we need to have more precision
if(newXZoom < 10) if(newXZoom < 10)
newXZoom = (Modules.Settings.xzoom+xZoomDelta).toDecimalPrecision(4) newXZoom = (settingsInstance.xzoom+xZoomDelta).toFixed(4)
if(newYZoom < 10) if(newYZoom < 10)
newYZoom = (Modules.Settings.yzoom+yZoomDelta).toDecimalPrecision(4) newYZoom = (settingsInstance.yzoom+yZoomDelta).toFixed(4)
if(newXZoom > 0.5) if(newXZoom > 0.5)
Modules.Settings.set("xzoom", newXZoom) settingsInstance.xzoom = newXZoom
if(newYZoom > 0.5) if(newYZoom > 0.5)
Modules.Settings.set("yzoom", newYZoom) settingsInstance.yzoom = newYZoom
Modules.Canvas.requestPaint() settingsInstance.changed()
} }
} }
} }

View file

@ -1,7 +1,4 @@
module eu.ad5001.LogarithmPlotter module eu.ad5001.LogarithmPlotter
AppMenuBar 1.0 AppMenuBar.qml
LogGraphCanvas 1.0 LogGraphCanvas.qml
PickLocationOverlay 1.0 PickLocationOverlay.qml
Settings 1.0 Settings.qml Settings 1.0 Settings.qml
ViewPositionChangeOverlay 1.0 ViewPositionChangeOverlay.qml Alert 1.0 Alert.qml

View file

@ -19,16 +19,13 @@
from os import path, environ, makedirs from os import path, environ, makedirs
from platform import system from platform import system
from json import load, dumps from json import load, dumps
from shutil import which
from PySide6.QtCore import QLocale, QTranslator from PySide6.QtCore import QLocale, QTranslator
DEFAULT_SETTINGS = { DEFAULT_SETTINGS = {
"check_for_updates": True, "check_for_updates": True,
"reset_redo_stack": True, "reset_redo_stack": True,
"last_install_greet": "0", "last_install_greet": "0",
"enable_latex": which("latex") is not None and which("dvipng") is not None, "enable_latex": False,
"enable_latex_async": True,
"expression_editor": { "expression_editor": {
"autoclose": True, "autoclose": True,
"colorize": True, "colorize": True,

View file

@ -19,12 +19,10 @@
from PySide6.QtCore import QtMsgType, qInstallMessageHandler, QMessageLogContext from PySide6.QtCore import QtMsgType, qInstallMessageHandler, QMessageLogContext
from math import ceil, log10 from math import ceil, log10
from os import path from os import path
from re import compile
CURRENT_PATH = path.dirname(path.realpath(__file__)) CURRENT_PATH = path.dirname(path.realpath(__file__))
SOURCEMAP_PATH = path.realpath(f"{CURRENT_PATH}/../qml/eu/ad5001/LogarithmPlotter/js/index.mjs.map") SOURCEMAP_PATH = path.realpath(f"{CURRENT_PATH}/../qml/eu/ad5001/LogarithmPlotter/js/index.mjs.map")
SOURCEMAP_INDEX = None SOURCEMAP_INDEX = None
INDEX_REG = compile(r"build\/runtime-pyside6\/LogarithmPlotter\/qml\/eu\/ad5001\/LogarithmPlotter\/js\/index.mjs:(\d+)")
class LOG_COLORS: class LOG_COLORS:
@ -79,7 +77,6 @@ def create_log_terminal_message(mode: QtMsgType, context: QMessageLogContext, me
# Check MJS # Check MJS
if line is not None and source_file is not None and source_file.endswith("index.mjs"): if line is not None and source_file is not None and source_file.endswith("index.mjs"):
source_file, line = map_javascript_source(source_file, line) source_file, line = map_javascript_source(source_file, line)
# Parse message
prefix = f"{LOG_COLORS.INVERT}{mode[1]}[{mode[0].upper()}]{LOG_COLORS.RESET_INVERT}" prefix = f"{LOG_COLORS.INVERT}{mode[1]}[{mode[0].upper()}]{LOG_COLORS.RESET_INVERT}"
message = message + LOG_COLORS.RESET message = message + LOG_COLORS.RESET
context = f"{context.function} at {source_file}:{line}" context = f"{context.function} at {source_file}:{line}"

View file

@ -15,9 +15,10 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from PySide6.QtWidgets import QMessageBox, QApplication from PySide6.QtWidgets import QMessageBox, QApplication
from PySide6.QtCore import QRunnable, QThreadPool, QThread, QObject, Signal, Slot, QCoreApplication from PySide6.QtCore import QRunnable, QThreadPool, QThread, QObject, Signal, Slot, QCoreApplication
from PySide6.QtQml import QJSValue from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtGui import QImage from PySide6.QtGui import QImage
from PySide6 import __version__ as PySide6_version from PySide6 import __version__ as PySide6_version
@ -29,27 +30,30 @@ from urllib.error import HTTPError, URLError
from LogarithmPlotter import __VERSION__ from LogarithmPlotter import __VERSION__
from LogarithmPlotter.util import config from LogarithmPlotter.util import config
from LogarithmPlotter.util.promise import PyPromise
SHOW_GUI_MESSAGES = "--test-build" not in argv SHOW_GUI_MESSAGES = "--test-build" not in argv
CHANGELOG_VERSION = __VERSION__ CHANGELOG_VERSION = __VERSION__
CHANGELOG_CACHE_PATH = path.join(path.dirname(path.realpath(__file__)), "CHANGELOG.md")
class InvalidFileException(Exception): pass class InvalidFileException(Exception): pass
def show_message(msg: str) -> None: def show_message(msg: str) -> None:
""" """
Shows a GUI message if GUI messages are enabled Shows a GUI message if GUI messages are enabled
""" """
if SHOW_GUI_MESSAGES: if SHOW_GUI_MESSAGES:
QMessageBox.warning(None, "LogarithmPlotter", msg) QMessageBox.warning(None, "LogarithmPlotter", msg, QMessageBox.OK)
else: else:
raise InvalidFileException(msg) raise InvalidFileException(msg)
def fetch_changelog():
class ChangelogFetcher(QRunnable):
def __init__(self, helper):
QRunnable.__init__(self)
self.helper = helper
def run(self):
msg_text = "Unknown changelog error." msg_text = "Unknown changelog error."
try: try:
# Fetching version # Fetching version
@ -62,17 +66,12 @@ def fetch_changelog():
str(e.code)) str(e.code))
except URLError as e: except URLError as e:
msg_text = QCoreApplication.translate("changelog", "Could not fetch update: {}.").format(str(e.reason)) msg_text = QCoreApplication.translate("changelog", "Could not fetch update: {}.").format(str(e.reason))
return msg_text self.helper.changelogFetched.emit(msg_text)
def read_changelog():
f = open(CHANGELOG_CACHE_PATH, 'r', -1)
data = f.read().strip()
f.close()
return data
class Helper(QObject): class Helper(QObject):
changelogFetched = Signal(str)
def __init__(self, cwd: str, tmpfile: str): def __init__(self, cwd: str, tmpfile: str):
QObject.__init__(self) QObject.__init__(self)
self.cwd = cwd self.cwd = cwd
@ -136,13 +135,29 @@ class Helper(QObject):
def getVersion(self): def getVersion(self):
return __VERSION__ return __VERSION__
@Slot(str, result=QJSValue) @Slot(str, result=str)
def getSetting(self, namespace: str) -> QJSValue: def getSetting(self, namespace):
return QJSValue(config.getSetting(namespace)) return str(config.getSetting(namespace))
@Slot(str, QJSValue) @Slot(str, result=float)
def setSetting(self, namespace: str, value: QJSValue): def getSettingInt(self, namespace):
return config.setSetting(namespace, value.toPrimitive().toVariant()) return float(config.getSetting(namespace))
@Slot(str, result=bool)
def getSettingBool(self, namespace):
return bool(config.getSetting(namespace))
@Slot(str, str)
def setSetting(self, namespace, value):
return config.setSetting(namespace, str(value))
@Slot(str, bool)
def setSettingBool(self, namespace, value):
return config.setSetting(namespace, bool(value))
@Slot(str, float)
def setSettingInt(self, namespace, value):
return config.setSetting(namespace, float(value))
@Slot(result=str) @Slot(result=str)
def getDebugInfos(self): def getDebugInfos(self):
@ -152,14 +167,15 @@ class Helper(QObject):
msg = QCoreApplication.translate('main', "Built with PySide6 (Qt) v{} and python v{}") msg = QCoreApplication.translate('main', "Built with PySide6 (Qt) v{} and python v{}")
return msg.format(PySide6_version, sys_version.split("\n")[0]) return msg.format(PySide6_version, sys_version.split("\n")[0])
@Slot(result=PyPromise) @Slot()
def fetchChangelog(self): def fetchChangelog(self):
""" changelog_cache_path = path.join(path.dirname(path.realpath(__file__)), "CHANGELOG.md")
Fetches the changelog and returns a Promise. if path.exists(changelog_cache_path):
"""
if path.exists(CHANGELOG_CACHE_PATH):
# We have a cached version of the changelog, for env that don't have access to the internet. # We have a cached version of the changelog, for env that don't have access to the internet.
return PyPromise(read_changelog) f = open(changelog_cache_path);
self.changelogFetched.emit("".join(f.readlines()).strip())
f.close()
else: else:
# Fetch it from the internet. # Fetch it from the internet.
return PyPromise(fetch_changelog) runnable = ChangelogFetcher(self)
QThreadPool.globalInstance().start(runnable)

View file

@ -16,13 +16,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from re import Pattern from re import Pattern
from typing import Callable
from PySide6.QtCore import QMetaObject, QObject, QDateTime from PySide6.QtCore import QMetaObject, QObject, QDateTime
from PySide6.QtQml import QJSValue from PySide6.QtQml import QJSValue
class InvalidAttributeValueException(Exception): pass class InvalidAttributeValueException(Exception): pass
class NotAPrimitiveException(Exception): pass class NotAPrimitiveException(Exception): pass
class Function: pass
class URL: pass class URL: pass
class PyJSValue: class PyJSValue:
@ -75,11 +75,10 @@ class PyJSValue:
return value return value
def type(self) -> any: def type(self) -> any:
ret = None
matcher = [ matcher = [
(lambda: self.qjs_value.isArray(), list), (lambda: self.qjs_value.isArray(), list),
(lambda: self.qjs_value.isBool(), bool), (lambda: self.qjs_value.isBool(), bool),
(lambda: self.qjs_value.isCallable(), Callable), (lambda: self.qjs_value.isCallable(), Function),
(lambda: self.qjs_value.isDate(), QDateTime), (lambda: self.qjs_value.isDate(), QDateTime),
(lambda: self.qjs_value.isError(), Exception), (lambda: self.qjs_value.isError(), Exception),
(lambda: self.qjs_value.isNull(), None), (lambda: self.qjs_value.isNull(), None),
@ -94,9 +93,8 @@ class PyJSValue:
] ]
for (test, value) in matcher: for (test, value) in matcher:
if test(): if test():
ret = value return value
break return None
return ret
def primitive(self): def primitive(self):
""" """
@ -106,5 +104,3 @@ class PyJSValue:
if self.type() not in [bool, float, str, None]: if self.type() not in [bool, float, str, None]:
raise NotAPrimitiveException() raise NotAPrimitiveException()
return self.qjs_value.toPrimitive().toVariant() return self.qjs_value.toPrimitive().toVariant()

View file

@ -15,22 +15,18 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from time import sleep
from PySide6.QtCore import QObject, Slot, Property, QCoreApplication, Signal from PySide6.QtCore import QObject, Slot, Property, QCoreApplication
from PySide6.QtGui import QImage, QColor from PySide6.QtGui import QImage, QColor
from PySide6.QtWidgets import QMessageBox from PySide6.QtWidgets import QMessageBox
from os import path, remove, makedirs from os import path, remove
from string import Template from string import Template
from tempfile import TemporaryDirectory
from subprocess import Popen, TimeoutExpired, PIPE from subprocess import Popen, TimeoutExpired, PIPE
from hashlib import sha512
from shutil import which from shutil import which
from sys import argv from sys import argv
from LogarithmPlotter.util import config
from LogarithmPlotter.util.promise import PyPromise
""" """
Searches for a valid Latex and DVIPNG (http://savannah.nongnu.org/projects/dvipng/) Searches for a valid Latex and DVIPNG (http://savannah.nongnu.org/projects/dvipng/)
installation and collects the binary path in the DVIPNG_PATH variable. installation and collects the binary path in the DVIPNG_PATH variable.
@ -79,20 +75,14 @@ class Latex(QObject):
dvipng to be installed on the system. dvipng to be installed on the system.
""" """
def __init__(self, cache_path): def __init__(self, tempdir: TemporaryDirectory):
QObject.__init__(self) QObject.__init__(self)
self.tempdir = path.join(cache_path, "latex") self.tempdir = tempdir
self.render_pipeline_locks = {}
makedirs(self.tempdir, exist_ok=True)
@Property(bool) @Property(bool)
def latexSupported(self) -> bool: def latexSupported(self) -> bool:
return LATEX_PATH is not None and DVIPNG_PATH is not None return LATEX_PATH is not None and DVIPNG_PATH is not None
@Property(bool)
def supportsAsyncRender(self) -> bool:
return config.getSetting("enable_latex_async")
@Slot(result=bool) @Slot(result=bool)
def checkLatexInstallation(self) -> bool: def checkLatexInstallation(self) -> bool:
""" """
@ -113,77 +103,21 @@ class Latex(QObject):
valid_install = False valid_install = False
else: else:
try: try:
self.renderSync("", 14, QColor(0, 0, 0, 255)) self.render("", 14, QColor(0, 0, 0, 255))
except MissingPackageException: except MissingPackageException:
valid_install = False # Should have sent an error message if failed to render valid_install = False # Should have sent an error message if failed to render
return valid_install return valid_install
def lock(self, markup_hash, render_hash, promise):
"""
Locks the render pipeline for a given markup hash and render hash.
"""
# print("Locking", markup_hash, render_hash)
if markup_hash not in self.render_pipeline_locks:
self.render_pipeline_locks[markup_hash] = promise
self.render_pipeline_locks[render_hash] = promise
def release_lock(self, markup_hash, render_hash):
"""
Release locks on the markup and render hashes.
"""
# print("Releasing", markup_hash, render_hash)
if markup_hash in self.render_pipeline_locks:
del self.render_pipeline_locks[markup_hash]
del self.render_pipeline_locks[render_hash]
@Slot(str, float, QColor, result=PyPromise)
def renderAsync(self, latex_markup: str, font_size: float, color: QColor) -> PyPromise:
"""
Prepares and renders a latex string into a png file asynchronously.
"""
markup_hash, render_hash, export_path = self.create_export_path(latex_markup, font_size, color)
promise = None
if render_hash in self.render_pipeline_locks:
# A PyPromise for this specific render is already running.
# print("Already running render of", latex_markup)
promise = self.render_pipeline_locks[render_hash]
elif markup_hash in self.render_pipeline_locks:
# A PyPromise with the same markup, but not the same color or font size is already running.
# print("Chaining render of", latex_markup)
existing_promise = self.render_pipeline_locks[markup_hash]
promise = self._create_async_promise(latex_markup, font_size, color)
existing_promise.then(promise.start)
else:
# No such PyPromise is running.
promise = self._create_async_promise(latex_markup, font_size, color)
promise.start()
return promise
def _create_async_promise(self, latex_markup: str, font_size: float, color: QColor) -> PyPromise:
"""
Createsa PyPromise to render a latex string into a PNG file.
Internal method. Use renderAsync that makes use of locks.
"""
markup_hash, render_hash, export_path = self.create_export_path(latex_markup, font_size, color)
promise = PyPromise(self.renderSync, [latex_markup, font_size, color], start_automatically=False)
self.lock(markup_hash, render_hash, promise)
# Make the lock release at the end.
def unlock(data, markup_hash=markup_hash, render_hash=render_hash):
self.release_lock(markup_hash, render_hash)
promise.then(unlock, unlock)
return promise
@Slot(str, float, QColor, result=str) @Slot(str, float, QColor, result=str)
def renderSync(self, latex_markup: str, font_size: float, color: QColor) -> str: def render(self, latex_markup: str, font_size: float, color: QColor) -> str:
""" """
Prepares and renders a latex string into a png file. Prepares and renders a latex string into a png file.
""" """
markup_hash, render_hash, export_path = self.create_export_path(latex_markup, font_size, color) markup_hash, export_path = self.create_export_path(latex_markup, font_size, color)
if self.latexSupported and not path.exists(export_path + ".png"): if self.latexSupported and not path.exists(export_path + ".png"):
print("Rendering", latex_markup) print("Rendering", latex_markup, export_path)
# Generating file # Generating file
latex_path = path.join(self.tempdir, str(markup_hash)) latex_path = path.join(self.tempdir.name, str(markup_hash))
# If the formula is just recolored or the font is just changed, no need to recreate the DVI. # If the formula is just recolored or the font is just changed, no need to recreate the DVI.
if not path.exists(latex_path + ".dvi"): if not path.exists(latex_path + ".dvi"):
self.create_latex_doc(latex_path, latex_markup) self.create_latex_doc(latex_path, latex_markup)
@ -203,7 +137,7 @@ class Latex(QObject):
""" """
Finds a prerendered image and returns its data if possible, and an empty string if not. Finds a prerendered image and returns its data if possible, and an empty string if not.
""" """
markup_hash, render_hash, export_path = self.create_export_path(latex_markup, font_size, color) markup_hash, export_path = self.create_export_path(latex_markup, font_size, color)
data = "" data = ""
if path.exists(export_path + ".png"): if path.exists(export_path + ".png"):
img = QImage(export_path) img = QImage(export_path)
@ -213,13 +147,10 @@ class Latex(QObject):
def create_export_path(self, latex_markup: str, font_size: float, color: QColor): def create_export_path(self, latex_markup: str, font_size: float, color: QColor):
""" """
Standardizes export path for renders. Standardizes export path for renders.
Markup hash is unique for the markup
Render hash is unique for the markup, the font size and the color.
""" """
markup_hash = "render" + str(sha512(latex_markup.encode()).hexdigest()) markup_hash = "render" + str(hash(latex_markup))
render_hash = f'{markup_hash}_{int(font_size)}_{color.rgb()}' export_path = path.join(self.tempdir.name, f'{markup_hash}_{int(font_size)}_{color.rgb()}')
export_path = path.join(self.tempdir, render_hash) return markup_hash, export_path
return markup_hash, render_hash, export_path
def create_latex_doc(self, export_path: str, latex_markup: str): def create_latex_doc(self, export_path: str, latex_markup: str):
""" """
@ -262,7 +193,7 @@ class Latex(QObject):
Runs a subprocess and handles exceptions and messages them to the user. Runs a subprocess and handles exceptions and messages them to the user.
""" """
cmd = " ".join(process) cmd = " ".join(process)
proc = Popen(process, stdout=PIPE, stderr=PIPE, cwd=self.tempdir) proc = Popen(process, stdout=PIPE, stderr=PIPE, cwd=self.tempdir.name)
try: try:
out, err = proc.communicate(timeout=2) # 2 seconds is already FAR too long. out, err = proc.communicate(timeout=2) # 2 seconds is already FAR too long.
if proc.returncode != 0: if proc.returncode != 0:

View file

@ -49,7 +49,3 @@ class MacOSFileOpenHandler(QObject):
else: else:
# standard event processing # standard event processing
return QObject.eventFilter(self, obj, event) return QObject.eventFilter(self, obj, event)

View file

@ -1,173 +0,0 @@
"""
* 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/>.
"""
from typing import Callable
from PySide6.QtCore import QRunnable, Signal, Property, QObject, Slot, QThreadPool
from PySide6.QtQml import QJSValue
from LogarithmPlotter.util.js import PyJSValue
def check_callable(function: Callable|QJSValue) -> Callable|None:
"""
Checks if the given function can be called (either a python callable
or a QJSValue function), and returns the object that can be called directly.
Returns None if not a function.
"""
if isinstance(function, QJSValue) and function.isCallable():
return PyJSValue(function)
elif callable(function):
return function
return None
class InvalidReturnValue(Exception): pass
class PyPromiseRunner(QRunnable):
"""
QRunnable for running Promises in different threads.
"""
def __init__(self, runner, promise, args):
QRunnable.__init__(self)
self.runner = runner
self.promise = promise
self.args = args
def run(self):
try:
data = self.runner(*self.args)
if type(data) in [int, str, float, bool]:
data = QJSValue(data)
elif data is None:
data = QJSValue.SpecialValue.UndefinedValue
elif isinstance(data, QJSValue):
data = data
elif isinstance(data, PyJSValue):
data = data.qjs_value
else:
raise InvalidReturnValue("Must return either a primitive, a JS Value, or None.")
self.promise.fulfilled.emit(data)
except Exception as e:
try:
self.promise.rejected.emit(repr(e))
except RuntimeError as e2:
# Happens when the PyPromise has already been garbage collected.
# In other words, nothing to report to nowhere.
pass
class PyPromise(QObject):
"""
Threaded A+/Promise implementation meant to interface between Python and Javascript easily.
Runs to_run in another thread, and calls fulfilled (populated by then) with its return value.
"""
fulfilled = Signal(QJSValue)
rejected = Signal(str)
def __init__(self, to_run: Callable|QJSValue, args=[], start_automatically=True):
QObject.__init__(self)
self._fulfills = []
self._rejects = []
self._state = "pending"
self._started = False
self.fulfilled.connect(self._fulfill)
self.rejected.connect(self._reject)
to_run = check_callable(to_run)
if to_run is None:
raise ValueError("New PyPromise created with invalid function")
self._runner = PyPromiseRunner(to_run, self, args)
if start_automatically:
self.start()
@Slot()
def start(self, *args, **kwargs):
"""
Starts the thread that will run the promise.
"""
if not self._started: # Avoid getting started twice.
QThreadPool.globalInstance().start(self._runner)
self._started = True
@Property(str)
def state(self):
return self._state
@Slot(QJSValue, result=QObject)
@Slot(QJSValue, QJSValue, result=QObject)
def then(self, on_fulfill: QJSValue | Callable, on_reject: QJSValue | Callable = None):
"""
Adds listeners for both fulfilment and catching errors of the Promise.
"""
on_fulfill = check_callable(on_fulfill)
on_reject = check_callable(on_reject)
self._fulfills.append(on_fulfill)
self._rejects.append(on_reject)
return self
def calls_upon_fulfillment(self, function: Callable | QJSValue) -> bool:
"""
Returns True if the given function will be callback upon the promise fulfillment.
False otherwise.
"""
return self._calls_in(function, self._fulfills)
def calls_upon_rejection(self, function: Callable | QJSValue) -> bool:
"""
Returns True if the given function will be callback upon the promise rejection.
False otherwise.
"""
return self._calls_in(function, self._rejects)
def _calls_in(self, function: Callable | QJSValue, within: list) -> bool:
"""
Returns True if the given function resides in the given within list, False otherwise.
Internal method of calls_upon_fulfill
"""
function = check_callable(function)
ret = False
if isinstance(function, PyJSValue):
found = next((f for f in within if f.qjs_value == function.qjs_value), None)
ret = found is not None
elif callable(function):
found = next((f for f in within if f == function), None)
ret = found is not None
return ret
@Slot(QJSValue)
@Slot(QObject)
def _fulfill(self, data):
self._state = "fulfilled"
no_return = [None, QJSValue.SpecialValue.UndefinedValue]
for i in range(len(self._fulfills)):
try:
result = self._fulfills[i](data)
result = result.qjs_value if isinstance(result, PyJSValue) else result
data = result if result not in no_return else data # Forward data.
except Exception as e:
self._reject(repr(e), start_at=i)
break
@Slot(QJSValue)
@Slot(str)
def _reject(self, error, start_at=0):
self._state = "rejected"
no_return = [None, QJSValue.SpecialValue.UndefinedValue]
for i in range(start_at, len(self._rejects)):
result = self._rejects[i](error)
result = result.qjs_value if isinstance(result, PyJSValue) else result
error = result if result not in no_return else error # Forward data.

View file

@ -24,73 +24,83 @@ files = [
[[package]] [[package]]
name = "coverage" name = "coverage"
version = "7.6.2" version = "7.6.1"
description = "Code coverage measurement for Python" description = "Code coverage measurement for Python"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.8"
files = [ files = [
{file = "coverage-7.6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c9df1950fb92d49970cce38100d7e7293c84ed3606eaa16ea0b6bc27175bb667"}, {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"},
{file = "coverage-7.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:24500f4b0e03aab60ce575c85365beab64b44d4db837021e08339f61d1fbfe52"}, {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"},
{file = "coverage-7.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a663b180b6669c400b4630a24cc776f23a992d38ce7ae72ede2a397ce6b0f170"}, {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"},
{file = "coverage-7.6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfde025e2793a22efe8c21f807d276bd1d6a4bcc5ba6f19dbdfc4e7a12160909"}, {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"},
{file = "coverage-7.6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:087932079c065d7b8ebadd3a0160656c55954144af6439886c8bcf78bbbcde7f"}, {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"},
{file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9c6b0c1cafd96213a0327cf680acb39f70e452caf8e9a25aeb05316db9c07f89"}, {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"},
{file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6e85830eed5b5263ffa0c62428e43cb844296f3b4461f09e4bdb0d44ec190bc2"}, {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"},
{file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62ab4231c01e156ece1b3a187c87173f31cbeee83a5e1f6dff17f288dca93345"}, {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"},
{file = "coverage-7.6.2-cp310-cp310-win32.whl", hash = "sha256:7b80fbb0da3aebde102a37ef0138aeedff45997e22f8962e5f16ae1742852676"}, {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"},
{file = "coverage-7.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:d20c3d1f31f14d6962a4e2f549c21d31e670b90f777ef4171be540fb7fb70f02"}, {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"},
{file = "coverage-7.6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bb21bac7783c1bf6f4bbe68b1e0ff0d20e7e7732cfb7995bc8d96e23aa90fc7b"}, {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"},
{file = "coverage-7.6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a7b2e437fbd8fae5bc7716b9c7ff97aecc95f0b4d56e4ca08b3c8d8adcaadb84"}, {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"},
{file = "coverage-7.6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:536f77f2bf5797983652d1d55f1a7272a29afcc89e3ae51caa99b2db4e89d658"}, {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"},
{file = "coverage-7.6.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f361296ca7054f0936b02525646b2731b32c8074ba6defab524b79b2b7eeac72"}, {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"},
{file = "coverage-7.6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7926d8d034e06b479797c199747dd774d5e86179f2ce44294423327a88d66ca7"}, {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"},
{file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0bbae11c138585c89fb4e991faefb174a80112e1a7557d507aaa07675c62e66b"}, {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"},
{file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fcad7d5d2bbfeae1026b395036a8aa5abf67e8038ae7e6a25c7d0f88b10a8e6a"}, {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"},
{file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f01e53575f27097d75d42de33b1b289c74b16891ce576d767ad8c48d17aeb5e0"}, {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"},
{file = "coverage-7.6.2-cp311-cp311-win32.whl", hash = "sha256:7781f4f70c9b0b39e1b129b10c7d43a4e0c91f90c60435e6da8288efc2b73438"}, {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"},
{file = "coverage-7.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:9bcd51eeca35a80e76dc5794a9dd7cb04b97f0e8af620d54711793bfc1fbba4b"}, {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"},
{file = "coverage-7.6.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ebc94fadbd4a3f4215993326a6a00e47d79889391f5659bf310f55fe5d9f581c"}, {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"},
{file = "coverage-7.6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9681516288e3dcf0aa7c26231178cc0be6cac9705cac06709f2353c5b406cfea"}, {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"},
{file = "coverage-7.6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d9c5d13927d77af4fbe453953810db766f75401e764727e73a6ee4f82527b3e"}, {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"},
{file = "coverage-7.6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92f9ca04b3e719d69b02dc4a69debb795af84cb7afd09c5eb5d54b4a1ae2191"}, {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"},
{file = "coverage-7.6.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ff2ef83d6d0b527b5c9dad73819b24a2f76fdddcfd6c4e7a4d7e73ecb0656b4"}, {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"},
{file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:47ccb6e99a3031ffbbd6e7cc041e70770b4fe405370c66a54dbf26a500ded80b"}, {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"},
{file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a867d26f06bcd047ef716175b2696b315cb7571ccb951006d61ca80bbc356e9e"}, {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"},
{file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cdfcf2e914e2ba653101157458afd0ad92a16731eeba9a611b5cbb3e7124e74b"}, {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"},
{file = "coverage-7.6.2-cp312-cp312-win32.whl", hash = "sha256:f9035695dadfb397bee9eeaf1dc7fbeda483bf7664a7397a629846800ce6e276"}, {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"},
{file = "coverage-7.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:5ed69befa9a9fc796fe015a7040c9398722d6b97df73a6b608e9e275fa0932b0"}, {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"},
{file = "coverage-7.6.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eea60c79d36a8f39475b1af887663bc3ae4f31289cd216f514ce18d5938df40"}, {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"},
{file = "coverage-7.6.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa68a6cdbe1bc6793a9dbfc38302c11599bbe1837392ae9b1d238b9ef3dafcf1"}, {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"},
{file = "coverage-7.6.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ec528ae69f0a139690fad6deac8a7d33629fa61ccce693fdd07ddf7e9931fba"}, {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"},
{file = "coverage-7.6.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed5ac02126f74d190fa2cc14a9eb2a5d9837d5863920fa472b02eb1595cdc925"}, {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"},
{file = "coverage-7.6.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21c0ea0d4db8a36b275cb6fb2437a3715697a4ba3cb7b918d3525cc75f726304"}, {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"},
{file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:35a51598f29b2a19e26d0908bd196f771a9b1c5d9a07bf20be0adf28f1ad4f77"}, {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"},
{file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c9192925acc33e146864b8cf037e2ed32a91fdf7644ae875f5d46cd2ef086a5f"}, {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"},
{file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf4eeecc9e10f5403ec06138978235af79c9a79af494eb6b1d60a50b49ed2869"}, {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"},
{file = "coverage-7.6.2-cp313-cp313-win32.whl", hash = "sha256:e4ee15b267d2dad3e8759ca441ad450c334f3733304c55210c2a44516e8d5530"}, {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"},
{file = "coverage-7.6.2-cp313-cp313-win_amd64.whl", hash = "sha256:c71965d1ced48bf97aab79fad56df82c566b4c498ffc09c2094605727c4b7e36"}, {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"},
{file = "coverage-7.6.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7571e8bbecc6ac066256f9de40365ff833553e2e0c0c004f4482facb131820ef"}, {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"},
{file = "coverage-7.6.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:078a87519057dacb5d77e333f740708ec2a8f768655f1db07f8dfd28d7a005f0"}, {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"},
{file = "coverage-7.6.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e5e92e3e84a8718d2de36cd8387459cba9a4508337b8c5f450ce42b87a9e760"}, {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"},
{file = "coverage-7.6.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ebabdf1c76593a09ee18c1a06cd3022919861365219ea3aca0247ededf6facd6"}, {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"},
{file = "coverage-7.6.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12179eb0575b8900912711688e45474f04ab3934aaa7b624dea7b3c511ecc90f"}, {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"},
{file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:39d3b964abfe1519b9d313ab28abf1d02faea26cd14b27f5283849bf59479ff5"}, {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"},
{file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:84c4315577f7cd511d6250ffd0f695c825efe729f4205c0340f7004eda51191f"}, {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"},
{file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ff797320dcbff57caa6b2301c3913784a010e13b1f6cf4ab3f563f3c5e7919db"}, {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"},
{file = "coverage-7.6.2-cp313-cp313t-win32.whl", hash = "sha256:2b636a301e53964550e2f3094484fa5a96e699db318d65398cfba438c5c92171"}, {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"},
{file = "coverage-7.6.2-cp313-cp313t-win_amd64.whl", hash = "sha256:d03a060ac1a08e10589c27d509bbdb35b65f2d7f3f8d81cf2fa199877c7bc58a"}, {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"},
{file = "coverage-7.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c37faddc8acd826cfc5e2392531aba734b229741d3daec7f4c777a8f0d4993e5"}, {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"},
{file = "coverage-7.6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab31fdd643f162c467cfe6a86e9cb5f1965b632e5e65c072d90854ff486d02cf"}, {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"},
{file = "coverage-7.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97df87e1a20deb75ac7d920c812e9326096aa00a9a4b6d07679b4f1f14b06c90"}, {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"},
{file = "coverage-7.6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:343056c5e0737487a5291f5691f4dfeb25b3e3c8699b4d36b92bb0e586219d14"}, {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"},
{file = "coverage-7.6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4ef1c56b47b6b9024b939d503ab487231df1f722065a48f4fc61832130b90e"}, {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"},
{file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fca4a92c8a7a73dee6946471bce6d1443d94155694b893b79e19ca2a540d86e"}, {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"},
{file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69f251804e052fc46d29d0e7348cdc5fcbfc4861dc4a1ebedef7e78d241ad39e"}, {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"},
{file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e8ea055b3ea046c0f66217af65bc193bbbeca1c8661dc5fd42698db5795d2627"}, {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"},
{file = "coverage-7.6.2-cp39-cp39-win32.whl", hash = "sha256:6c2ba1e0c24d8fae8f2cf0aeb2fc0a2a7f69b6d20bd8d3749fd6b36ecef5edf0"}, {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"},
{file = "coverage-7.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:2186369a654a15628e9c1c9921409a6b3eda833e4b91f3ca2a7d9f77abb4987c"}, {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"},
{file = "coverage-7.6.2-pp39.pp310-none-any.whl", hash = "sha256:667952739daafe9616db19fbedbdb87917eee253ac4f31d70c7587f7ab531b4e"}, {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"},
{file = "coverage-7.6.2.tar.gz", hash = "sha256:a5f81e68aa62bc0cfca04f7b19eaa8f9c826b53fc82ab9e2121976dc74f131f3"}, {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"},
{file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"},
{file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"},
{file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"},
{file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"},
{file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"},
{file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"},
{file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"},
{file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"},
{file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"},
{file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"},
] ]
[package.dependencies] [package.dependencies]
@ -251,36 +261,36 @@ setuptools = ">=42.0.0"
[[package]] [[package]]
name = "pyside6-addons" name = "pyside6-addons"
version = "6.8.0" version = "6.7.3"
description = "Python bindings for the Qt cross-platform application and UI framework (Addons)" description = "Python bindings for the Qt cross-platform application and UI framework (Addons)"
optional = false optional = false
python-versions = "<3.13,>=3.9" python-versions = "<3.13,>=3.9"
files = [ files = [
{file = "PySide6_Addons-6.8.0-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:aebab1e4fe63ebceccae4068768bf20959ab78f7fe01af832458837241334b5c"}, {file = "PySide6_Addons-6.7.3-cp39-abi3-macosx_11_0_universal2.whl", hash = "sha256:3174cb3a373c09c98740b452e8e8f4945d64cfa18ed8d43964111d570f0dc647"},
{file = "PySide6_Addons-6.8.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4b3be260f9cc415d1a12b77a703ced18b8854f56985f4708cab5618a9554bbd6"}, {file = "PySide6_Addons-6.7.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:bde1eb03dbffd089b50cd445847aaecaf4056cea84c49ea592d00f84f247251e"},
{file = "PySide6_Addons-6.8.0-cp39-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:7c153a685341683fac82d32926e2204747a83c13ef7b203db8ae9efe27f26a0f"}, {file = "PySide6_Addons-6.7.3-cp39-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:5a9e0df31345fe6caea677d916ea48b53ba86f95cc6499c57f89e392447ad6db"},
{file = "PySide6_Addons-6.8.0-cp39-abi3-win_amd64.whl", hash = "sha256:8f7f20fb3758995580f1fb8342df3479be51958eca36db2d6f6a3304f31471de"}, {file = "PySide6_Addons-6.7.3-cp39-abi3-win_amd64.whl", hash = "sha256:d8a19c2b2446407724c81c33ebf3217eaabd092f0f72da8130c17079e04a7813"},
] ]
[package.dependencies] [package.dependencies]
PySide6-Essentials = "6.8.0" PySide6-Essentials = "6.7.3"
shiboken6 = "6.8.0" shiboken6 = "6.7.3"
[[package]] [[package]]
name = "pyside6-essentials" name = "pyside6-essentials"
version = "6.8.0" version = "6.7.3"
description = "Python bindings for the Qt cross-platform application and UI framework (Essentials)" description = "Python bindings for the Qt cross-platform application and UI framework (Essentials)"
optional = false optional = false
python-versions = "<3.13,>=3.9" python-versions = "<3.13,>=3.9"
files = [ files = [
{file = "PySide6_Essentials-6.8.0-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:c2ad37de574ed911ac2dd392e95888ee7354c4bc475259dafc31978efb710a6a"}, {file = "PySide6_Essentials-6.7.3-cp39-abi3-macosx_11_0_universal2.whl", hash = "sha256:f9e08a4e9e7dc7b5ab72fde20abce8c97df7af1b802d9743f098f577dfe1f649"},
{file = "PySide6_Essentials-6.8.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:da99a94806416ec1e386426a474e7d1e514c1cdf8ad171c005376f4f633e7216"}, {file = "PySide6_Essentials-6.7.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cda6fd26aead48f32e57f044d18aa75dc39265b49d7957f515ce7ac3989e7029"},
{file = "PySide6_Essentials-6.8.0-cp39-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:ae0732228e93eb882c9a93fd510819fb64b7d09d8e500912b485a604537215d6"}, {file = "PySide6_Essentials-6.7.3-cp39-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:acdde06b74f26e7d26b4ae1461081b32a6cb17fcaa2a580050b5e0f0f12236c9"},
{file = "PySide6_Essentials-6.8.0-cp39-abi3-win_amd64.whl", hash = "sha256:2ef7138dc7efb9f1153c1dda7a7bd6ac02badad1aa1971cc140d0b9bf962c3dc"}, {file = "PySide6_Essentials-6.7.3-cp39-abi3-win_amd64.whl", hash = "sha256:f0950fcdcbcd4f2443336dc6a5fe692172adc225f876839583503ded0ab2f2a7"},
] ]
[package.dependencies] [package.dependencies]
shiboken6 = "6.8.0" shiboken6 = "6.7.3"
[[package]] [[package]]
name = "pytest" name = "pytest"
@ -374,15 +384,15 @@ type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11
[[package]] [[package]]
name = "shiboken6" name = "shiboken6"
version = "6.8.0" version = "6.7.3"
description = "Python/C++ bindings helper module" description = "Python/C++ bindings helper module"
optional = false optional = false
python-versions = "<3.13,>=3.9" python-versions = "<3.13,>=3.9"
files = [ files = [
{file = "shiboken6-6.8.0-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:0d3171c496e7474ad29d73686e46e741317a9b29ae9fa30c421fa0360bc10af0"}, {file = "shiboken6-6.7.3-cp39-abi3-macosx_11_0_universal2.whl", hash = "sha256:285fe3cf79be3135fe1ad1e2b9ff6db3a48698887425af6aa6ed7a05a9abc3d6"},
{file = "shiboken6-6.8.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ad88c0e73c9e4de3723c6e6b846e651729433ff9d9086bb2b4e6d49965477d97"}, {file = "shiboken6-6.7.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f0852e5781de78be5b13c140ec4c7fb9734e2aaf2986eb2d6a224363e03efccc"},
{file = "shiboken6-6.8.0-cp39-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:0f62ee7c34337e2c39fff0985694224f7503328c450245c399846b72cd71c410"}, {file = "shiboken6-6.7.3-cp39-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:f0dd635178e64a45be2f84c9f33dd79ac30328da87f834f21a0baf69ae210e6e"},
{file = "shiboken6-6.8.0-cp39-abi3-win_amd64.whl", hash = "sha256:cb98424a1f0c2d6ebf7f6be99660a121b9b22601a058e6b7efeadbc60bcd2182"}, {file = "shiboken6-6.7.3-cp39-abi3-win_amd64.whl", hash = "sha256:5f29325dfa86fde0274240f1f38e421303749d3174ce3ada178715b5f4719db9"},
] ]
[[package]] [[package]]
@ -408,13 +418,13 @@ files = [
[[package]] [[package]]
name = "tomli" name = "tomli"
version = "2.0.2" version = "2.0.1"
description = "A lil' TOML parser" description = "A lil' TOML parser"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.7"
files = [ files = [
{file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
] ]
[[package]] [[package]]
@ -439,4 +449,4 @@ type = ["pytest-mypy"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = ">=3.9,<3.13" python-versions = ">=3.9,<3.13"
content-hash = "fad810a5ba9b4cb5ab759c9b5641ccba2b735e12064e510c0bfe0f4766c576f1" content-hash = "5636605737f21954e102a0110972e6bd3df07f2d5929f41fe541c7347c3ecf08"

View file

@ -9,8 +9,8 @@ package-mode = false
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = ">=3.9,<3.13" python = ">=3.9,<3.13"
PySide6-Essentials = "^6.8" PySide6-Essentials = "^6.7"
PySide6-Addons = "^6.8" PySide6-Addons = "^6.7"
[tool.poetry.group.packaging.dependencies] [tool.poetry.group.packaging.dependencies]
pyinstaller = "^6.10.0" pyinstaller = "^6.10.0"

View file

@ -26,27 +26,27 @@ print(sys.argv)
current_dir = os.path.realpath(os.path.dirname(os.path.realpath(__file__))) current_dir = os.path.realpath(os.path.dirname(os.path.realpath(__file__)))
# Check where to install by default # Check where to install by default
# if "PREFIX" not in os.environ and sys.platform == 'linux': if "PREFIX" not in os.environ and sys.platform == 'linux':
# from getopt import getopt from getopt import getopt
# optlist, args = getopt(sys.argv, '', ['prefix=', 'root=']) optlist, args = getopt(sys.argv, '', ['prefix=', 'root='])
# for arg,value in optlist: for arg,value in optlist:
# if arg == "prefix" or arg == "root": if arg == "prefix" or arg == "root":
# os.environ["PREFIX"] = value os.environ["PREFIX"] = value
# if "PREFIX" not in os.environ and sys.platform == 'linux': if "PREFIX" not in os.environ and sys.platform == 'linux':
# if "XDG_DATA_HOME" in os.environ: if "XDG_DATA_HOME" in os.environ:
# os.environ["PREFIX"] = os.environ["XDG_DATA_HOME"] os.environ["PREFIX"] = os.environ["XDG_DATA_HOME"]
# else: else:
# try: try:
# # Checking if we have permission to write to root. # Checking if we have permission to write to root.
# from os import makedirs, rmdir from os import makedirs, rmdir
# makedirs("/usr/share/applications/test") makedirs("/usr/share/applications/test")
# rmdir("/usr/share/applications/test") rmdir("/usr/share/applications/test")
# os.environ["PREFIX"] = "/usr/share" os.environ["PREFIX"] = "/usr/share"
# except: except:
# if ".pybuild" in os.environ["HOME"]: # Launchpad building. if ".pybuild" in os.environ["HOME"]: # Launchpad building.
# os.environ["PREFIX"] = "share" os.environ["PREFIX"] = "share"
# else: else:
# os.environ["PREFIX"] = os.environ["HOME"] + "/.local/share" os.environ["PREFIX"] = os.environ["HOME"] + "/.local/share"
from LogarithmPlotter import __VERSION__ as pkg_version from LogarithmPlotter import __VERSION__ as pkg_version

View file

@ -1,22 +0,0 @@
"""
* 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/>.
"""
from .spy import Spy
from .that import that
from .interfaces.base import Assertion

View file

@ -1,39 +0,0 @@
"""
* 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/>.
"""
class Assertion(Exception):
def __init__(self, assertion: bool, message: str, invert: bool):
self.assertion = assertion
self.message = message
self.invert = invert
def _invert_message(self):
for verb in ('is', 'was', 'has', 'have'):
for negative in ("n't", ' not', ' never', ' no'):
self.message = self.message.replace(f"{verb}{negative}", verb.upper())
def __str__(self):
return self.message
def __bool__(self):
if not self.invert and not self.assertion:
raise self
if self.invert and self.assertion:
self._invert_message()
raise self
return True # Raises otherwise.

View file

@ -1,171 +0,0 @@
"""
* 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/>.
"""
from typing import Self, Callable, Any
from .assertion import Assertion
from .utils import repr_
class AssertionInterface:
"""
Most basic assertion interface.
You probably want to use BaseAssertionInterface
"""
def __init__(self, value, parent: Self = None):
self._value = value
self._parent = parent
if parent is None:
self.__not = False
@property
def _not(self) -> bool:
"""
Internal state of whether the expression was negated.
Use "not_" to set it.
:return:
"""
return self.__not if self._parent is None else self._parent._not
@_not.setter
def _not(self, value: bool):
if self._not is True:
raise RuntimeError("Cannot call is_not or was_not twice in the same statement.")
if self._parent is None:
self.__not = True
else:
self._parent._not = True
def instance_of(self, type_: type) -> Assertion:
"""
Checks if the current value is equal to the provided value
"""
value_type_name = type(self._value).__name__
if not isinstance(type_, type):
raise RuntimeError("Provided 'type' provided is not a class.")
return Assertion(
isinstance(self._value, type_),
f"The value ({value_type_name} {repr_(self._value)}) is not a {type_.__name__}.",
self._not
)
def __call__(self, condition: Callable[[Any], bool]) -> Assertion:
"""
Apply condition to value that returns whether or not the value is valid.
"""
return Assertion(
condition(self._value),
f"The value ({repr_(self._value)}) did not match given conditions.",
self._not
)
"""
NOT Properties.
"""
@property
def NOT(self) -> Self:
self._not = True
return self
@property
def not_(self) -> Self:
self._not = True
return self
@property
def never(self) -> Self:
self._not = True
return self
"""
Chain self properties to sound natural
"""
@property
def that(self) -> Self:
return self
@property
def is_(self) -> Self:
return self
@property
def does(self) -> Self:
return self
@property
def was(self) -> Self:
return self
@property
def been(self) -> Self:
return self
@property
def have(self) -> Self:
return self
@property
def has(self) -> Self:
return self
@property
def a(self) -> Self:
return self
@property
def an(self) -> Self:
return self
class EqualAssertionInterface(AssertionInterface):
"""
Interface created for when its value should be checked for equality
"""
def __init__(self, value, parent: AssertionInterface = None):
super().__init__(value, parent)
def __call__(self, value) -> Assertion:
return Assertion(
value == self._value,
f"The value {repr_(self._value)} is different from {repr(value)}.",
self._not
)
@property
def to(self) -> Self:
return self
class BaseAssertionInterface(AssertionInterface):
@property
def equals(self) -> EqualAssertionInterface:
"""
Checks if the current value is equal to the provided value
"""
return EqualAssertionInterface(self._value, self)
@property
def equal(self) -> EqualAssertionInterface:
"""
Checks if the current value is equal to the provided value
"""
return self.equals

View file

@ -1,83 +0,0 @@
"""
* 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/>.
"""
from .assertion import Assertion
from .base import BaseAssertionInterface
from .int import NumberInterface
from .utils import repr_
class FixedIteratorInterface(BaseAssertionInterface):
@property
def length(self) -> NumberInterface:
return NumberInterface(len(self._value), self)
def elements(self, *elements) -> Assertion:
tests = [repr_(elem) for elem in elements if elem not in self._value]
return Assertion(
len(tests) == 0,
f"This value ({repr_(self._value)}) does not have elements {', '.join(tests)}.",
self._not
)
def element(self, element) -> Assertion:
return Assertion(
element in self._value,
f"This value ({repr_(self._value)}) does not have element {repr_(element)}.",
self._not
)
def contains(self, *elements) -> Assertion:
"""
Check if the element(s) are contained in the iterator.
"""
if len(elements) == 1:
return self.element(elements[0])
else:
return self.elements(*elements)
def contain(self, *elements):
"""
Check if the element(s) are contained in the iterator.
"""
return self.contains(*elements)
class BoolInterface(BaseAssertionInterface):
@property
def true(self):
return Assertion(
self._value == True,
f"The value ({repr_(self._value)}) is not True.",
self._not
)
@property
def false(self):
return Assertion(
self._value == False,
f"The value ({repr_(self._value)}) is not False.",
self._not
)
class StringInterface(FixedIteratorInterface):
pass
class ListInterface(FixedIteratorInterface):
pass

View file

@ -1,320 +0,0 @@
"""
* 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/>.
"""
from math import log10, floor
from typing import Self
from .assertion import Assertion
from .base import AssertionInterface
from .utils import repr_
class NumberComparisonAssertionInterface(AssertionInterface):
def __init__(self, value, parent: AssertionInterface = None):
super().__init__(value, parent)
self._compare_stack = []
def _generate_compare_to(self) -> int:
"""
The number generated by the comparison stack.
E.g. can parse one.hundred.million.and.thirty.three.thousand.and.twelve.hundred.and.seven
as ['one', 'hundred', 'million', 'thirty', 'three', 'thousand', 'twelve', 'hundred', 'seven']
which results 100,034,207
"""
minus = len(self._compare_stack) > 0 and self._compare_stack[0] == -1
if len(self._compare_stack) < (2 if minus else 1):
raise RuntimeError("No number to compare the value to provided.")
if minus:
self._compare_stack.pop(0)
# Compute the number
add_stack = [self._compare_stack.pop(0)]
for element in self._compare_stack:
last_power = floor(log10(abs(add_stack[-1])))
current_power = floor(log10(abs(element)))
if last_power < current_power: # E.g. one hundred
add_stack[-1] *= element
elif last_power == 1 and current_power == 0: # E.g thirty four
add_stack[-1] += element
elif last_power > current_power: # E.g a hundred and five
add_stack.append(element)
else:
raise RuntimeError(f"Cannot chain two numbers with the same power ({add_stack[-1]} => {element}.")
total = sum(add_stack)
return -total if minus else total
def _compare(self) -> Assertion:
raise RuntimeError(f"No comparison method defined in {type(self).__name__}.")
def __bool__(self) -> bool:
return bool(self._compare())
def __call__(self, compare_to: int) -> Self:
if type(compare_to) not in (float, int):
raise RuntimeError(f"Cannot compare number ({self._value}) to non number ({repr_(compare_to)}).")
self._compare_stack.append(compare_to)
return self
"""
Chain self properties
"""
@property
def and_(self) -> Self:
return self
@property
def AND(self) -> Self:
return self
"""
Number shorthands
"""
@property
def once(self) -> Self:
return self(1)
@property
def twice(self) -> Self:
return self(2)
@property
def thrice(self) -> Self:
return self(3)
@property
def minus(self) -> Self:
return self(-1)
@property
def zero(self) -> Self:
return self(0)
@property
def one(self) -> Self:
return self(1)
@property
def two(self) -> Self:
return self(2)
@property
def three(self) -> Self:
return self(3)
@property
def four(self) -> Self:
return self(4)
@property
def five(self) -> Self:
return self(5)
@property
def six(self) -> Self:
return self(6)
@property
def seven(self) -> Self:
return self(7)
@property
def eight(self) -> Self:
return self(8)
@property
def nine(self) -> Self:
return self(9)
@property
def ten(self) -> Self:
return self(10)
@property
def eleven(self) -> Self:
return self(11)
@property
def twelve(self) -> Self:
return self(12)
@property
def thirteen(self) -> Self:
return self(13)
@property
def fourteen(self) -> Self:
return self(14)
@property
def fifteen(self) -> Self:
return self(15)
@property
def sixteen(self) -> Self:
return self(16)
@property
def seventeen(self) -> Self:
return self(17)
@property
def eighteen(self) -> Self:
return self(18)
@property
def nineteen(self) -> Self:
return self(19)
@property
def twenty(self) -> Self:
return self(20)
@property
def thirty(self) -> Self:
return self(30)
@property
def forty(self) -> Self:
return self(40)
@property
def fifty(self) -> Self:
return self(50)
@property
def sixty(self) -> Self:
return self(60)
@property
def seventy(self) -> Self:
return self(70)
@property
def eighty(self) -> Self:
return self(80)
@property
def ninety(self) -> Self:
return self(90)
@property
def hundred(self) -> Self:
return self(100)
@property
def thousand(self) -> Self:
return self(1_000)
@property
def million(self) -> Self:
return self(1_000_000)
@property
def billion(self) -> Self:
return self(1_000_000_000)
class LessThanComparisonInterface(NumberComparisonAssertionInterface):
def _compare(self) -> Assertion:
compare = self._generate_compare_to()
return Assertion(
self._value < compare,
f"The value ({repr_(self._value)}) is not less than to {repr_(compare)}.",
self._not
)
class MoreThanComparisonInterface(NumberComparisonAssertionInterface):
def _compare(self) -> Assertion:
compare = self._generate_compare_to()
return Assertion(
self._value > compare,
f"The value ({repr_(self._value)}) is not more than to {repr_(compare)}.",
self._not
)
class AtLeastComparisonInterface(NumberComparisonAssertionInterface):
def _compare(self) -> Assertion:
compare = self._generate_compare_to()
return Assertion(
self._value >= compare,
f"The value ({repr_(self._value)}) is not at least to {repr_(compare)}.",
self._not
)
class AtMostComparisonInterface(NumberComparisonAssertionInterface):
def _compare(self) -> Assertion:
compare = self._generate_compare_to()
return Assertion(
self._value <= compare,
f"The value ({repr_(self._value)}) is not at least to {repr_(compare)}.",
self._not
)
class EqualComparisonInterface(NumberComparisonAssertionInterface):
def _compare(self) -> Assertion:
compare = self._generate_compare_to()
return Assertion(
self._value == compare,
f"The value ({repr_(self._value)}) is not equal to {repr_(compare)}.",
self._not
)
@property
def to(self) -> Self:
return self
class NumberInterface(AssertionInterface):
def __call__(self, value):
return EqualComparisonInterface(self._value, self)(value)
@property
def equals(self) -> EqualComparisonInterface:
return EqualComparisonInterface(self._value, self)
@property
def equal(self) -> EqualComparisonInterface:
return EqualComparisonInterface(self._value, self)
@property
def exactly(self) -> EqualComparisonInterface:
return EqualComparisonInterface(self._value, self)
@property
def of(self) -> EqualComparisonInterface:
return EqualComparisonInterface(self._value, self)
@property
def less_than(self) -> LessThanComparisonInterface:
return LessThanComparisonInterface(self._value, self)
@property
def more_than(self) -> MoreThanComparisonInterface:
return MoreThanComparisonInterface(self._value, self)
@property
def at_least(self) -> AtLeastComparisonInterface:
return AtLeastComparisonInterface(self._value, self)
@property
def at_most(self) -> AtMostComparisonInterface:
return AtMostComparisonInterface(self._value, self)

View file

@ -1,218 +0,0 @@
"""
* 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/>.
"""
from typing import Callable, Self
from .base import Assertion, repr_, AssertionInterface
from .int import NumberComparisonAssertionInterface
PRINT_PREFIX = (" " * 3)
class SpyAssertion(Assertion):
def __init__(self, assertion: bool, message: str, calls: list, invert: bool):
super().__init__(assertion, message + "\n", invert)
if len(calls) > 0:
self.message += self.render_calls(calls)
else:
self.message += f"{PRINT_PREFIX}0 registered calls."
def render_calls(self, calls):
lines = [f"{PRINT_PREFIX}{len(calls)} registered call(s):"]
for call in calls:
repr_args = [repr_(arg) for arg in call[0]]
repr_kwargs = [f"{key}={repr_(arg)}" for key, arg in call[1].items()]
lines.append(f" - {', '.join([*repr_args, *repr_kwargs])}")
return ("\n" + PRINT_PREFIX).join(lines)
class Methods:
AT_LEAST_ONCE = "AT_LEAST_ONCE"
EXACTLY = "EXACTLY"
AT_LEAST = "AT_LEAST"
AT_MOST = "AT_MOST"
MORE_THAN = "MORE_THAN"
LESS_THAN = "LESS_THAN"
class CalledInterface(NumberComparisonAssertionInterface):
"""
Internal class generated by Spy.called.
"""
def __init__(self, calls: list[tuple[list, dict]], parent: AssertionInterface):
super().__init__(len(calls), parent)
self.__calls = calls
self.__method = Methods.AT_LEAST_ONCE
def __apply_method(self, calls):
required = None if self._compare_stack == [] else self._generate_compare_to()
calls_count = len(calls)
match self.__method:
case Methods.AT_LEAST_ONCE:
compare = len(calls) >= 1
error = f"Method was not called"
case Methods.EXACTLY:
compare = len(calls) == required
error = f"Method was not called {required} times ({required} != {calls_count})"
case Methods.AT_LEAST:
compare = len(calls) >= required
error = f"Method was not called at least {required} times ({required} >= {calls_count})"
case Methods.AT_MOST:
compare = len(calls) <= required
error = f"Method was not called at most {required} times ({required} <= {calls_count})"
case Methods.MORE_THAN:
compare = len(calls) > required
error = f"Method was not called more than {required} times ({required} > {calls_count})"
case Methods.LESS_THAN:
compare = len(calls) < required
error = f"Method was not called less than {required} times ({required} < {calls_count})"
case _:
raise RuntimeError(f"Unknown method {self.__method}.")
return compare, error
def __bool__(self) -> bool:
"""
Converts to boolean on assertion.
"""
compare, error = self.__apply_method(self.__calls)
return bool(SpyAssertion(compare, error + ".", self.__calls, self._not))
"""
Chaining methods
"""
def __call__(self, compare_to: int) -> Self:
super().__call__(compare_to)
if self.__method == Methods.AT_LEAST_ONCE:
self.__method = Methods.EXACTLY
return self
@property
def at_least(self) -> Self:
if self.__method == Methods.AT_LEAST_ONCE:
self.__method = Methods.AT_LEAST
else:
raise RuntimeError(f"Cannot redefine method from {self.__method} to {Methods.AT_MOST}")
return self
@property
def at_most(self) -> Self:
if self.__method == Methods.AT_LEAST_ONCE:
self.__method = Methods.AT_MOST
else:
raise RuntimeError(f"Cannot redefine method from {self.__method} to {Methods.AT_MOST}")
return self
@property
def more_than(self) -> Self:
if self.__method == Methods.AT_LEAST_ONCE:
self.__method = Methods.MORE_THAN
else:
raise RuntimeError(f"Cannot redefine method from {self.__method} to {Methods.MORE_THAN}")
return self
@property
def less_than(self) -> Self:
if self.__method == Methods.AT_LEAST_ONCE:
self.__method = Methods.LESS_THAN
else:
raise RuntimeError(f"Cannot redefine method from {self.__method} to {Methods.LESS_THAN}")
return self
@property
def time(self) -> Self:
return self
@property
def times(self) -> Self:
return self
"""
Class properties.
"""
def __match_calls_for_condition(self, condition: Callable[[list, dict], bool]) -> tuple[bool, str]:
calls = []
for call in self.__calls:
if condition(call[0], call[1]):
calls.append(call)
compare, error = self.__apply_method(calls)
return compare, error
def with_arguments(self, *args, **kwargs) -> SpyAssertion:
"""
Checks if the Spy has been called the given number of times
with at least the given arguments.
"""
def some_args_matched(a, kw):
args_matched = all((
arg in a
for arg in args
))
kwargs_matched = all((
key in kw and kw[key] == arg
for key, arg in kwargs.items()
))
return args_matched and kwargs_matched
compare, error = self.__match_calls_for_condition(some_args_matched)
repr_args = ', '.join([repr(arg) for arg in args])
repr_kwargs = ', '.join([f"{key}={repr(arg)}" for key, arg in kwargs.items()])
msg = f"{error} with arguments ({repr_args}) and keyword arguments ({repr_kwargs})."
return SpyAssertion(compare, msg, self.__calls, self._not)
def with_arguments_matching(self, test_condition: Callable[[list, dict], bool]) -> SpyAssertion:
"""
Checks if the Spy has been called the given number of times
with arguments matching the given conditions.
"""
compare, error = self.__match_calls_for_condition(test_condition)
msg = f"{error} with arguments matching given conditions."
return SpyAssertion(compare, msg, self.__calls, self._not)
def with_exact_arguments(self, *args, **kwargs) -> SpyAssertion:
"""
Checks if the Spy has been called the given number of times
with all the given arguments.
"""
compare, error = self.__match_calls_for_condition(lambda a, kw: a == args and kw == kwargs)
repr_args = ', '.join([repr(arg) for arg in args])
repr_kwargs = ', '.join([f"{key}={repr(arg)}" for key, arg in kwargs.items()])
msg = f"{error} with exact arguments ({repr_args}) and keyword arguments ({repr_kwargs})."
return SpyAssertion(compare, msg, self.__calls, self._not)
def with_no_argument(self) -> SpyAssertion:
"""
Checks if the Spy has been called the given number of times
with all the given arguments.
"""
compare, error = self.__match_calls_for_condition(lambda a, kw: len(a) == 0 and len(kw) == 0)
return SpyAssertion(compare, f"{error} with no arguments.", self.__calls, self._not)
class SpyAssertionInterface(AssertionInterface):
@property
def called(self) -> CalledInterface:
"""
Returns a boolean-able interface to check conditions for a given number of
time the spy was called.
"""
return CalledInterface(self._value.calls, self)

Some files were not shown because too many files have changed in this diff Show more