Compare commits

...

71 commits

Author SHA1 Message Date
a01b7a17ef
Merge branch 'master' of https://git.ad5001.eu/Ad5001/LogarithmPlotter
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-22 02:51:42 +02:00
67799e9908
Removing debug from python. 2024-10-22 02:50:56 +02:00
d53f50193a
Finishing latex and polyfill tests. 2024-10-22 02:50:24 +02:00
14e8cef6af
Fixing issues with LaTeX and rendering variable pi. 2024-10-22 02:50:00 +02:00
b989a685e9
Fixing issue with LaTeX sqrt. 2024-10-22 01:05:06 +02:00
e35f6cebec
Starting Latex tests. 2024-10-20 00:11:35 +02:00
6251835aa0
Translated using Weblate (German)
All checks were successful
continuous-integration/drone/push Build is passing
Currently translated at 100.0% (265 of 265 strings)

Translation: LogarithmPlotter/LogarithmPlotter
Translate-URL: https://hosted.weblate.org/projects/logarithmplotter/logarithmplotter/de/
2024-10-18 16:09:36 +02:00
14c092b9fa
Fixing tests
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-17 05:28:03 +02:00
811262b1fb
Changing lupdate and lrelease to use pyside6 versions, updating images so I no longer have to install node to build.
Some checks failed
continuous-integration/drone/push Build is failing
2024-10-17 05:09:33 +02:00
3c0d99d9c0
Exiting when not all tests are fulfilled.
Some checks failed
continuous-integration/drone/push Build is failing
2024-10-17 03:44:35 +02:00
2899ac6cde
Fixing unit testing importing unexpected and unused libraries.
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-17 03:41:41 +02:00
a182c703f4
Finishing testing promises.
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-17 03:38:36 +02:00
ef465b34e7
Finishing natural language plugin. 2024-10-17 02:08:24 +02:00
8fab9d8e52
Starting natural language plugin. 2024-10-16 22:18:53 +02:00
34caf20593
Fixing LaTeX tests, adding new sexy natural language method spy, started testing Promises. 2024-10-16 05:38:49 +02:00
a85a4721e3
Fixing double redraw when opening a file.
Some checks failed
continuous-integration/drone/push Build is failing
2024-10-15 20:39:03 +02:00
aeaaba759f
Starting latex render locking. 2024-10-15 19:21:40 +02:00
ccddb068a6
Fixing tests for Promises (new ones need to be written) 2024-10-15 18:06:24 +02:00
37ac400f23
Enabling latex async generation by default, default LaTeX setting now depends on state of installation.
Some checks failed
continuous-integration/drone/push Build is failing
2024-10-15 03:52:47 +02:00
5313428250
Improving stability of asynchronous LaTeX renderer. 2024-10-15 03:52:06 +02:00
cf73b35a9a
Adding experimental async LaTeX renderer (speeds up rendering ridiculously, but causes instability) 2024-10-15 03:01:27 +02:00
f734e40ad9
Starting PyPromise 2024-10-15 02:28:27 +02:00
b33e1329db
Removing typed config functions in Helper.
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-14 23:22:57 +02:00
2995b2271a
Fixing mind-bogingingly strange issue causing Qt crash when attempting to set WeakSet during mouse move events.
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-14 17:55:53 +02:00
a26dbc8a00
Getting rid of Qt5Compat, ridding dependency on PySide6-Addons
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-14 17:39:02 +02:00
89e78913de
Adding dependencies for Ubuntu 24.10.
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-14 17:28:43 +02:00
c03afdf4ee
Upgrading deb packaging
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-14 17:18:58 +02:00
3a81441d0b
Finished expr-eval testing
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-13 00:33:22 +02:00
edf4518494
Starting expr-eval's tests. 2024-10-12 20:37:16 +02:00
345458f453
Adding new test for Settings Module
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-12 06:19:30 +02:00
974baa6cc2
Adding base module tests
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-12 05:31:42 +02:00
4c1b705240
Mocking interfaces (+adding new method to canvas to make it more JS-like)
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-12 04:57:07 +02:00
885d1f5dc3
Adding test for Utils
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-12 03:22:49 +02:00
0abb22130f
Disable domain tests, started base tests. 2024-10-12 00:40:46 +02:00
42d5add810
Fixing tests and LaTeX
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-11 22:33:57 +02:00
e2d259f866
Fixing French localization.
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-11 22:18:35 +02:00
8a878b4cc1
Storing LaTeX renders in cache directory instead of temporary to store them for later use.
All checks were successful
continuous-integration/drone/push Build is passing
+ Makes reponening files instantaneous.
+ Improves performance of 'base state' objects (e.g. A = (1, 0) or f(x) = x).
2024-10-11 22:04:12 +02:00
07e58a3a55
Translated using Weblate (French)
All checks were successful
continuous-integration/drone/push Build is passing
Currently translated at 100.0% (265 of 265 strings)

Translation: LogarithmPlotter/LogarithmPlotter
Translate-URL: https://hosted.weblate.org/projects/logarithmplotter/logarithmplotter/fr/
2024-10-11 22:00:30 +02:00
c592b92212
Translated using Weblate (Spanish)
All checks were successful
continuous-integration/drone/push Build is passing
Currently translated at 100.0% (265 of 265 strings)

Translation: LogarithmPlotter/LogarithmPlotter
Translate-URL: https://hosted.weblate.org/projects/logarithmplotter/logarithmplotter/es/
2024-10-11 19:18:30 +02:00
7935d0134d
Fixing wrap on Greet Screen buttons in certain locales.
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-11 19:06:33 +02:00
5745587c72
Fixing git version detection. 2024-10-11 19:06:15 +02:00
84adc787e5
Fixing typo in Spanish translation 2024-10-11 19:05:53 +02:00
f3307b47d9
Fixing bug with saving files 2024-10-11 19:05:34 +02:00
9017f84c06
Updating metadata and python package lock. 2024-10-11 18:21:03 +02:00
00ab895b21
Adding x value as argument for derivative objects.
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-11 02:38:11 +02:00
82e6d2ffe3
Removing HistoryInterface (no longer needed)
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-11 02:35:13 +02:00
b91dbfb311
Finishing touches on putting History off QML.
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-11 02:14:42 +02:00
448d94fee3
Fixed many issues with new History module, including saved status.
+ Fixed (old) bug that label content wasn't being saved.
2024-10-11 02:03:27 +02:00
2dc9234b22
Decoupled History from QML 2024-10-11 01:14:52 +02:00
54363b25bc
Fixing issue with Replace All when replacement string includes source string.
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-10 23:56:42 +02:00
52f859349a
Converting actual settings to new Settings module. 2024-10-10 23:28:25 +02:00
d1ac70a946
Switching a lot of stuff to new Settings module
+ Fixing a bug that did not load the showygrad setting properly.
2024-10-10 19:15:46 +02:00
f4920aadb6
Merge branch 'master' of https://git.ad5001.eu/Ad5001/LogarithmPlotter
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-10 06:50:08 +02:00
af2950c3d2
Starting Settings modules + implemented basic events for ECMAScript <=> Qt compat. 2024-10-10 06:49:14 +02:00
gallegonovato
bd346240bd
Translated using Weblate (Spanish)
All checks were successful
continuous-integration/drone/push Build is passing
Currently translated at 100.0% (269 of 269 strings)

Translation: LogarithmPlotter/LogarithmPlotter
Translate-URL: https://hosted.weblate.org/projects/logarithmplotter/logarithmplotter/es/
2024-10-10 03:34:21 +00:00
9663c33563
Improving MJS lupdate hacky script to take private fields into account.
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-10 05:33:01 +02:00
934dd3ea1b
Adding private fields for Modules 2024-10-10 05:25:34 +02:00
b02ed87a29
Updating package-lock 2024-10-10 04:56:09 +02:00
40d86c8f82
Translated using Weblate (French)
All checks were successful
continuous-integration/drone/push Build is passing
Currently translated at 100.0% (269 of 269 strings)

Translation: LogarithmPlotter/LogarithmPlotter
Translate-URL: https://hosted.weblate.org/projects/logarithmplotter/logarithmplotter/fr/
2024-10-09 20:22:37 +02:00
6b3cce4252
Translated using Weblate (German)
Currently translated at 100.0% (269 of 269 strings)

Translation: LogarithmPlotter/LogarithmPlotter
Translate-URL: https://hosted.weblate.org/projects/logarithmplotter/logarithmplotter/de/
2024-10-09 20:22:36 +02:00
8c273f4220
Translated using Weblate (English)
Currently translated at 100.0% (269 of 269 strings)

Translation: LogarithmPlotter/LogarithmPlotter
Translate-URL: https://hosted.weblate.org/projects/logarithmplotter/logarithmplotter/en/
2024-10-09 20:22:36 +02:00
a60ac79d83
Updating translations to new folder structure.
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-09 20:15:27 +02:00
23c3b771c2
Merge branch 'master' of https://git.ad5001.eu/Ad5001/LogarithmPlotter
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-09 20:04:07 +02:00
041d4f424e
Reorganisation du projet pour séparer le module ECMAScript du reste de l'application.
Merge branch 'rollup-js'
2024-10-09 20:02:53 +02:00
ovari
7ef55e48e8
Translated using Weblate (Hungarian)
All checks were successful
continuous-integration/drone/push Build is passing
Currently translated at 100.0% (269 of 269 strings)

Translation: LogarithmPlotter/LogarithmPlotter
Translate-URL: https://hosted.weblate.org/projects/logarithmplotter/logarithmplotter/hu/
2024-10-01 17:23:20 +02:00
c74c2fb747
Merge branch 'master' of https://git.ad5001.eu/Ad5001/LogarithmPlotter
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-27 22:30:36 +02:00
c2eae30bd6
Updating qmldirs 2024-09-27 22:26:15 +02:00
80cea6d280
Translated using Weblate (Hungarian)
All checks were successful
continuous-integration/drone/push Build is passing
Currently translated at 95.1% (256 of 269 strings)

Translation: LogarithmPlotter/LogarithmPlotter
Translate-URL: https://hosted.weblate.org/projects/logarithmplotter/logarithmplotter/hu/
2024-09-27 18:16:19 +02:00
f8ce98d4ad
Removing logplotter.svg file (duplicate) + adding PySide6-Addons as dependency 2024-09-27 04:15:34 +02:00
fbb85083c1
Translated using Weblate (German)
All checks were successful
continuous-integration/drone/push Build is passing
Currently translated at 100.0% (269 of 269 strings)

Translation: LogarithmPlotter/LogarithmPlotter
Translate-URL: https://hosted.weblate.org/projects/logarithmplotter/logarithmplotter/de/
2024-09-25 22:15:42 +02:00
gallegonovato
17b6e40d60
Translated using Weblate (Spanish)
All checks were successful
continuous-integration/drone/push Build is passing
Currently translated at 100.0% (269 of 269 strings)

Translation: LogarithmPlotter/LogarithmPlotter
Translate-URL: https://hosted.weblate.org/projects/logarithmplotter/logarithmplotter/es/
2024-09-24 19:15:41 +02:00
116 changed files with 8229 additions and 4716 deletions

6
.gitignore vendored
View file

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

View file

@ -1,4 +1,4 @@
# ![icon](https://git.ad5001.eu/Ad5001/LogarithmPlotter/raw/branch/master/logplotter.svg) LogarithmPlotter
# ![icon](https://apps.ad5001.eu/icons/apps/svg/logarithmplotter.svg) 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/)
@ -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`).
You can simply run LogarithmPlotter using `python3 run.py`. It automatically compiles the language files (requires
`lrelease` to be installed and in path), and the JavaScript modules.
`pyside6-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
`python3 build/runtime-pyside6/LogarithmPlotter/logarithmplotter.py`.
@ -68,7 +68,13 @@ To run LogarithmPlotter's tests, follow these steps:
- Python
- Install python3 and [poetry](https://python-poetry.org/)
- Run `poetry install --with test`
- Create and activate virtual env (recommended)
- 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`
## 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
lrelease *.ts
pyside6-lrelease *.ts

View file

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

View file

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

View file

@ -0,0 +1 @@
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,50 +66,54 @@
<categories>
<category>Science</category>
<category>Education</category>
<category>Qt</category>
</categories>
<url type="homepage">https://apps.ad5001.eu/logarithmplotter/</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="translate">https://hosted.weblate.org/engage/logarithmplotter/</url>
<screenshots>
<screenshot type="default">
<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.5</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.5</image>
<image xml:lang="no">https://apps.ad5001.eu/img/no/logarithmplotter/gain.png?v=0.5</image>
<image>https://apps.ad5001.eu/img/en/logarithmplotter/gain.png?v=0.6</image>
<image xml:lang="de">https://apps.ad5001.eu/img/de/logarithmplotter/gain.png?v=0.6</image>
<image xml:lang="fr">https://apps.ad5001.eu/img/fr/logarithmplotter/gain.png?v=0.6</image>
<image xml:lang="hu">https://apps.ad5001.eu/img/hu/logarithmplotter/gain.png?v=0.6</image>
<image xml:lang="no">https://apps.ad5001.eu/img/no/logarithmplotter/gain.png?v=0.6</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 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="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="es">Vista principal de LogarithmPlotter mostrando un gráfico asintótico de una magnitud de Bode.</caption>
</screenshot>
<screenshot>
<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.5</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.5</image>
<image xml:lang="no">https://apps.ad5001.eu/img/no/logarithmplotter/phase.png?v=0.5</image>
<image>https://apps.ad5001.eu/img/en/logarithmplotter/phase.png?v=0.6</image>
<image xml:lang="de">https://apps.ad5001.eu/img/de/logarithmplotter/phase.png?v=0.6</image>
<image xml:lang="fr">https://apps.ad5001.eu/img/fr/logarithmplotter/phase.png?v=0.6</image>
<image xml:lang="hu">https://apps.ad5001.eu/img/hu/logarithmplotter/phase.png?v=0.6</image>
<image xml:lang="no">https://apps.ad5001.eu/img/no/logarithmplotter/phase.png?v=0.6</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 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="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="es">Vista principal de LogarithmPlotter mostrando un gráfico asintótico de una fase de Bode.</caption>
</screenshot>
<screenshot>
<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.5</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.5</image>
<image xml:lang="no">https://apps.ad5001.eu/img/no/logarithmplotter/welcome.png?v=0.5</image>
<image>https://apps.ad5001.eu/img/en/logarithmplotter/welcome.png?v=0.6</image>
<image xml:lang="de">https://apps.ad5001.eu/img/de/logarithmplotter/welcome.png?v=0.6</image>
<image xml:lang="fr">https://apps.ad5001.eu/img/fr/logarithmplotter/welcome.png?v=0.6</image>
<image xml:lang="hu">https://apps.ad5001.eu/img/hu/logarithmplotter/welcome.png?v=0.6</image>
<image xml:lang="no">https://apps.ad5001.eu/img/no/logarithmplotter/welcome.png?v=0.6</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 xml:lang="de">LogarithmPlotter's Willkommensseite.</caption>
<caption xml:lang="fr">Page d'accueil de LogarithmPlotter.</caption>
<caption xml:lang="hu">LogarithmPlotter üdvözlő oldala.</caption>
<caption xml:lang="no">LogarithmPlotters velkomstside.</caption>
<caption xml:lang="es">Página de bienvenida de LogarithmPlotter.</caption>
</screenshot>
</screenshots>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<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">
<comment>Logarithm Plot File</comment>
<comment>Logarithmic Plot File</comment>
<comment xml:lang="fr">Fichier Graphe Logarithmique</comment>
<icon name="application-x-logarithm-plot"/>
<glob-deleteall/>

View file

@ -12,28 +12,21 @@ steps:
- git submodule update --init --recursive
- name: Build
image: node:18-bookworm
image: ad5001/ubuntu-pyside-xvfb:linux-6-latest-latex-node
commands:
- cd common && npm install && cd ..
- apt update
- apt install -y qtchooser qttools5-dev-tools
# Start building
- bash scripts/build.sh
when:
event: [ push, tag ]
- name: Unit Tests
image: ad5001/ubuntu-pyside-xvfb:linux-6-latest-latex
image: ad5001/ubuntu-pyside-xvfb:linux-6-latest-latex-node
commands:
- apt update
- apt install -y npm
- cd common && npm install -D && cd ..
- xvfb-run bash scripts/run-tests.sh --no-rebuild
when:
event: [ push, tag ]
- name: File Tests
image: ad5001/ubuntu-pyside-xvfb:linux-6-latest-latex
image: ad5001/ubuntu-pyside-xvfb:linux-6-latest-latex-node
commands:
- 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

1539
common/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

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

116
common/src/events.mjs Normal file
View file

@ -0,0 +1,116 @@
/**
* 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.
* @param {Instruction[]} tokens
* @param {string} variable
* @param {number} expr
* @param {ExprEvalExpression} expr
* @return {Instruction[]}
*/
function substitute(tokens, variable, expr) {
@ -171,9 +171,6 @@ function evaluate(tokens, expr, values) {
nstack.push(n1 ? !!evaluate(n2, expr, values) : false)
} else if(item.value === "or") {
nstack.push(n1 ? true : !!evaluate(n2, expr, values))
} else if(item.value === "=") {
f = expr.binaryOps[item.value]
nstack.push(f(n1, evaluate(n2, expr, values), values))
} else {
f = expr.binaryOps[item.value]
nstack.push(f(resolveExpression(n1, values), resolveExpression(n2, values)))
@ -490,18 +487,6 @@ export class ExprEvalExpression {
return evaluate(this.tokens, this, values)
}
/**
* Returns a list of symbols (string of characters) in the expressions.
* Can be functions, constants, or variables.
* @returns {string[]}
*/
symbols(options) {
options = options || {}
const vars = []
getSymbols(this.tokens, vars, options)
return vars
}
toString() {
return expressionToString(this.tokens, false)
}

View file

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

View file

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

View file

@ -472,7 +472,7 @@ export class TokenStream {
this.current = this.newToken(TOP, "==")
this.pos++
} else {
this.current = this.newToken(TOP, c)
return false
}
} else if(c === "!") {
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.
* Copyright (C) 2021-2024 Ad5001
*
@ -64,10 +64,20 @@ function arrayFlatMap(callbackFn, thisArg) {
* @return {String}
*/
function stringReplaceAll(from, to) {
let str = this
while(str.includes(from))
str = str.replace(from, to)
return str
return this.split(from).join(to)
}
/**
* 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]
}
@ -98,8 +108,8 @@ const polyfills = {
[String.prototype, "replaceAll", stringReplaceAll]
],
2022: [
[Array.prototype, "at", notPolyfilled("Array.prototype.at")],
[String.prototype, "at", notPolyfilled("String.prototype.at")],
[Array.prototype, "at", arrayAt],
[String.prototype, "at", arrayAt],
[Object, "hasOwn", notPolyfilled("Object.hasOwn")]
],
2023: [

View file

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

View file

@ -17,6 +17,7 @@
*/
import { Interface } from "./interface.mjs"
import { BaseEventEmitter } from "../events.mjs"
// Define Modules interface before they are imported.
globalThis.Modules = globalThis.Modules || {}
@ -24,7 +25,13 @@ globalThis.Modules = globalThis.Modules || {}
/**
* Base class for global APIs in runtime.
*/
export class Module {
export class Module extends BaseEventEmitter {
/** @type {string} */
#name
/** @type {Object.<string, (Interface|string|number|boolean)>} */
#initializationParameters
/** @type {boolean} */
#initialized = false
/**
*
@ -32,11 +39,18 @@ export class Module {
* @param {Object.<string, (Interface|string|number|boolean)>} initializationParameters - List of parameters for the initialize function.
*/
constructor(name, initializationParameters = {}) {
super()
console.log(`Loading module ${name}...`)
this.__name = name
this.__initializationParameters = initializationParameters
this.initialized = false
this.#name = name
this.#initializationParameters = initializationParameters
}
get name() {
return this.#name;
}
get initialized() {
return this.#initialized
}
/**
@ -44,17 +58,17 @@ export class Module {
* @param {Object.<string, any>} options
*/
initialize(options) {
if(this.initialized)
throw new Error(`Cannot reinitialize module ${this.__name}.`)
console.log(`Initializing ${this.__name}...`)
for(const [name, value] of Object.entries(this.__initializationParameters)) {
if(this.#initialized)
throw new Error(`Cannot reinitialize module ${this.#name}.`)
console.log(`Initializing ${this.#name}...`)
for(const [name, value] of Object.entries(this.#initializationParameters)) {
if(!options.hasOwnProperty(name))
throw new Error(`Option '${name}' of initialize of module ${this.__name} does not exist.`)
throw new Error(`Option '${name}' of initialize of module ${this.#name} does not exist.`)
if(typeof value === "function" && value.prototype instanceof Interface)
Interface.check_implementation(value, options[name])
Interface.checkImplementation(value, options[name])
else if(typeof value !== typeof options[name])
throw new Error(`Option '${name}' of initialize of module ${this.__name} is not a '${value}' (${typeof options[name]}).`)
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 { Parser } from "../lib/expr-eval/parser.mjs"
const evalVariables = {
const EVAL_VARIABLES = {
// Variables not provided by expr-eval.js, needs to be provided manually
"pi": Math.PI,
"PI": Math.PI,
@ -35,15 +35,17 @@ const evalVariables = {
}
class ExprParserAPI extends Module {
#parser = new Parser()
constructor() {
super("ExprParser")
this.currentVars = {}
this._parser = new Parser()
this.#parser = new Parser()
this._parser.consts = Object.assign({}, this._parser.consts, evalVariables)
this.#parser.consts = Object.assign({}, this.#parser.consts, EVAL_VARIABLES)
this._parser.functions.integral = this.integral.bind(this)
this._parser.functions.derivative = this.derivative.bind(this)
this.#parser.functions.integral = this.integral.bind(this)
this.#parser.functions.derivative = this.derivative.bind(this)
}
/**
@ -68,7 +70,7 @@ class ExprParserAPI extends Module {
[f, variable] = args
if(typeof f !== "string" || typeof variable !== "string")
throw EvalError(qsTranslate("usage", "Usage:\n%1").arg(usage2))
f = this._parser.parse(f).toJSFunction(variable, this.currentVars)
f = this.#parser.parse(f).toJSFunction(variable, this.currentVars)
} else
throw EvalError(qsTranslate("usage", "Usage:\n%1\n%2").arg(usage1).arg(usage2))
return f
@ -79,14 +81,14 @@ class ExprParserAPI extends Module {
* @returns {ExprEvalExpression}
*/
parse(expression) {
return this._parser.parse(expression)
return this.#parser.parse(expression)
}
integral(a, b, ...args) {
integral(a = null, b = null, ...args) {
let usage1 = qsTranslate("usage", "integral(<from: number>, <to: number>, <f: ExecutableObject>)")
let usage2 = qsTranslate("usage", "integral(<from: number>, <to: number>, <f: string>, <variable: string>)")
let f = this.parseArgumentsForFunction(args, usage1, usage2)
if(a == null || b == null)
if(typeof a !== "number" || typeof b !== "number")
throw EvalError(qsTranslate("usage", "Usage:\n%1\n%2").arg(usage1).arg(usage2))
// https://en.wikipedia.org/wiki/Simpson%27s_rule
@ -99,10 +101,10 @@ class ExprParserAPI extends Module {
let usage2 = qsTranslate("usage", "derivative(<f: string>, <variable: string>, <x: number>)")
let x = args.pop()
let f = this.parseArgumentsForFunction(args, usage1, usage2)
if(x == null)
if(typeof x !== "number")
throw EvalError(qsTranslate("usage", "Usage:\n%1\n%2").arg(usage1).arg(usage2))
let derivative_precision = x / 10
let derivative_precision = 1e-8
return (f(x + derivative_precision / 2) - f(x - derivative_precision / 2)) / derivative_precision
}
}

View file

@ -17,60 +17,164 @@
*/
import { Module } from "./common.mjs"
import { HistoryInterface, NUMBER, STRING } from "./interface.mjs"
import { HelperInterface, 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 {
static emits = ["cleared", "loaded", "added", "undone", "redone"]
#helper
constructor() {
super("History", {
historyObj: HistoryInterface,
helper: HelperInterface,
themeTextColor: STRING,
imageDepth: NUMBER,
fontSize: NUMBER
})
// History QML object
this.history = null
/** @type {Action[]} */
this.undoStack = []
/** @type {Action[]} */
this.redoStack = []
this.themeTextColor = "#FF0000"
this.imageDepth = 2
this.fontSize = 28
}
initialize({ historyObj, themeTextColor, imageDepth, fontSize }) {
super.initialize({ historyObj, themeTextColor, imageDepth, fontSize })
this.history = historyObj
/**
* @param {HelperInterface} historyObj
* @param {string} themeTextColor
* @param {number} imageDepth
* @param {number} fontSize
*/
initialize({ helper, themeTextColor, imageDepth, fontSize }) {
super.initialize({ helper, themeTextColor, imageDepth, fontSize })
this.#helper = helper
this.themeTextColor = themeTextColor
this.imageDepth = imageDepth
this.fontSize = fontSize
}
/**
* Undoes the Action at the top of the undo stack and pushes it to the top of the redo stack.
*/
undo() {
if(!this.initialized) throw new Error("Attempting undo before initialize!")
this.history.undo()
if(this.undoStack.length > 0) {
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() {
if(!this.initialized) throw new Error("Attempting redo before initialize!")
this.history.redo()
if(this.redoStack.length > 0) {
const action = this.redoStack.pop()
action.redo()
this.undoStack.push(action)
this.emit(new RedoneEvent(action))
}
}
/**
* Clears both undo and redo stacks completely.
*/
clear() {
if(!this.initialized) throw new Error("Attempting clear before initialize!")
this.history.clear()
this.undoStack = []
this.redoStack = []
this.emit(new ClearedEvent())
}
/**
* Adds an instance of HistoryLib.Action to history.
* @param action
*/
addToHistory(action) {
if(!this.initialized) throw new Error("Attempting addToHistory before initialize!")
this.history.addToHistory(action)
if(action instanceof 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!")
this.history.unserialize(...data)
this.clear()
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() {
if(!this.initialized) throw new Error("Attempting serialize before initialize!")
return this.history.serialize()
let undoSt = [], redoSt = [];
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,6 +17,7 @@
*/
import Objects from "./objects.mjs"
import Settings from "./settings.mjs"
import ExprParser from "./expreval.mjs"
import Latex from "./latex.mjs"
import History from "./history.mjs"
@ -26,6 +27,7 @@ import Preferences from "./preferences.mjs"
export default {
Objects,
Settings,
ExprParser,
Latex,
History,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,186 @@
/**
* 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
om_0 = Objects.createNewRegisteredObject("Point", [Objects.getNewName("ω"), this.color, "name"])
om_0.labelPosition = this.phase.execute() >= 0 ? "above" : "below"
History.history.addToHistory(new CreateNewObject(om_0.name, "Point", om_0.export()))
History.addToHistory(new CreateNewObject(om_0.name, "Point", om_0.export()))
labelPosition = "below"
}
om_0.requiredBy.push(this)

View file

@ -53,11 +53,11 @@ export class BoolSetting extends Setting {
}
value() {
return Helper.getSettingBool(this.nameInConfig)
return Helper.getSetting(this.nameInConfig)
}
set(value) {
Helper.setSettingBool(this.nameInConfig, value)
Helper.setSetting(this.nameInConfig, value === true)
}
}
@ -69,11 +69,11 @@ export class NumberSetting extends Setting {
}
value() {
return Helper.getSettingInt(this.nameInConfig)
return Helper.getSetting(this.nameInConfig)
}
set(value) {
Helper.setSettingInt(this.nameInConfig, value)
Helper.setSetting(this.nameInConfig, +value)
}
}
@ -84,11 +84,11 @@ export class EnumIntSetting extends Setting {
}
value() {
return Helper.getSettingInt(this.nameInConfig)
return Helper.getSetting(this.nameInConfig)
}
set(value) {
Helper.setSettingInt(this.nameInConfig, value)
Helper.setSetting(this.nameInConfig, +value)
}
}
@ -131,6 +131,6 @@ export class StringSetting extends Setting {
}
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(
qsTranslate("Settings", "Y Zoom"),
"default_graph.xzoom",
"default_graph.yzoom",
"yzoom",
0.1
)
@ -37,7 +37,7 @@ const XMIN = new NumberSetting(
qsTranslate("Settings", "Min X"),
"default_graph.xmin",
"xmin",
() => Helper.getSettingBool("default_graph.logscalex") ? 1e-100 : -Infinity
() => Helper.getSetting("default_graph.logscalex") ? 1e-100 : -Infinity
)
const YMAX = new NumberSetting(

View file

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

View file

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

View file

@ -0,0 +1,118 @@
/**
* 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

@ -0,0 +1,58 @@
/**
* 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

@ -0,0 +1,231 @@
/**
* 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

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

View file

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

View file

@ -0,0 +1,59 @@
/**
* 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

@ -0,0 +1,23 @@
/**
* 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,7 +22,8 @@ const DEFAULT_SETTINGS = {
"check_for_updates": true,
"reset_redo_stack": true,
"last_install_greet": "0",
"enable_latex": false,
"enable_latex": true,
"enable_latex_async": true,
"expression_editor": {
"autoclose": true,
"colorize": true,
@ -53,7 +54,13 @@ export class MockHelper {
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(".")
let data = this.__settings
for(const name of namespace)
@ -64,7 +71,12 @@ export class MockHelper {
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 finalName = namespace.pop()
let data = this.__settings
@ -76,60 +88,6 @@ export class MockHelper {
data[finalName] = value
}
/**
* Gets a setting from the config
* @param {string} settingName - Setting (and its dot-separated namespace) to get (e.g. "default_graph.xmin")
* @returns {boolean} Value of the setting
*/
getSettingBool(settingName) {
return this.__getSetting(settingName) === true
}
/**
* Gets a setting from the config
* @param {string} settingName - Setting (and its dot-separated namespace) to get (e.g. "default_graph.xmin")
* @returns {number} Value of the setting
*/
getSettingInt(settingName) {
return +(this.__getSetting(settingName))
}
/**
* Gets a setting from the config
* @param {string} settingName - Setting (and its dot-separated namespace) to get (e.g. "default_graph.xmin")
* @returns {string} Value of the setting
*/
getSetting(settingName) {
return this.__getSetting(settingName).toString()
}
/**
* Sets a setting in the config
* @param {string} settingName - Setting (and its dot-separated namespace) to set (e.g. "default_graph.xmin")
* @param {boolean} value
*/
setSettingBool(settingName, value) {
return this.__setSetting(settingName, value === true)
}
/**
* Sets a setting in the config
* @param {string} settingName - Setting (and its dot-separated namespace) to set (e.g. "default_graph.xmin")
* @param {number} value
*/
setSettingInt(settingName, value) {
return this.__setSetting(settingName, +(value))
}
/**
* Sets a setting in the config
* @param {string} settingName - Setting (and its dot-separated namespace) to set (e.g. "default_graph.xmin")
* @param {string} value
*/
setSetting(settingName, value) {
return this.__setSetting(settingName, value.toString())
}
/**
* Sends data to be written
* @param {string} file

View file

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

61
common/test/mock/root.mjs Normal file
View file

@ -0,0 +1,61 @@
/**
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 Ad5001
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* 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

@ -0,0 +1,89 @@
/**
* 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

@ -0,0 +1,389 @@
/**
* 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

@ -0,0 +1,231 @@
/**
* 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

@ -0,0 +1,30 @@
/**
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 Ad5001
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// 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

@ -0,0 +1,101 @@
/**
* 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
cmd = ['git', 'log', '--format=%ci', '-n 1']
cwd = realpath(join(dirname(__file__), '..')) # Root AccountFree directory.
cwd = realpath(join(dirname(__file__), '..', '..', '..')) # Root LogarithmPlotter directory.
if exists(join(cwd, '.git')):
date_str = check_output(cmd, cwd=cwd).decode('utf-8').split(' ')[0]
try:

View file

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

View file

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

View file

@ -1,222 +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 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,6 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
pragma ComponentBehavior: Bound
import QtQuick.Controls
import QtQuick
import eu.ad5001.LogarithmPlotter.Setting 1.0 as Setting
@ -47,6 +49,18 @@ Item {
*/
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 {
id: filterInput
anchors.left: parent.left
@ -76,19 +90,22 @@ Item {
id: redoColumn
anchors.right: parent.right
anchors.top: parent.top
width: actionWidth
width: historyBrowser.actionWidth
Repeater {
model: history.redoCount
model: historyBrowser.redoCount
HistoryItem {
id: redoButton
width: actionWidth
width: historyBrowser.actionWidth
//height: actionHeight
isRedo: true
idx: index
darkTheme: historyBrowser.darkTheme
hidden: !(filterInput.value == "" || content.includes(filterInput.value))
onClicked: {
redoTimer.toRedoCount = Modules.History.redoStack.length-index
redoTimer.start()
}
}
}
}
@ -101,14 +118,14 @@ Item {
transform: Rotation { origin.x: 30; origin.y: 30; angle: 270}
height: 70
width: 20
visible: history.redoCount > 0
visible: historyBrowser.redoCount > 0
}
Rectangle {
id: nowRect
anchors.right: parent.right
anchors.top: redoColumn.bottom
width: actionWidth
width: historyBrowser.actionWidth
height: 40
color: sysPalette.highlight
Text {
@ -124,20 +141,24 @@ Item {
id: undoColumn
anchors.right: parent.right
anchors.top: nowRect.bottom
width: actionWidth
width: historyBrowser.actionWidth
Repeater {
model: history.undoCount
model: historyBrowser.undoCount
HistoryItem {
id: undoButton
width: actionWidth
width: historyBrowser.actionWidth
//height: actionHeight
isRedo: false
idx: index
darkTheme: historyBrowser.darkTheme
hidden: !(filterInput.value == "" || content.includes(filterInput.value))
onClicked: {
undoTimer.toUndoCount = +index+1
undoTimer.start()
}
}
}
}
@ -150,7 +171,39 @@ Item {
transform: Rotation { origin.x: 30; origin.y: 30; angle: 270}
height: 60
width: 20
visible: history.undoCount > 0
visible: historyBrowser.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;
}
}
}
@ -163,6 +216,18 @@ Item {
let hex = sysPalette.windowText.toString()
// We only check the first parameter, as on all normal OSes, text color is grayscale.
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,9 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick.Controls
import QtQuick
import Qt5Compat.GraphicalEffects
import QtQuick.Controls
import eu.ad5001.LogarithmPlotter.Setting 1.0 as Setting
@ -41,17 +40,17 @@ Button {
\qmlproperty bool HistoryItem::isRedo
true if the action is in the redo stack, false othewise.
*/
property bool isRedo
required property bool isRedo
/*!
\qmlproperty int HistoryItem::idx
\qmlproperty int HistoryItem::index
Index of the item within the HistoryBrowser list.
*/
property int idx
required property int index
/*!
\qmlproperty bool HistoryItem::darkTheme
true when the system is running with a dark theme, false otherwise.
*/
property bool darkTheme
required property bool darkTheme
/*!
\qmlproperty bool HistoryItem::hidden
true when the item is filtered out, false otherwise.
@ -61,7 +60,7 @@ Button {
\qmlproperty int HistoryItem::historyAction
Associated history action.
*/
readonly property var historyAction: isRedo ? history.redoStack[idx] : history.undoStack[history.undoCount-idx-1]
readonly property var historyAction: isRedo ? Modules.History.redoStack.at(index) : Modules.History.undoStack.at(-index-1)
/*!
\qmlproperty int HistoryItem::actionHeight
@ -82,12 +81,11 @@ Button {
height: hidden ? 8 : Math.max(actionHeight, label.height + 15)
LinearGradient {
Rectangle {
anchors.fill: parent
//opacity: hidden ? 0.6 : 1
start: Qt.point(0, 0)
end: Qt.point(parent.width, 0)
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.1; color: "transparent" }
GradientStop { position: 1.5; color: clr }
}
@ -147,13 +145,6 @@ Button {
ToolTip.visible: hovered
ToolTip.delay: 200
ToolTip.text: content
onClicked: {
if(isRedo)
history.redoMultipleDefered(history.redoCount-idx)
else
history.undoMultipleDefered(+idx+1)
}
}

View file

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

View file

@ -30,104 +30,15 @@ import Qt.labs.platform as Native
*/
Canvas {
id: canvas
anchors.top: separator.bottom
anchors.top: parent.top
anchors.left: parent.left
height: parent.height - 90
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
Dictionary of format {image: [callback.image data]} containing data for defered image loading.
Dictionary of format {image: callback} containing data for deferred image loading.
*/
property var imageLoaders: {}
/*!
\qmlproperty var LogGraphCanvas::ctx
Cache for the 2D context so that it may be used asynchronously.
*/
property var ctx
Component.onCompleted: {
imageLoaders = {}
@ -155,9 +66,21 @@ Canvas {
Object.keys(imageLoaders).forEach((key) => {
if(isImageLoaded(key)) {
// Calling callback
imageLoaders[key][0](canvas, ctx, imageLoaders[key][1])
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
height: 500
color: sysPalette.window
title: "LogarithmPlotter " + (settings.saveFilename != "" ? " - " + settings.saveFilename.split('/').pop() : "") + (history.saved ? "" : "*")
title: qsTr("untitled")
SystemPalette { id: sysPalette; colorGroup: SystemPalette.Active }
SystemPalette { id: sysPaletteIn; colorGroup: SystemPalette.Disabled }
@ -51,8 +51,6 @@ ApplicationWindow {
AppMenuBar {id: appMenu}
History { id: history }
Popup.GreetScreen {}
Popup.Preferences {id: preferences}
@ -145,20 +143,6 @@ ApplicationWindow {
width: sidebar.inPortrait ? parent.width : parent.width - 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
onPainted: if(!firstDrawDone) {
@ -199,7 +183,7 @@ ApplicationWindow {
}
onClosing: function(close) {
if(!history.saved) {
if(!Modules.IO.saved) {
close.accepted = false
appMenu.openSaveUnsavedChangesDialog()
}
@ -264,5 +248,22 @@ ApplicationWindow {
Component.onCompleted: {
Modules.IO.initialize({ root, settings, alert })
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
onChanged: function(newExpr) {
if(obj[propertyName].toString() != newExpr.toString()) {
history.addToHistory(new JS.HistoryLib.EditedProperty(
Modules.History.addToHistory(new JS.HistoryLib.EditedProperty(
obj.name, objType, propertyName,
obj[propertyName], newExpr
))
@ -123,7 +123,7 @@ Repeater {
// Ensuring old and new values are different to prevent useless adding to history.
if(obj[propertyName] != newValueParsed) {
history.addToHistory(new JS.HistoryLib.EditedProperty(
Modules.History.addToHistory(new JS.HistoryLib.EditedProperty(
obj.name, objType, propertyName,
obj[propertyName], newValueParsed
))
@ -168,7 +168,7 @@ Repeater {
return obj[propertyName]
}
onClicked: {
history.addToHistory(new JS.HistoryLib.EditedProperty(
Modules.History.addToHistory(new JS.HistoryLib.EditedProperty(
obj.name, objType, propertyName,
obj[propertyName], this.checked
))
@ -209,7 +209,9 @@ Repeater {
if(selectedObj == null) {
// Creating new object.
selectedObj = Modules.Objects.createNewRegisteredObject(propertyType.objType)
history.addToHistory(new JS.HistoryLib.CreateNewObject(selectedObj.name, propertyType.objType, selectedObj.export()))
Modules.History.addToHistory(
new JS.HistoryLib.CreateNewObject(selectedObj.name, propertyType.objType, selectedObj.export())
)
baseModel = Modules.Objects.getObjectsName(propertyType.objType).concat(
isRealObject ? [qsTr("+ Create new %1").arg(Modules.Objects.types[propertyType.objType].displayType())] :
[])
@ -219,14 +221,14 @@ Repeater {
//Modules.Objects.currentObjects[objType][objIndex].requiredBy = obj[propertyName].filter((obj) => obj.name != obj.name)
}
obj.requiredBy = obj.requiredBy.filter((obj) => obj.name != obj.name)
history.addToHistory(new JS.HistoryLib.EditedProperty(
Modules.History.addToHistory(new JS.HistoryLib.EditedProperty(
obj.name, objType, propertyName,
obj[propertyName], selectedObj
))
obj[propertyName] = selectedObj
} else if(baseModel[newIndex] != obj[propertyName]) {
// Ensuring new property is different to not add useless history entries.
history.addToHistory(new JS.HistoryLib.EditedProperty(
Modules.History.addToHistory(new JS.HistoryLib.EditedProperty(
obj.name, objType, propertyName,
obj[propertyName], baseModel[newIndex]
))
@ -256,7 +258,7 @@ Repeater {
onChanged: {
var exported = exportModel()
history.addToHistory(new JS.HistoryLib.EditedProperty(
Modules.History.addToHistory(new JS.HistoryLib.EditedProperty(
obj.name, objType, propertyName,
obj[propertyName], exported
))

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,8 @@
module eu.ad5001.LogarithmPlotter.Setting
AutocompletionCategory 1.0 AutocompletionCategory.qml
ComboBoxSetting 1.0 ComboBoxSetting.qml
ExpressionEditor 1.0 ExpressionEditor.qml
Icon 1.0 Icon.qml
ListSetting 1.0 ListSetting.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.
\sa Settings
*/
property double xzoom: Helper.getSettingInt('default_graph.xzoom')
property double xzoom: Helper.getSetting('default_graph.xzoom')
/*!
\qmlproperty double Settings::yzoom
Zoom on the y axis of the diagram, provided from settings.
\sa Settings
*/
property double yzoom: Helper.getSettingInt('default_graph.yzoom')
property double yzoom: Helper.getSetting('default_graph.yzoom')
/*!
\qmlproperty double Settings::xmin
Minimum x of the diagram, provided from settings.
\sa Settings
*/
property double xmin: Helper.getSettingInt('default_graph.xmin')
property double xmin: Helper.getSetting('default_graph.xmin')
/*!
\qmlproperty double Settings::ymax
Maximum y of the diagram, provided from settings.
\sa Settings
*/
property double ymax: Helper.getSettingInt('default_graph.ymax')
property double ymax: Helper.getSetting('default_graph.ymax')
/*!
\qmlproperty string Settings::xaxisstep
Step of the x axis graduation, provided from settings.
@ -93,39 +93,34 @@ ScrollView {
Width of lines that will be drawn into the canvas, provided from settings.
\sa Settings
*/
property double linewidth: Helper.getSettingInt('default_graph.linewidth')
property double linewidth: Helper.getSetting('default_graph.linewidth')
/*!
\qmlproperty double Settings::textsize
Font size of the text that will be drawn into the canvas, provided from settings.
\sa Settings
*/
property double textsize: Helper.getSettingInt('default_graph.textsize')
property double textsize: Helper.getSetting('default_graph.textsize')
/*!
\qmlproperty bool Settings::logscalex
true if the canvas should be in logarithmic mode, false otherwise.
Provided from settings.
\sa Settings
*/
property bool logscalex: Helper.getSettingBool('default_graph.logscalex')
property bool logscalex: Helper.getSetting('default_graph.logscalex')
/*!
\qmlproperty bool Settings::showxgrad
true if the x graduation should be shown, false otherwise.
Provided from settings.
\sa Settings
*/
property bool showxgrad: Helper.getSettingBool('default_graph.showxgrad')
property bool showxgrad: Helper.getSetting('default_graph.showxgrad')
/*!
\qmlproperty bool Settings::showygrad
true if the y graduation should be shown, false otherwise.
Provided from settings.
\sa Settings
*/
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: ""
property bool showygrad: Helper.getSetting('default_graph.showygrad')
Column {
spacing: 10
@ -136,15 +131,18 @@ ScrollView {
id: fdiag
onAccepted: {
var filePath = fdiag.currentFile.toString().substr(7)
settings.saveFilename = filePath
Modules.Settings.set("saveFilename", filePath)
if(exportMode) {
Modules.IO.saveDiagram(filePath)
} else {
Modules.IO.loadDiagram(filePath)
if(xAxisLabel.find(settings.xlabel) == -1) xAxisLabel.model.append({text: settings.xlabel})
xAxisLabel.editText = settings.xlabel
if(yAxisLabel.find(settings.ylabel) == -1) yAxisLabel.model.append({text: settings.ylabel})
yAxisLabel.editText = settings.ylabel
// Adding labels.
if(xAxisLabel.find(Modules.Settings.xlabel) === -1)
xAxisLabel.model.append({text: Modules.Settings.xlabel})
xAxisLabel.editText = Modules.Settings.xlabel
if(yAxisLabel.find(Modules.Settings.ylabel) === -1)
yAxisLabel.model.append({text: Modules.Settings.ylabel})
yAxisLabel.editText = Modules.Settings.ylabel
}
}
}
@ -158,11 +156,16 @@ ScrollView {
min: 0.1
icon: "settings/xzoom.svg"
width: settings.settingWidth
value: settings.xzoom.toFixed(2)
onChanged: function(newValue) {
settings.xzoom = newValue
Modules.Settings.set("xzoom", newValue, true)
settings.changed()
}
function update(newValue) {
value = Modules.Settings.xzoom.toFixed(2)
maxX.update()
}
}
Setting.TextSetting {
@ -173,11 +176,16 @@ ScrollView {
label: qsTr("Y Zoom")
icon: "settings/yzoom.svg"
width: settings.settingWidth
value: settings.yzoom.toFixed(2)
onChanged: function(newValue) {
settings.yzoom = newValue
Modules.Settings.set("yzoom", newValue, true)
settings.changed()
}
function update(newValue) {
value = Modules.Settings.yzoom.toFixed(2)
minY.update()
}
}
// Positioning the graph
@ -189,14 +197,18 @@ ScrollView {
label: qsTr("Min X")
icon: "settings/xmin.svg"
width: settings.settingWidth
defValue: settings.xmin
onChanged: function(newValue) {
if(parseFloat(maxX.value) > newValue) {
settings.xmin = newValue
Modules.Settings.set("xmin", newValue, true)
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()
}
}
@ -208,11 +220,16 @@ ScrollView {
label: qsTr("Max Y")
icon: "settings/ymax.svg"
width: settings.settingWidth
defValue: settings.ymax
onChanged: function(newValue) {
settings.ymax = newValue
Modules.Settings.set("ymax", newValue, true)
settings.changed()
}
function update() {
value = Modules.Settings.ymax
minY.update()
}
}
Setting.TextSetting {
@ -223,15 +240,24 @@ ScrollView {
label: qsTr("Max X")
icon: "settings/xmax.svg"
width: settings.settingWidth
defValue: Modules.Canvas.px2x(canvas.width).toFixed(2)
onChanged: function(xvaluemax) {
if(xvaluemax > settings.xmin) {
settings.xzoom = settings.xzoom * canvas.width/(Modules.Canvas.x2px(xvaluemax)) // Adjusting zoom to fit. = (end)/(px of current point)
if(xvaluemax > Modules.Settings.xmin) {
const newXZoom = Modules.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()
} else {
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 {
@ -242,15 +268,21 @@ ScrollView {
label: qsTr("Min Y")
icon: "settings/ymin.svg"
width: settings.settingWidth
defValue: Modules.Canvas.px2y(canvas.height).toFixed(2)
onChanged: function(yvaluemin) {
if(yvaluemin < settings.ymax) {
settings.yzoom = settings.yzoom * canvas.height/(Modules.Canvas.y2px(yvaluemin)) // Adjusting zoom to fit. = (end)/(px of current point)
const newYZoom = Modules.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()
} else {
alert.show("Minimum y value must be inferior to maximum.")
}
}
function update() {
value = Modules.Canvas.px2y(canvas.height).toDecimalPrecision(8)
}
}
Setting.TextSetting {
@ -260,12 +292,16 @@ ScrollView {
label: qsTr("X Axis Step")
icon: "settings/xaxisstep.svg"
width: settings.settingWidth
defValue: settings.xaxisstep
visible: !settings.logscalex
onChanged: function(newValue) {
settings.xaxisstep = newValue
Modules.Settings.set("xaxisstep", newValue, true)
settings.changed()
}
function update() {
value = Modules.Settings.xaxisstep
visible = !Modules.Settings.logscalex
}
}
Setting.TextSetting {
@ -275,11 +311,13 @@ ScrollView {
label: qsTr("Y Axis Step")
icon: "settings/yaxisstep.svg"
width: settings.settingWidth
defValue: settings.yaxisstep
onChanged: function(newValue) {
settings.yaxisstep = newValue
Modules.Settings.set("yaxisstep", newValue, true)
settings.changed()
}
function update() { value = Modules.Settings.yaxisstep }
}
Setting.TextSetting {
@ -290,11 +328,13 @@ ScrollView {
min: 1
icon: "settings/linewidth.svg"
width: settings.settingWidth
defValue: settings.linewidth
onChanged: function(newValue) {
settings.linewidth = newValue
Modules.Settings.set("linewidth", newValue, true)
settings.changed()
}
function update() { value = Modules.Settings.linewidth }
}
Setting.TextSetting {
@ -305,11 +345,13 @@ ScrollView {
min: 1
icon: "settings/textsize.svg"
width: settings.settingWidth
defValue: settings.textsize
onChanged: function(newValue) {
settings.textsize = newValue
Modules.Settings.set("textsize", newValue, true)
settings.changed()
}
function update() { value = Modules.Settings.textsize }
}
Setting.ComboBoxSetting {
@ -318,24 +360,31 @@ ScrollView {
width: settings.settingWidth
label: qsTr('X Label')
icon: "settings/xlabel.svg"
editable: true
model: ListModel {
ListElement { text: "" }
ListElement { text: "x" }
ListElement { text: "ω (rad/s)" }
}
currentIndex: find(settings.xlabel)
editable: true
onAccepted: function(){
editText = JS.Utils.parseName(editText, false)
if (find(editText) === -1) model.append({text: editText})
settings.xlabel = editText
if(find(editText) === -1) model.append({text: editText})
currentIndex = find(editText)
Modules.Settings.set("xlabel", editText, true)
settings.changed()
}
onActivated: function(selectedId) {
settings.xlabel = model.get(selectedId).text
Modules.Settings.set("xlabel", model.get(selectedId).text, true)
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 {
@ -344,6 +393,7 @@ ScrollView {
width: settings.settingWidth
label: qsTr('Y Label')
icon: "settings/ylabel.svg"
editable: true
model: ListModel {
ListElement { text: "" }
ListElement { text: "y" }
@ -352,39 +402,52 @@ ScrollView {
ListElement { text: "φ (deg)" }
ListElement { text: "φ (rad)" }
}
currentIndex: find(settings.ylabel)
editable: true
onAccepted: function(){
editText = JS.Utils.parseName(editText, false)
if (find(editText) === -1) model.append({text: editText, yaxisstep: root.yaxisstep})
settings.ylabel = editText
if(find(editText) === -1) model.append({text: editText})
currentIndex = find(editText)
Modules.Settings.set("ylabel", editText, true)
settings.changed()
}
onActivated: function(selectedId) {
settings.ylabel = model.get(selectedId).text
Modules.Settings.set("ylabel", model.get(selectedId).text, true)
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 {
id: logScaleX
checked: settings.logscalex
text: qsTr('X Log scale')
onClicked: {
settings.logscalex = checked
Modules.Settings.set("logscalex", checked, true)
if(Modules.Settings.xmin <= 0) // Reset xmin to prevent crash.
Modules.Settings.set("xmin", .5)
settings.changed()
}
function update() {
checked = Modules.Settings.logscalex
xAxisStep.update()
}
}
CheckBox {
id: showXGrad
checked: settings.showxgrad
text: qsTr('Show X graduation')
onClicked: {
settings.showxgrad = checked
Modules.Settings.set("showxgrad", checked, true)
settings.changed()
}
function update() { checked = Modules.Settings.showxgrad }
}
CheckBox {
@ -392,9 +455,10 @@ ScrollView {
checked: settings.showygrad
text: qsTr('Show Y graduation')
onClicked: {
settings.showygrad = checked
Modules.Settings.set("showygrad", checked, true)
settings.changed()
}
function update() { checked = Modules.Settings.showygrad }
}
Button {
@ -440,10 +504,10 @@ ScrollView {
Saves the current canvas in the opened file. If no file is currently opened, prompts to pick a save location.
*/
function save() {
if(settings.saveFilename == "") {
if(Modules.Settings.saveFilename == "") {
saveAs()
} else {
Modules.IO.saveDiagram(settings.saveFilename)
Modules.IO.saveDiagram(Modules.Settings.saveFilename)
}
}
@ -464,4 +528,30 @@ ScrollView {
fdiag.exportMode = false
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,8 +17,6 @@
*/
import QtQuick
import QtQuick.Controls
import eu.ad5001.LogarithmPlotter.Setting 1.0 as Setting
/*!
\qmltype ViewPositionChangeOverlay
@ -81,7 +79,7 @@ Item {
property int prevY
/*!
\qmlproperty double ViewPositionChangeOverlay::baseZoomMultiplier
How much should the zoom be mutliplied/scrolled by for one scroll step (120° on the mouse wheel).
How much should the zoom be multiplied/scrolled by for one scroll step (120° on the mouse wheel).
*/
property double baseZoomMultiplier: 0.1
@ -91,15 +89,15 @@ Item {
cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor
property int positionChangeTimer: 0
function updatePosition(deltaX, deltaY) {
function updatePosition(deltaX, deltaY, isEnd) {
const unauthorized = [NaN, Infinity, -Infinity]
const xmin = (Modules.Canvas.px2x(Modules.Canvas.x2px(settingsInstance.xmin)-deltaX))
const ymax = settingsInstance.ymax + deltaY/canvas.yzoom
const xmin = (Modules.Canvas.px2x(Modules.Canvas.x2px(Modules.Settings.xmin)-deltaX))
const ymax = Modules.Settings.ymax + deltaY/Modules.Settings.yzoom
if(!unauthorized.includes(xmin))
settingsInstance.xmin = xmin
Modules.Settings.set("xmin", xmin, isEnd)
if(!unauthorized.includes(ymax))
settingsInstance.ymax = ymax.toFixed(4)
settingsInstance.changed()
Modules.Settings.set("ymax", ymax.toDecimalPrecision(6), isEnd)
Modules.Canvas.requestPaint()
parent.positionChanged(deltaX, deltaY)
}
@ -113,9 +111,9 @@ Item {
onPositionChanged: function(mouse) {
positionChangeTimer++
if(positionChangeTimer == 3) {
let deltaX = mouse.x - prevX
let deltaY = mouse.y - prevY
updatePosition(deltaX, deltaY)
let deltaX = mouse.x - parent.prevX
let deltaY = mouse.y - parent.prevY
updatePosition(deltaX, deltaY, false)
prevX = mouse.x
prevY = mouse.y
positionChangeTimer = 0
@ -123,35 +121,35 @@ Item {
}
onReleased: function(mouse) {
let deltaX = mouse.x - prevX
let deltaY = mouse.y - prevY
updatePosition(deltaX, deltaY)
let deltaX = mouse.x - parent.prevX
let deltaY = mouse.y - parent.prevY
updatePosition(deltaX, deltaY, true)
parent.endPositionChange(deltaX, deltaY)
}
onWheel: function(wheel) {
// Scrolling
let scrollSteps = Math.round(wheel.angleDelta.y / 120)
let zoomMultiplier = Math.pow(1+baseZoomMultiplier, Math.abs(scrollSteps))
let zoomMultiplier = Math.pow(1+parent.baseZoomMultiplier, Math.abs(scrollSteps))
// Avoid floating-point rounding errors by removing the zoom *after*
let xZoomDelta = (settingsInstance.xzoom*zoomMultiplier - settingsInstance.xzoom)
let yZoomDelta = (settingsInstance.yzoom*zoomMultiplier - settingsInstance.yzoom)
let xZoomDelta = (Modules.Settings.xzoom*zoomMultiplier - Modules.Settings.xzoom)
let yZoomDelta = (Modules.Settings.yzoom*zoomMultiplier - Modules.Settings.yzoom)
if(scrollSteps < 0) { // Negative scroll
xZoomDelta *= -1
yZoomDelta *= -1
}
let newXZoom = (settingsInstance.xzoom+xZoomDelta).toFixed(0)
let newYZoom = (settingsInstance.yzoom+yZoomDelta).toFixed(0)
let newXZoom = (Modules.Settings.xzoom+xZoomDelta).toDecimalPrecision(0)
let newYZoom = (Modules.Settings.yzoom+yZoomDelta).toDecimalPrecision(0)
// Check if we need to have more precision
if(newXZoom < 10)
newXZoom = (settingsInstance.xzoom+xZoomDelta).toFixed(4)
newXZoom = (Modules.Settings.xzoom+xZoomDelta).toDecimalPrecision(4)
if(newYZoom < 10)
newYZoom = (settingsInstance.yzoom+yZoomDelta).toFixed(4)
newYZoom = (Modules.Settings.yzoom+yZoomDelta).toDecimalPrecision(4)
if(newXZoom > 0.5)
settingsInstance.xzoom = newXZoom
Modules.Settings.set("xzoom", newXZoom)
if(newYZoom > 0.5)
settingsInstance.yzoom = newYZoom
settingsInstance.changed()
Modules.Settings.set("yzoom", newYZoom)
Modules.Canvas.requestPaint()
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -15,18 +15,22 @@
* 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 time import sleep
from PySide6.QtCore import QObject, Slot, Property, QCoreApplication
from PySide6.QtCore import QObject, Slot, Property, QCoreApplication, Signal
from PySide6.QtGui import QImage, QColor
from PySide6.QtWidgets import QMessageBox
from os import path, remove
from os import path, remove, makedirs
from string import Template
from tempfile import TemporaryDirectory
from subprocess import Popen, TimeoutExpired, PIPE
from hashlib import sha512
from shutil import which
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/)
installation and collects the binary path in the DVIPNG_PATH variable.
@ -75,14 +79,20 @@ class Latex(QObject):
dvipng to be installed on the system.
"""
def __init__(self, tempdir: TemporaryDirectory):
def __init__(self, cache_path):
QObject.__init__(self)
self.tempdir = tempdir
self.tempdir = path.join(cache_path, "latex")
self.render_pipeline_locks = {}
makedirs(self.tempdir, exist_ok=True)
@Property(bool)
def latexSupported(self) -> bool:
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)
def checkLatexInstallation(self) -> bool:
"""
@ -103,21 +113,77 @@ class Latex(QObject):
valid_install = False
else:
try:
self.render("", 14, QColor(0, 0, 0, 255))
self.renderSync("", 14, QColor(0, 0, 0, 255))
except MissingPackageException:
valid_install = False # Should have sent an error message if failed to render
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)
def render(self, latex_markup: str, font_size: float, color: QColor) -> str:
def renderSync(self, latex_markup: str, font_size: float, color: QColor) -> str:
"""
Prepares and renders a latex string into a png file.
"""
markup_hash, export_path = self.create_export_path(latex_markup, font_size, color)
markup_hash, render_hash, export_path = self.create_export_path(latex_markup, font_size, color)
if self.latexSupported and not path.exists(export_path + ".png"):
print("Rendering", latex_markup, export_path)
print("Rendering", latex_markup)
# Generating file
latex_path = path.join(self.tempdir.name, str(markup_hash))
latex_path = path.join(self.tempdir, str(markup_hash))
# 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"):
self.create_latex_doc(latex_path, latex_markup)
@ -137,7 +203,7 @@ class Latex(QObject):
"""
Finds a prerendered image and returns its data if possible, and an empty string if not.
"""
markup_hash, export_path = self.create_export_path(latex_markup, font_size, color)
markup_hash, render_hash, export_path = self.create_export_path(latex_markup, font_size, color)
data = ""
if path.exists(export_path + ".png"):
img = QImage(export_path)
@ -147,10 +213,13 @@ class Latex(QObject):
def create_export_path(self, latex_markup: str, font_size: float, color: QColor):
"""
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(hash(latex_markup))
export_path = path.join(self.tempdir.name, f'{markup_hash}_{int(font_size)}_{color.rgb()}')
return markup_hash, export_path
markup_hash = "render" + str(sha512(latex_markup.encode()).hexdigest())
render_hash = f'{markup_hash}_{int(font_size)}_{color.rgb()}'
export_path = path.join(self.tempdir, render_hash)
return markup_hash, render_hash, export_path
def create_latex_doc(self, export_path: str, latex_markup: str):
"""
@ -193,7 +262,7 @@ class Latex(QObject):
Runs a subprocess and handles exceptions and messages them to the user.
"""
cmd = " ".join(process)
proc = Popen(process, stdout=PIPE, stderr=PIPE, cwd=self.tempdir.name)
proc = Popen(process, stdout=PIPE, stderr=PIPE, cwd=self.tempdir)
try:
out, err = proc.communicate(timeout=2) # 2 seconds is already FAR too long.
if proc.returncode != 0:

View file

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

View file

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

View file

@ -9,8 +9,8 @@ package-mode = false
[tool.poetry.dependencies]
python = ">=3.9,<3.13"
PySide6-Essentials = "^6.7"
PySide6-Addons = "^6.7"
PySide6-Essentials = "^6.8"
PySide6-Addons = "^6.8"
[tool.poetry.group.packaging.dependencies]
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__)))
# Check where to install by default
if "PREFIX" not in os.environ and sys.platform == 'linux':
from getopt import getopt
optlist, args = getopt(sys.argv, '', ['prefix=', 'root='])
for arg,value in optlist:
if arg == "prefix" or arg == "root":
os.environ["PREFIX"] = value
if "PREFIX" not in os.environ and sys.platform == 'linux':
if "XDG_DATA_HOME" in os.environ:
os.environ["PREFIX"] = os.environ["XDG_DATA_HOME"]
else:
try:
# Checking if we have permission to write to root.
from os import makedirs, rmdir
makedirs("/usr/share/applications/test")
rmdir("/usr/share/applications/test")
os.environ["PREFIX"] = "/usr/share"
except:
if ".pybuild" in os.environ["HOME"]: # Launchpad building.
os.environ["PREFIX"] = "share"
else:
os.environ["PREFIX"] = os.environ["HOME"] + "/.local/share"
# if "PREFIX" not in os.environ and sys.platform == 'linux':
# from getopt import getopt
# optlist, args = getopt(sys.argv, '', ['prefix=', 'root='])
# for arg,value in optlist:
# if arg == "prefix" or arg == "root":
# os.environ["PREFIX"] = value
# if "PREFIX" not in os.environ and sys.platform == 'linux':
# if "XDG_DATA_HOME" in os.environ:
# os.environ["PREFIX"] = os.environ["XDG_DATA_HOME"]
# else:
# try:
# # Checking if we have permission to write to root.
# from os import makedirs, rmdir
# makedirs("/usr/share/applications/test")
# rmdir("/usr/share/applications/test")
# os.environ["PREFIX"] = "/usr/share"
# except:
# if ".pybuild" in os.environ["HOME"]: # Launchpad building.
# os.environ["PREFIX"] = "share"
# else:
# os.environ["PREFIX"] = os.environ["HOME"] + "/.local/share"
from LogarithmPlotter import __VERSION__ as pkg_version

View file

View file

@ -0,0 +1,22 @@
"""
* 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

@ -0,0 +1,39 @@
"""
* 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

@ -0,0 +1,171 @@
"""
* 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

@ -0,0 +1,83 @@
"""
* 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

@ -0,0 +1,320 @@
"""
* 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

@ -0,0 +1,218 @@
"""
* 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