Compare commits
71 commits
Author | SHA1 | Date | |
---|---|---|---|
a01b7a17ef | |||
67799e9908 | |||
d53f50193a | |||
14e8cef6af | |||
b989a685e9 | |||
e35f6cebec | |||
6251835aa0 | |||
14c092b9fa | |||
811262b1fb | |||
3c0d99d9c0 | |||
2899ac6cde | |||
a182c703f4 | |||
ef465b34e7 | |||
8fab9d8e52 | |||
34caf20593 | |||
a85a4721e3 | |||
aeaaba759f | |||
ccddb068a6 | |||
37ac400f23 | |||
5313428250 | |||
cf73b35a9a | |||
f734e40ad9 | |||
b33e1329db | |||
2995b2271a | |||
a26dbc8a00 | |||
89e78913de | |||
c03afdf4ee | |||
3a81441d0b | |||
edf4518494 | |||
345458f453 | |||
974baa6cc2 | |||
4c1b705240 | |||
885d1f5dc3 | |||
0abb22130f | |||
42d5add810 | |||
e2d259f866 | |||
8a878b4cc1 | |||
07e58a3a55 | |||
c592b92212 | |||
7935d0134d | |||
5745587c72 | |||
84adc787e5 | |||
f3307b47d9 | |||
9017f84c06 | |||
00ab895b21 | |||
82e6d2ffe3 | |||
b91dbfb311 | |||
448d94fee3 | |||
2dc9234b22 | |||
54363b25bc | |||
52f859349a | |||
d1ac70a946 | |||
f4920aadb6 | |||
af2950c3d2 | |||
|
bd346240bd | ||
9663c33563 | |||
934dd3ea1b | |||
b02ed87a29 | |||
40d86c8f82 | |||
6b3cce4252 | |||
8c273f4220 | |||
a60ac79d83 | |||
23c3b771c2 | |||
041d4f424e | |||
|
7ef55e48e8 | ||
c74c2fb747 | |||
c2eae30bd6 | |||
80cea6d280 | |||
f8ce98d4ad | |||
fbb85083c1 | |||
|
17b6e40d60 |
116 changed files with 8229 additions and 4716 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -37,8 +37,10 @@ docs/html
|
||||||
*.lpf
|
*.lpf
|
||||||
*.lgg
|
*.lgg
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
common/coverage/
|
||||||
|
**/.coverage
|
||||||
|
|
||||||
# npm
|
# npm
|
||||||
common/node_modules
|
common/node_modules
|
||||||
common/coverage/
|
|
||||||
common/.coverage
|
|
||||||
runtime-pyside6/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/index.mjs*
|
runtime-pyside6/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/index.mjs*
|
||||||
|
|
12
README.md
12
README.md
|
@ -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)
|
[![Build Status](https://ci.ad5001.eu/api/badges/Ad5001/LogarithmPlotter/status.svg)](https://ci.ad5001.eu/Ad5001/LogarithmPlotter)
|
||||||
[![Translation status](https://hosted.weblate.org/widgets/logarithmplotter/-/logarithmplotter/svg-badge.svg)](https://hosted.weblate.org/engage/logarithmplotter/)
|
[![Translation status](https://hosted.weblate.org/widgets/logarithmplotter/-/logarithmplotter/svg-badge.svg)](https://hosted.weblate.org/engage/logarithmplotter/)
|
||||||
|
@ -24,7 +24,7 @@ First, you'll need to install all the required dependencies:
|
||||||
- [npm](https://npmjs.com) (or [yarn](https://yarnpkg.com/)), go to the `common` directory, and run `npm install` (or `yarn install`).
|
- [npm](https://npmjs.com) (or [yarn](https://yarnpkg.com/)), go to the `common` directory, and run `npm install` (or `yarn install`).
|
||||||
|
|
||||||
You can simply run LogarithmPlotter using `python3 run.py`. It automatically compiles the language files (requires
|
You can simply run LogarithmPlotter using `python3 run.py`. It automatically compiles the language files (requires
|
||||||
`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
|
If you do not wish do recompile the files again on every run, you can use the build script (`scripts/build.sh`) and run
|
||||||
`python3 build/runtime-pyside6/LogarithmPlotter/logarithmplotter.py`.
|
`python3 build/runtime-pyside6/LogarithmPlotter/logarithmplotter.py`.
|
||||||
|
@ -68,7 +68,13 @@ To run LogarithmPlotter's tests, follow these steps:
|
||||||
|
|
||||||
- Python
|
- Python
|
||||||
- Install python3 and [poetry](https://python-poetry.org/)
|
- Install python3 and [poetry](https://python-poetry.org/)
|
||||||
- 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`
|
- Run `scripts/run-tests.sh`
|
||||||
|
|
||||||
## Legal notice
|
## Legal notice
|
||||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,2 +1,2 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
lrelease *.ts
|
pyside6-lrelease *.ts
|
||||||
|
|
|
@ -21,7 +21,7 @@ replace() {
|
||||||
|
|
||||||
rm ../qml/eu/ad5001/LogarithmPlotter/js/index.mjs # Remove index which should not be scanned
|
rm ../qml/eu/ad5001/LogarithmPlotter/js/index.mjs # Remove index which should not be scanned
|
||||||
|
|
||||||
files=$(find .. -name *.mjs)
|
files=$(find ../../common/src -name '*.mjs')
|
||||||
for file in $files; do
|
for file in $files; do
|
||||||
echo "Moving '$file' to '${file%.*}.js'..."
|
echo "Moving '$file' to '${file%.*}.js'..."
|
||||||
mv "$file" "${file%.*}.js"
|
mv "$file" "${file%.*}.js"
|
||||||
|
@ -33,12 +33,14 @@ for file in $files; do
|
||||||
replace "${file%.*}.js" "^export" "/*export*/"
|
replace "${file%.*}.js" "^export" "/*export*/"
|
||||||
replace "${file%.*}.js" "async " "/*async */"
|
replace "${file%.*}.js" "async " "/*async */"
|
||||||
replace "${file%.*}.js" "await" "/*await */"
|
replace "${file%.*}.js" "await" "/*await */"
|
||||||
|
replace "${file%.*}.js" " #" "// #"
|
||||||
|
replace "${file%.*}.js" "this.#" "/*this.#*/"
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "----------------------------"
|
echo "----------------------------"
|
||||||
echo "| Updating translations... |"
|
echo "| Updating translations... |"
|
||||||
echo "----------------------------"
|
echo "----------------------------"
|
||||||
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
|
# Updating locations in files
|
||||||
for lp in *.ts; do
|
for lp in *.ts; do
|
||||||
echo "Replacing locations in $lp..."
|
echo "Replacing locations in $lp..."
|
||||||
|
@ -55,7 +57,9 @@ for file in $files; do
|
||||||
replace "$file" "/*async */" "async "
|
replace "$file" "/*async */" "async "
|
||||||
replace "$file" "^/*export*/" "export"
|
replace "$file" "^/*export*/" "export"
|
||||||
replace "$file" "^/*export default*/" "export default"
|
replace "$file" "^/*export default*/" "export default"
|
||||||
|
replace "$file" '.mjs"*/' '.mjs"'
|
||||||
replace "$file" "^/*import" "import"
|
replace "$file" "^/*import" "import"
|
||||||
replace "$file" "^/*export" "export"
|
replace "$file" "^/*export" "export"
|
||||||
replace "$file" '.mjs"*/$' '.mjs"'
|
replace "$file" "// #" " #"
|
||||||
|
replace "$file" "/*this.#*/" "this.#"
|
||||||
done
|
done
|
||||||
|
|
|
@ -3,7 +3,7 @@ Source: logarithmplotter
|
||||||
Version: 0.6.0
|
Version: 0.6.0
|
||||||
Architecture: all
|
Architecture: all
|
||||||
Maintainer: Ad5001 <mail@ad5001.eu>
|
Maintainer: Ad5001 <mail@ad5001.eu>
|
||||||
Depends: python3 (>= 3.9), python3-pip, python3-pyside6-essentials (>= 6.7.0), 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
|
Build-Depends: debhelper (>=11~), dh-python, dpkg-dev (>= 1.16.1~), python-setuptools
|
||||||
Section: science
|
Section: science
|
1
assets/native/linux/debian/depends.packaged
Normal file
1
assets/native/linux/debian/depends.packaged
Normal 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
|
|
@ -66,50 +66,54 @@
|
||||||
<categories>
|
<categories>
|
||||||
<category>Science</category>
|
<category>Science</category>
|
||||||
<category>Education</category>
|
<category>Education</category>
|
||||||
<category>Qt</category>
|
|
||||||
</categories>
|
</categories>
|
||||||
|
|
||||||
<url type="homepage">https://apps.ad5001.eu/logarithmplotter/</url>
|
<url type="homepage">https://apps.ad5001.eu/logarithmplotter/</url>
|
||||||
<url type="bugtracker">https://git.ad5001.eu/Ad5001/LogarithmPlotter/issues/</url>
|
<url type="bugtracker">https://git.ad5001.eu/Ad5001/LogarithmPlotter/issues/</url>
|
||||||
<url type="help">https://git.ad5001.eu/Ad5001/LogarithmPlotter/wiki/</url>
|
<url type="help">https://git.ad5001.eu/Ad5001/LogarithmPlotter/wiki/</url>
|
||||||
<url type="translate">https://hosted.weblate.org/engage/logarithmplotter/</url>
|
<url type="translate">https://hosted.weblate.org/engage/logarithmplotter/</url>
|
||||||
|
|
||||||
<screenshots>
|
<screenshots>
|
||||||
<screenshot type="default">
|
<screenshot type="default">
|
||||||
<image>https://apps.ad5001.eu/img/en/logarithmplotter/gain.png?v=0.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.5</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.5</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.5</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.5</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>Main view of LogarithmPlotter showing an asymptotic Bode magnitude plot.</caption>
|
||||||
<caption xml:lang="de">Die Hauptansicht des LogarithmPlotters zeigt eine asymptotische Bode-Magnitude-Darstellung.</caption>
|
<caption xml:lang="de">Die Hauptansicht des LogarithmPlotters zeigt eine asymptotische Bode-Magnitude-Darstellung.</caption>
|
||||||
<caption xml:lang="fr">Vue principale de LogarithmPlotter montrant un tracé asymptotique d'une magnitude de Bode.</caption>
|
<caption xml:lang="fr">Vue principale de LogarithmPlotter montrant un tracé asymptotique d'une magnitude de Bode.</caption>
|
||||||
<caption xml:lang="hu">A LogarithmPlotter fő nézete, amely egy aszimptotikus Bode-magnitúdó ábrát mutat.</caption>
|
<caption xml:lang="hu">A LogarithmPlotter fő nézete, amely egy aszimptotikus Bode-magnitúdó ábrát mutat.</caption>
|
||||||
<caption xml:lang="no">Hovedvisning av LogarithmPlotter som viser et asymptotisk Bode-størrelsesplott.</caption>
|
<caption xml:lang="no">Hovedvisning av LogarithmPlotter som viser et asymptotisk Bode-størrelsesplott.</caption>
|
||||||
|
<caption xml:lang="es">Vista principal de LogarithmPlotter mostrando un gráfico asintótico de una magnitud de Bode.</caption>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
<screenshot>
|
<screenshot>
|
||||||
<image>https://apps.ad5001.eu/img/en/logarithmplotter/phase.png?v=0.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.5</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.5</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.5</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.5</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>Main view of LogarithmPlotter showing an asymptotic Bode phase plot.</caption>
|
||||||
<caption xml:lang="de">Hauptansicht des LogarithmPlotters mit einer asymptotischen Bode-Phasendarstellung.</caption>
|
<caption xml:lang="de">Hauptansicht des LogarithmPlotters mit einer asymptotischen Bode-Phasendarstellung.</caption>
|
||||||
<caption xml:lang="fr">Vue principale de LogarithmPlotter montrant un tracé asymptotique d'une phase de Bode.</caption>
|
<caption xml:lang="fr">Vue principale de LogarithmPlotter montrant un tracé asymptotique d'une phase de Bode.</caption>
|
||||||
<caption xml:lang="hu">A LogarithmPlotter fő nézete, amely egy aszimptotikus Bode-fázis ábrát mutat.</caption>
|
<caption xml:lang="hu">A LogarithmPlotter fő nézete, amely egy aszimptotikus Bode-fázis ábrát mutat.</caption>
|
||||||
<caption xml:lang="no">Hovedvisning av LogarithmPlotter som viser et asymptotisk Bode-fasediagram.</caption>
|
<caption xml:lang="no">Hovedvisning av LogarithmPlotter som viser et asymptotisk Bode-fasediagram.</caption>
|
||||||
|
<caption xml:lang="es">Vista principal de LogarithmPlotter mostrando un gráfico asintótico de una fase de Bode.</caption>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
<screenshot>
|
<screenshot>
|
||||||
<image>https://apps.ad5001.eu/img/en/logarithmplotter/welcome.png?v=0.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.5</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.5</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.5</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.5</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>LogarithmPlotter's welcome page.</caption>
|
||||||
<caption xml:lang="de">LogarithmPlotter's Willkommensseite.</caption>
|
<caption xml:lang="de">LogarithmPlotter's Willkommensseite.</caption>
|
||||||
<caption xml:lang="fr">Page d'accueil de LogarithmPlotter.</caption>
|
<caption xml:lang="fr">Page d'accueil de LogarithmPlotter.</caption>
|
||||||
<caption xml:lang="hu">LogarithmPlotter üdvözlő oldala.</caption>
|
<caption xml:lang="hu">LogarithmPlotter üdvözlő oldala.</caption>
|
||||||
<caption xml:lang="no">LogarithmPlotters velkomstside.</caption>
|
<caption xml:lang="no">LogarithmPlotters velkomstside.</caption>
|
||||||
|
<caption xml:lang="es">Página de bienvenida de LogarithmPlotter.</caption>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
</screenshots>
|
</screenshots>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
|
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
|
||||||
<mime-type xmlns="http://www.freedesktop.org/standards/shared-mime-info" type="application/x-logarithm-plot">
|
<mime-type xmlns="http://www.freedesktop.org/standards/shared-mime-info" type="application/x-logarithm-plot">
|
||||||
<comment>Logarithm Plot File</comment>
|
<comment>Logarithmic Plot File</comment>
|
||||||
<comment xml:lang="fr">Fichier Graphe Logarithmique</comment>
|
<comment xml:lang="fr">Fichier Graphe Logarithmique</comment>
|
||||||
<icon name="application-x-logarithm-plot"/>
|
<icon name="application-x-logarithm-plot"/>
|
||||||
<glob-deleteall/>
|
<glob-deleteall/>
|
||||||
|
|
13
ci/drone.yml
13
ci/drone.yml
|
@ -12,28 +12,21 @@ steps:
|
||||||
- git submodule update --init --recursive
|
- git submodule update --init --recursive
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
image: node:18-bookworm
|
image: ad5001/ubuntu-pyside-xvfb:linux-6-latest-latex-node
|
||||||
commands:
|
commands:
|
||||||
- cd common && npm install && cd ..
|
- cd common && npm install && cd ..
|
||||||
- apt update
|
|
||||||
- apt install -y qtchooser qttools5-dev-tools
|
|
||||||
# Start building
|
|
||||||
- bash scripts/build.sh
|
- bash scripts/build.sh
|
||||||
when:
|
|
||||||
event: [ push, tag ]
|
|
||||||
|
|
||||||
- name: Unit Tests
|
- name: Unit Tests
|
||||||
image: ad5001/ubuntu-pyside-xvfb:linux-6-latest-latex
|
image: ad5001/ubuntu-pyside-xvfb:linux-6-latest-latex-node
|
||||||
commands:
|
commands:
|
||||||
- apt update
|
|
||||||
- apt install -y npm
|
|
||||||
- cd common && npm install -D && cd ..
|
- cd common && npm install -D && cd ..
|
||||||
- xvfb-run bash scripts/run-tests.sh --no-rebuild
|
- xvfb-run bash scripts/run-tests.sh --no-rebuild
|
||||||
when:
|
when:
|
||||||
event: [ push, tag ]
|
event: [ push, tag ]
|
||||||
|
|
||||||
- name: File Tests
|
- name: File Tests
|
||||||
image: ad5001/ubuntu-pyside-xvfb:linux-6-latest-latex
|
image: ad5001/ubuntu-pyside-xvfb:linux-6-latest-latex-node
|
||||||
commands:
|
commands:
|
||||||
- xvfb-run python3 run.py --test-build --no-check-for-updates
|
- xvfb-run python3 run.py --test-build --no-check-for-updates
|
||||||
- xvfb-run python3 run.py --test-build --no-check-for-updates ./ci/test1.lpf
|
- xvfb-run python3 run.py --test-build --no-check-for-updates ./ci/test1.lpf
|
||||||
|
|
1539
common/package-lock.json
generated
1539
common/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -2,7 +2,7 @@
|
||||||
"name": "logarithmplotter",
|
"name": "logarithmplotter",
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"description": "2D plotter software to make Bode plots, sequences and distribution functions.",
|
"description": "2D plotter software to make Bode plots, sequences and distribution functions.",
|
||||||
"main": "LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/autoload.mjs",
|
"main": "src/index.mjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rollup --config rollup.config.mjs",
|
"build": "rollup --config rollup.config.mjs",
|
||||||
"test": "c8 mocha test/**/*.mjs"
|
"test": "c8 mocha test/**/*.mjs"
|
||||||
|
@ -24,9 +24,12 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chai": "^5.0.0",
|
"@types/chai": "^5.0.0",
|
||||||
|
"@types/chai-spies": "^1.0.6",
|
||||||
|
"@types/chai-as-promised": "^8.0.1",
|
||||||
"@types/mocha": "^10.0.8",
|
"@types/mocha": "^10.0.8",
|
||||||
"chai": "^5.1.1",
|
"chai": "^5.1.1",
|
||||||
"chai-as-promised": "^8.0.0",
|
"chai-as-promised": "^8.0.0",
|
||||||
|
"chai-spies": "^1.1.0",
|
||||||
"esm": "^3.2.25",
|
"esm": "^3.2.25",
|
||||||
"mocha": "^10.7.3"
|
"mocha": "^10.7.3"
|
||||||
}
|
}
|
||||||
|
|
116
common/src/events.mjs
Normal file
116
common/src/events.mjs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -111,7 +111,7 @@ function simplify(tokens, unaryOps, binaryOps, ternaryOps, values) {
|
||||||
* In the given instructions, replaces variable by expr.
|
* In the given instructions, replaces variable by expr.
|
||||||
* @param {Instruction[]} tokens
|
* @param {Instruction[]} tokens
|
||||||
* @param {string} variable
|
* @param {string} variable
|
||||||
* @param {number} expr
|
* @param {ExprEvalExpression} expr
|
||||||
* @return {Instruction[]}
|
* @return {Instruction[]}
|
||||||
*/
|
*/
|
||||||
function substitute(tokens, variable, expr) {
|
function substitute(tokens, variable, expr) {
|
||||||
|
@ -171,9 +171,6 @@ function evaluate(tokens, expr, values) {
|
||||||
nstack.push(n1 ? !!evaluate(n2, expr, values) : false)
|
nstack.push(n1 ? !!evaluate(n2, expr, values) : false)
|
||||||
} else if(item.value === "or") {
|
} else if(item.value === "or") {
|
||||||
nstack.push(n1 ? true : !!evaluate(n2, expr, values))
|
nstack.push(n1 ? true : !!evaluate(n2, expr, values))
|
||||||
} else if(item.value === "=") {
|
|
||||||
f = expr.binaryOps[item.value]
|
|
||||||
nstack.push(f(n1, evaluate(n2, expr, values), values))
|
|
||||||
} else {
|
} else {
|
||||||
f = expr.binaryOps[item.value]
|
f = expr.binaryOps[item.value]
|
||||||
nstack.push(f(resolveExpression(n1, values), resolveExpression(n2, values)))
|
nstack.push(f(resolveExpression(n1, values), resolveExpression(n2, values)))
|
||||||
|
@ -490,18 +487,6 @@ export class ExprEvalExpression {
|
||||||
return evaluate(this.tokens, this, values)
|
return evaluate(this.tokens, this, values)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a list of symbols (string of characters) in the expressions.
|
|
||||||
* Can be functions, constants, or variables.
|
|
||||||
* @returns {string[]}
|
|
||||||
*/
|
|
||||||
symbols(options) {
|
|
||||||
options = options || {}
|
|
||||||
const vars = []
|
|
||||||
getSymbols(this.tokens, vars, options)
|
|
||||||
return vars
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
return expressionToString(this.tokens, false)
|
return expressionToString(this.tokens, false)
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,9 +47,7 @@ const optionNameMap = {
|
||||||
"not": "logical",
|
"not": "logical",
|
||||||
"?": "conditional",
|
"?": "conditional",
|
||||||
":": "conditional",
|
":": "conditional",
|
||||||
//'=': 'assignment', // Disable assignment
|
|
||||||
"[": "array"
|
"[": "array"
|
||||||
//'()=': 'fndef' // Diable function definition
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Parser {
|
export class Parser {
|
||||||
|
@ -109,7 +107,6 @@ export class Parser {
|
||||||
and: Polyfill.andOperator,
|
and: Polyfill.andOperator,
|
||||||
or: Polyfill.orOperator,
|
or: Polyfill.orOperator,
|
||||||
"in": Polyfill.inOperator,
|
"in": Polyfill.inOperator,
|
||||||
"=": Polyfill.setVar,
|
|
||||||
"[": Polyfill.arrayIndex
|
"[": Polyfill.arrayIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,18 +120,13 @@ export class Parser {
|
||||||
min: Polyfill.min,
|
min: Polyfill.min,
|
||||||
max: Polyfill.max,
|
max: Polyfill.max,
|
||||||
hypot: Math.hypot || Polyfill.hypot,
|
hypot: Math.hypot || Polyfill.hypot,
|
||||||
pyt: Math.hypot || Polyfill.hypot, // backward compat
|
pyt: Math.hypot || Polyfill.hypot,
|
||||||
pow: Math.pow,
|
pow: Math.pow,
|
||||||
atan2: Math.atan2,
|
atan2: Math.atan2,
|
||||||
"if": Polyfill.condition,
|
"if": Polyfill.condition,
|
||||||
gamma: Polyfill.gamma,
|
gamma: Polyfill.gamma,
|
||||||
"Γ": Polyfill.gamma,
|
"Γ": Polyfill.gamma,
|
||||||
roundTo: Polyfill.roundTo,
|
roundTo: Polyfill.roundTo,
|
||||||
map: Polyfill.arrayMap,
|
|
||||||
fold: Polyfill.arrayFold,
|
|
||||||
filter: Polyfill.arrayFilter,
|
|
||||||
indexOf: Polyfill.stringOrArrayIndexOf,
|
|
||||||
join: Polyfill.arrayJoin
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// These constants will automatically be replaced the MOMENT they are parsed.
|
// These constants will automatically be replaced the MOMENT they are parsed.
|
||||||
|
@ -159,10 +151,6 @@ export class Parser {
|
||||||
return new ExprEvalExpression(instr, this)
|
return new ExprEvalExpression(instr, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
evaluate(expr, variables) {
|
|
||||||
return this.parse(expr).evaluate(variables)
|
|
||||||
}
|
|
||||||
|
|
||||||
isOperatorEnabled(op) {
|
isOperatorEnabled(op) {
|
||||||
const optionName = optionNameMap.hasOwnProperty(op) ? optionNameMap[op] : op
|
const optionName = optionNameMap.hasOwnProperty(op) ? optionNameMap[op] : op
|
||||||
const operators = this.options.operators || {}
|
const operators = this.options.operators || {}
|
||||||
|
|
|
@ -210,9 +210,8 @@ export function gamma(n) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stringOrArrayLength(s) {
|
export function stringOrArrayLength(s) {
|
||||||
if(Array.isArray(s)) {
|
if(Array.isArray(s))
|
||||||
return s.length
|
return s.length
|
||||||
}
|
|
||||||
return String(s).length
|
return String(s).length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -267,11 +266,6 @@ export function roundTo(value, exp) {
|
||||||
return +(value[0] + "e" + (value[1] ? (+value[1] + exp) : exp))
|
return +(value[0] + "e" + (value[1] ? (+value[1] + exp) : exp))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setVar(name, value, variables) {
|
|
||||||
if(variables) variables[name] = value
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
export function arrayIndex(array, index) {
|
export function arrayIndex(array, index) {
|
||||||
return array[index | 0]
|
return array[index | 0]
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
export function sign(x) {
|
||||||
return ((x > 0) - (x < 0)) || +x
|
return ((x > 0) - (x < 0)) || +x
|
||||||
}
|
}
|
||||||
|
|
|
@ -472,7 +472,7 @@ export class TokenStream {
|
||||||
this.current = this.newToken(TOP, "==")
|
this.current = this.newToken(TOP, "==")
|
||||||
this.pos++
|
this.pos++
|
||||||
} else {
|
} else {
|
||||||
this.current = this.newToken(TOP, c)
|
return false
|
||||||
}
|
}
|
||||||
} else if(c === "!") {
|
} else if(c === "!") {
|
||||||
if(this.expression.charAt(this.pos + 1) === "=") {
|
if(this.expression.charAt(this.pos + 1) === "=") {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/**
|
/*!
|
||||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||||
* Copyright (C) 2021-2024 Ad5001
|
* Copyright (C) 2021-2024 Ad5001
|
||||||
*
|
*
|
||||||
|
@ -64,10 +64,20 @@ function arrayFlatMap(callbackFn, thisArg) {
|
||||||
* @return {String}
|
* @return {String}
|
||||||
*/
|
*/
|
||||||
function stringReplaceAll(from, to) {
|
function stringReplaceAll(from, to) {
|
||||||
let str = this
|
return this.split(from).join(to)
|
||||||
while(str.includes(from))
|
}
|
||||||
str = str.replace(from, to)
|
|
||||||
return str
|
/**
|
||||||
|
* Returns the value of an element of the array at a given index.
|
||||||
|
* Accepts negative indexes.
|
||||||
|
* @this {Array|string}
|
||||||
|
* @param {number} index
|
||||||
|
* @return {*}
|
||||||
|
*/
|
||||||
|
function arrayAt(index) {
|
||||||
|
if(typeof index !== "number")
|
||||||
|
throw new Error(`${index} is not a number`)
|
||||||
|
return index >= 0 ? this[index] : this[this.length + index]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -98,8 +108,8 @@ const polyfills = {
|
||||||
[String.prototype, "replaceAll", stringReplaceAll]
|
[String.prototype, "replaceAll", stringReplaceAll]
|
||||||
],
|
],
|
||||||
2022: [
|
2022: [
|
||||||
[Array.prototype, "at", notPolyfilled("Array.prototype.at")],
|
[Array.prototype, "at", arrayAt],
|
||||||
[String.prototype, "at", notPolyfilled("String.prototype.at")],
|
[String.prototype, "at", arrayAt],
|
||||||
[Object, "hasOwn", notPolyfilled("Object.hasOwn")]
|
[Object, "hasOwn", notPolyfilled("Object.hasOwn")]
|
||||||
],
|
],
|
||||||
2023: [
|
2023: [
|
||||||
|
|
|
@ -23,25 +23,25 @@ import { Expression } from "../math/index.mjs"
|
||||||
import Latex from "./latex.mjs"
|
import Latex from "./latex.mjs"
|
||||||
import Objects from "./objects.mjs"
|
import Objects from "./objects.mjs"
|
||||||
import History from "./history.mjs"
|
import History from "./history.mjs"
|
||||||
|
import Settings from "./settings.mjs"
|
||||||
|
|
||||||
class CanvasAPI extends Module {
|
class CanvasAPI extends Module {
|
||||||
|
/** @type {CanvasInterface} */
|
||||||
|
#canvas = null
|
||||||
|
/** @type {CanvasRenderingContext2D} */
|
||||||
|
#ctx = null
|
||||||
|
/** Lock to prevent asynchronous stuff from printing stuff that is outdated. */
|
||||||
|
#redrawCount = 0
|
||||||
|
/** @type {{show(string, string, string)}} */
|
||||||
|
#drawingErrorDialog = null
|
||||||
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super("Canvas", {
|
super("Canvas", {
|
||||||
canvas: CanvasInterface,
|
canvas: CanvasInterface,
|
||||||
drawingErrorDialog: DialogInterface
|
drawingErrorDialog: DialogInterface
|
||||||
})
|
})
|
||||||
|
|
||||||
/** @type {CanvasInterface} */
|
|
||||||
this._canvas = null
|
|
||||||
|
|
||||||
/** @type {CanvasRenderingContext2D} */
|
|
||||||
this._ctx = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {{show(string, string, string)}}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
this._drawingErrorDialog = null
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {Object.<string, {expression: Expression, value: number, maxDraw: number}>}
|
* @type {Object.<string, {expression: Expression, value: number, maxDraw: number}>}
|
||||||
|
@ -67,18 +67,18 @@ class CanvasAPI extends Module {
|
||||||
*/
|
*/
|
||||||
initialize({ canvas, drawingErrorDialog }) {
|
initialize({ canvas, drawingErrorDialog }) {
|
||||||
super.initialize({ canvas, drawingErrorDialog })
|
super.initialize({ canvas, drawingErrorDialog })
|
||||||
this._canvas = canvas
|
this.#canvas = canvas
|
||||||
this._drawingErrorDialog = drawingErrorDialog
|
this.#drawingErrorDialog = drawingErrorDialog
|
||||||
}
|
}
|
||||||
|
|
||||||
get width() {
|
get width() {
|
||||||
if(!this.initialized) throw new Error("Attempting width before initialize!")
|
if(!this.initialized) throw new Error("Attempting width before initialize!")
|
||||||
return this._canvas.width
|
return this.#canvas.width
|
||||||
}
|
}
|
||||||
|
|
||||||
get height() {
|
get height() {
|
||||||
if(!this.initialized) throw new Error("Attempting height before initialize!")
|
if(!this.initialized) throw new Error("Attempting height before initialize!")
|
||||||
return this._canvas.height
|
return this.#canvas.height
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -87,7 +87,7 @@ class CanvasAPI extends Module {
|
||||||
*/
|
*/
|
||||||
get xmin() {
|
get xmin() {
|
||||||
if(!this.initialized) throw new Error("Attempting xmin before initialize!")
|
if(!this.initialized) throw new Error("Attempting xmin before initialize!")
|
||||||
return this._canvas.xmin
|
return Settings.xmin
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -96,7 +96,7 @@ class CanvasAPI extends Module {
|
||||||
*/
|
*/
|
||||||
get xzoom() {
|
get xzoom() {
|
||||||
if(!this.initialized) throw new Error("Attempting xzoom before initialize!")
|
if(!this.initialized) throw new Error("Attempting xzoom before initialize!")
|
||||||
return this._canvas.xzoom
|
return Settings.xzoom
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -105,7 +105,7 @@ class CanvasAPI extends Module {
|
||||||
*/
|
*/
|
||||||
get ymax() {
|
get ymax() {
|
||||||
if(!this.initialized) throw new Error("Attempting ymax before initialize!")
|
if(!this.initialized) throw new Error("Attempting ymax before initialize!")
|
||||||
return this._canvas.ymax
|
return Settings.ymax
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -114,7 +114,7 @@ class CanvasAPI extends Module {
|
||||||
*/
|
*/
|
||||||
get yzoom() {
|
get yzoom() {
|
||||||
if(!this.initialized) throw new Error("Attempting yzoom before initialize!")
|
if(!this.initialized) throw new Error("Attempting yzoom before initialize!")
|
||||||
return this._canvas.yzoom
|
return Settings.yzoom
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -123,7 +123,7 @@ class CanvasAPI extends Module {
|
||||||
*/
|
*/
|
||||||
get xlabel() {
|
get xlabel() {
|
||||||
if(!this.initialized) throw new Error("Attempting xlabel before initialize!")
|
if(!this.initialized) throw new Error("Attempting xlabel before initialize!")
|
||||||
return this._canvas.xlabel
|
return Settings.xlabel
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -132,7 +132,7 @@ class CanvasAPI extends Module {
|
||||||
*/
|
*/
|
||||||
get ylabel() {
|
get ylabel() {
|
||||||
if(!this.initialized) throw new Error("Attempting ylabel before initialize!")
|
if(!this.initialized) throw new Error("Attempting ylabel before initialize!")
|
||||||
return this._canvas.ylabel
|
return Settings.ylabel
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -141,7 +141,7 @@ class CanvasAPI extends Module {
|
||||||
*/
|
*/
|
||||||
get linewidth() {
|
get linewidth() {
|
||||||
if(!this.initialized) throw new Error("Attempting linewidth before initialize!")
|
if(!this.initialized) throw new Error("Attempting linewidth before initialize!")
|
||||||
return this._canvas.linewidth
|
return Settings.linewidth
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -150,7 +150,7 @@ class CanvasAPI extends Module {
|
||||||
*/
|
*/
|
||||||
get textsize() {
|
get textsize() {
|
||||||
if(!this.initialized) throw new Error("Attempting textsize before initialize!")
|
if(!this.initialized) throw new Error("Attempting textsize before initialize!")
|
||||||
return this._canvas.textsize
|
return Settings.textsize
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -159,7 +159,7 @@ class CanvasAPI extends Module {
|
||||||
*/
|
*/
|
||||||
get logscalex() {
|
get logscalex() {
|
||||||
if(!this.initialized) throw new Error("Attempting logscalex before initialize!")
|
if(!this.initialized) throw new Error("Attempting logscalex before initialize!")
|
||||||
return this._canvas.logscalex
|
return Settings.logscalex
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -168,7 +168,7 @@ class CanvasAPI extends Module {
|
||||||
*/
|
*/
|
||||||
get showxgrad() {
|
get showxgrad() {
|
||||||
if(!this.initialized) throw new Error("Attempting showxgrad before initialize!")
|
if(!this.initialized) throw new Error("Attempting showxgrad before initialize!")
|
||||||
return this._canvas.showxgrad
|
return Settings.showxgrad
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -177,7 +177,7 @@ class CanvasAPI extends Module {
|
||||||
*/
|
*/
|
||||||
get showygrad() {
|
get showygrad() {
|
||||||
if(!this.initialized) throw new Error("Attempting showygrad before initialize!")
|
if(!this.initialized) throw new Error("Attempting showygrad before initialize!")
|
||||||
return this._canvas.showygrad
|
return Settings.showygrad
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -201,7 +201,7 @@ class CanvasAPI extends Module {
|
||||||
|
|
||||||
requestPaint() {
|
requestPaint() {
|
||||||
if(!this.initialized) throw new Error("Attempting requestPaint before initialize!")
|
if(!this.initialized) throw new Error("Attempting requestPaint before initialize!")
|
||||||
this._canvas.requestPaint()
|
this.#canvas.requestPaint()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -209,17 +209,18 @@ class CanvasAPI extends Module {
|
||||||
*/
|
*/
|
||||||
redraw() {
|
redraw() {
|
||||||
if(!this.initialized) throw new Error("Attempting redraw before initialize!")
|
if(!this.initialized) throw new Error("Attempting redraw before initialize!")
|
||||||
this._ctx = this._canvas.getContext("2d")
|
this.#redrawCount = (this.#redrawCount + 1) % 10000
|
||||||
|
this.#ctx = this.#canvas.getContext("2d")
|
||||||
this._computeAxes()
|
this._computeAxes()
|
||||||
this._reset()
|
this._reset()
|
||||||
this._drawGrid()
|
this._drawGrid()
|
||||||
this._drawAxes()
|
this._drawAxes()
|
||||||
this._drawLabels()
|
this._drawLabels()
|
||||||
this._ctx.lineWidth = this.linewidth
|
this.#ctx.lineWidth = this.linewidth
|
||||||
for(let objType in Objects.currentObjects) {
|
for(let objType in Objects.currentObjects) {
|
||||||
for(let obj of Objects.currentObjects[objType]) {
|
for(let obj of Objects.currentObjects[objType]) {
|
||||||
this._ctx.strokeStyle = obj.color
|
this.#ctx.strokeStyle = obj.color
|
||||||
this._ctx.fillStyle = obj.color
|
this.#ctx.fillStyle = obj.color
|
||||||
if(obj.visible)
|
if(obj.visible)
|
||||||
try {
|
try {
|
||||||
obj.draw(this)
|
obj.draw(this)
|
||||||
|
@ -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)
|
// Drawing throws an error. Generally, it's due to a new modification (or the opening of a file)
|
||||||
console.error(e)
|
console.error(e)
|
||||||
console.log(e.stack)
|
console.log(e.stack)
|
||||||
this._drawingErrorDialog.show(objType, obj.name, e.message)
|
this.#drawingErrorDialog.show(objType, obj.name, e.message)
|
||||||
History.undo()
|
History.undo()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._ctx.lineWidth = 1
|
this.#ctx.lineWidth = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -240,9 +241,9 @@ class CanvasAPI extends Module {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_computeAxes() {
|
_computeAxes() {
|
||||||
let exprY = new Expression(`x*(${this._canvas.yaxisstep})`)
|
let exprY = new Expression(`x*(${Settings.yaxisstep})`)
|
||||||
let y1 = exprY.execute(1)
|
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)
|
let x1 = exprX.execute(1)
|
||||||
this.axesSteps = {
|
this.axesSteps = {
|
||||||
x: {
|
x: {
|
||||||
|
@ -264,10 +265,10 @@ class CanvasAPI extends Module {
|
||||||
*/
|
*/
|
||||||
_reset() {
|
_reset() {
|
||||||
// Reset
|
// Reset
|
||||||
this._ctx.fillStyle = "#FFFFFF"
|
this.#ctx.fillStyle = "#FFFFFF"
|
||||||
this._ctx.strokeStyle = "#000000"
|
this.#ctx.strokeStyle = "#000000"
|
||||||
this._ctx.font = `${this.textsize}px sans-serif`
|
this.#ctx.font = `${this.textsize}px sans-serif`
|
||||||
this._ctx.fillRect(0, 0, this.width, this.height)
|
this.#ctx.fillRect(0, 0, this.width, this.height)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -275,7 +276,7 @@ class CanvasAPI extends Module {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_drawGrid() {
|
_drawGrid() {
|
||||||
this._ctx.strokeStyle = "#C0C0C0"
|
this.#ctx.strokeStyle = "#C0C0C0"
|
||||||
if(this.logscalex) {
|
if(this.logscalex) {
|
||||||
for(let xpow = -this.maxgradx; xpow <= this.maxgradx; xpow++) {
|
for(let xpow = -this.maxgradx; xpow <= this.maxgradx; xpow++) {
|
||||||
for(let xmulti = 1; xmulti < 10; xmulti++) {
|
for(let xmulti = 1; xmulti < 10; xmulti++) {
|
||||||
|
@ -299,7 +300,7 @@ class CanvasAPI extends Module {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_drawAxes() {
|
_drawAxes() {
|
||||||
this._ctx.strokeStyle = "#000000"
|
this.#ctx.strokeStyle = "#000000"
|
||||||
let axisypos = this.logscalex ? 1 : 0
|
let axisypos = this.logscalex ? 1 : 0
|
||||||
this.drawXLine(axisypos)
|
this.drawXLine(axisypos)
|
||||||
this.drawYLine(0)
|
this.drawYLine(0)
|
||||||
|
@ -320,19 +321,19 @@ class CanvasAPI extends Module {
|
||||||
let axisypx = this.x2px(this.logscalex ? 1 : 0) // X coordinate of Y axis
|
let axisypx = this.x2px(this.logscalex ? 1 : 0) // X coordinate of Y axis
|
||||||
let axisxpx = this.y2px(0) // Y coordinate of X axis
|
let axisxpx = this.y2px(0) // Y coordinate of X axis
|
||||||
// Labels
|
// Labels
|
||||||
this._ctx.fillStyle = "#000000"
|
this.#ctx.fillStyle = "#000000"
|
||||||
this._ctx.font = `${this.textsize}px sans-serif`
|
this.#ctx.font = `${this.textsize}px sans-serif`
|
||||||
this._ctx.fillText(this.ylabel, axisypx + 10, 24)
|
this.#ctx.fillText(this.ylabel, axisypx + 10, 24)
|
||||||
let textWidth = this._ctx.measureText(this.xlabel).width
|
let textWidth = this.#ctx.measureText(this.xlabel).width
|
||||||
this._ctx.fillText(this.xlabel, this.width - 14 - textWidth, axisxpx - 5)
|
this.#ctx.fillText(this.xlabel, this.width - 14 - textWidth, axisxpx - 5)
|
||||||
// Axis graduation labels
|
// Axis graduation labels
|
||||||
this._ctx.font = `${this.textsize - 4}px sans-serif`
|
this.#ctx.font = `${this.textsize - 4}px sans-serif`
|
||||||
|
|
||||||
let txtMinus = this._ctx.measureText("-").width
|
let txtMinus = this.#ctx.measureText("-").width
|
||||||
if(this.showxgrad) {
|
if(this.showxgrad) {
|
||||||
if(this.logscalex) {
|
if(this.logscalex) {
|
||||||
for(let xpow = -this.maxgradx; xpow <= this.maxgradx; xpow += 1) {
|
for(let xpow = -this.maxgradx; xpow <= this.maxgradx; xpow += 1) {
|
||||||
textWidth = this._ctx.measureText("10" + textsup(xpow)).width
|
textWidth = this.#ctx.measureText("10" + textsup(xpow)).width
|
||||||
if(xpow !== 0)
|
if(xpow !== 0)
|
||||||
this.drawVisibleText("10" + textsup(xpow), this.x2px(Math.pow(10, xpow)) - textWidth / 2, axisxpx + 16 + (6 * (xpow === 1)))
|
this.drawVisibleText("10" + textsup(xpow), this.x2px(Math.pow(10, xpow)) - textWidth / 2, axisxpx + 16 + (6 * (xpow === 1)))
|
||||||
}
|
}
|
||||||
|
@ -350,13 +351,13 @@ class CanvasAPI extends Module {
|
||||||
for(let y = 0; y < this.axesSteps.y.maxDraw; y += 1) {
|
for(let y = 0; y < this.axesSteps.y.maxDraw; y += 1) {
|
||||||
let drawY = y * this.axesSteps.y.value
|
let drawY = y * this.axesSteps.y.value
|
||||||
let txtY = this.axesSteps.y.expression.simplify(y).toString().replace(/^\((.+)\)$/, "$1")
|
let txtY = this.axesSteps.y.expression.simplify(y).toString().replace(/^\((.+)\)$/, "$1")
|
||||||
textWidth = this._ctx.measureText(txtY).width
|
textWidth = this.#ctx.measureText(txtY).width
|
||||||
this.drawVisibleText(txtY, axisypx - 6 - textWidth, this.y2px(drawY) + 4 + (10 * (y === 0)))
|
this.drawVisibleText(txtY, axisypx - 6 - textWidth, this.y2px(drawY) + 4 + (10 * (y === 0)))
|
||||||
if(y !== 0)
|
if(y !== 0)
|
||||||
this.drawVisibleText("-" + txtY, axisypx - 6 - textWidth - txtMinus, this.y2px(-drawY) + 4)
|
this.drawVisibleText("-" + txtY, axisypx - 6 - textWidth - txtMinus, this.y2px(-drawY) + 4)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._ctx.fillStyle = "#FFFFFF"
|
this.#ctx.fillStyle = "#FFFFFF"
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -394,7 +395,7 @@ class CanvasAPI extends Module {
|
||||||
drawVisibleText(text, x, y) {
|
drawVisibleText(text, x, y) {
|
||||||
if(x > 0 && x < this.width && y > 0 && y < this.height) {
|
if(x > 0 && x < this.width && y > 0 && y < this.height) {
|
||||||
text.toString().split("\n").forEach((txt, i) => {
|
text.toString().split("\n").forEach((txt, i) => {
|
||||||
this._ctx.fillText(txt, x, y + (this.textsize * i))
|
this.#ctx.fillText(txt, x, y + (this.textsize * i))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -409,8 +410,8 @@ class CanvasAPI extends Module {
|
||||||
* @param {number} height
|
* @param {number} height
|
||||||
*/
|
*/
|
||||||
drawVisibleImage(image, x, y, width, height) {
|
drawVisibleImage(image, x, y, width, height) {
|
||||||
this._canvas.markDirty(Qt.rect(x, y, width, height))
|
this.#canvas.markDirty(Qt.rect(x, y, width, height))
|
||||||
this._ctx.drawImage(image, x, y, width, height)
|
this.#ctx.drawImage(image, x, y, width, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -424,7 +425,7 @@ class CanvasAPI extends Module {
|
||||||
let defaultHeight = this.textsize * 1.2 // Approximate but good enough!
|
let defaultHeight = this.textsize * 1.2 // Approximate but good enough!
|
||||||
for(let txt of text.split("\n")) {
|
for(let txt of text.split("\n")) {
|
||||||
theight += defaultHeight
|
theight += defaultHeight
|
||||||
if(this._ctx.measureText(txt).width > twidth) twidth = this._ctx.measureText(txt).width
|
if(this.#ctx.measureText(txt).width > twidth) twidth = this.#ctx.measureText(txt).width
|
||||||
}
|
}
|
||||||
return { "width": twidth, "height": theight }
|
return { "width": twidth, "height": theight }
|
||||||
}
|
}
|
||||||
|
@ -494,10 +495,10 @@ class CanvasAPI extends Module {
|
||||||
* @param {number} y2
|
* @param {number} y2
|
||||||
*/
|
*/
|
||||||
drawLine(x1, y1, x2, y2) {
|
drawLine(x1, y1, x2, y2) {
|
||||||
this._ctx.beginPath()
|
this.#ctx.beginPath()
|
||||||
this._ctx.moveTo(x1, y1)
|
this.#ctx.moveTo(x1, y1)
|
||||||
this._ctx.lineTo(x2, y2)
|
this.#ctx.lineTo(x2, y2)
|
||||||
this._ctx.stroke()
|
this.#ctx.stroke()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -509,9 +510,9 @@ class CanvasAPI extends Module {
|
||||||
* @param {number} dashPxSize
|
* @param {number} dashPxSize
|
||||||
*/
|
*/
|
||||||
drawDashedLine(x1, y1, x2, y2, dashPxSize = 6) {
|
drawDashedLine(x1, y1, x2, y2, dashPxSize = 6) {
|
||||||
this._ctx.setLineDash([dashPxSize / 2, dashPxSize])
|
this.#ctx.setLineDash([dashPxSize / 2, dashPxSize])
|
||||||
this.drawLine(x1, y1, x2, y2)
|
this.drawLine(x1, y1, x2, y2)
|
||||||
this._ctx.setLineDash([])
|
this.#ctx.setLineDash([])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -521,14 +522,22 @@ class CanvasAPI extends Module {
|
||||||
* @param {function(LatexRenderResult|{width: number, height: number, source: string})} callback
|
* @param {function(LatexRenderResult|{width: number, height: number, source: string})} callback
|
||||||
*/
|
*/
|
||||||
renderLatexImage(ltxText, color, callback) {
|
renderLatexImage(ltxText, color, callback) {
|
||||||
|
const currentRedrawCount = this.#redrawCount
|
||||||
const onRendered = (imgData) => {
|
const onRendered = (imgData) => {
|
||||||
if(!this._canvas.isImageLoaded(imgData.source) && !this._canvas.isImageLoading(imgData.source)) {
|
if(!this.#canvas.isImageLoaded(imgData.source) && !this.#canvas.isImageLoading(imgData.source)) {
|
||||||
// Wait until the image is loaded to callback.
|
// Wait until the image is loaded to callback.
|
||||||
this._canvas.loadImage(imgData.source)
|
this.#canvas.loadImageAsync(imgData.source).then(() => {
|
||||||
this._canvas.imageLoaders[imgData.source] = [callback, imgData]
|
if(this.#redrawCount === currentRedrawCount)
|
||||||
|
callback(imgData)
|
||||||
|
else
|
||||||
|
console.log("1. Discard render of", imgData.source, this.#redrawCount, currentRedrawCount)
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
// Callback directly
|
// Callback directly
|
||||||
|
if(this.#redrawCount === currentRedrawCount)
|
||||||
callback(imgData)
|
callback(imgData)
|
||||||
|
else
|
||||||
|
console.log("2. Discard render of", imgData.source, this.#redrawCount, currentRedrawCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const prerendered = Latex.findPrerendered(ltxText, this.textsize, color)
|
const prerendered = Latex.findPrerendered(ltxText, this.textsize, color)
|
||||||
|
@ -543,11 +552,11 @@ class CanvasAPI extends Module {
|
||||||
//
|
//
|
||||||
|
|
||||||
get font() {
|
get font() {
|
||||||
return this._ctx.font
|
return this.#ctx.font
|
||||||
}
|
}
|
||||||
|
|
||||||
set font(value) {
|
set font(value) {
|
||||||
return this._ctx.font = value
|
return this.#ctx.font = value
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -560,9 +569,9 @@ class CanvasAPI extends Module {
|
||||||
* @param {boolean} counterclockwise
|
* @param {boolean} counterclockwise
|
||||||
*/
|
*/
|
||||||
arc(x, y, radius, startAngle, endAngle, counterclockwise = false) {
|
arc(x, y, radius, startAngle, endAngle, counterclockwise = false) {
|
||||||
this._ctx.beginPath()
|
this.#ctx.beginPath()
|
||||||
this._ctx.arc(x, y, radius, startAngle, endAngle, counterclockwise)
|
this.#ctx.arc(x, y, radius, startAngle, endAngle, counterclockwise)
|
||||||
this._ctx.stroke()
|
this.#ctx.stroke()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -572,9 +581,9 @@ class CanvasAPI extends Module {
|
||||||
* @param {number} radius
|
* @param {number} radius
|
||||||
*/
|
*/
|
||||||
disc(x, y, radius) {
|
disc(x, y, radius) {
|
||||||
this._ctx.beginPath()
|
this.#ctx.beginPath()
|
||||||
this._ctx.arc(x, y, radius, 0, 2 * Math.PI)
|
this.#ctx.arc(x, y, radius, 0, 2 * Math.PI)
|
||||||
this._ctx.fill()
|
this.#ctx.fill()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -585,7 +594,7 @@ class CanvasAPI extends Module {
|
||||||
* @param {number} h
|
* @param {number} h
|
||||||
*/
|
*/
|
||||||
fillRect(x, y, w, h) {
|
fillRect(x, y, w, h) {
|
||||||
this._ctx.fillRect(x, y, w, h)
|
this.#ctx.fillRect(x, y, w, h)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Interface } from "./interface.mjs"
|
import { Interface } from "./interface.mjs"
|
||||||
|
import { BaseEventEmitter } from "../events.mjs"
|
||||||
|
|
||||||
// Define Modules interface before they are imported.
|
// Define Modules interface before they are imported.
|
||||||
globalThis.Modules = globalThis.Modules || {}
|
globalThis.Modules = globalThis.Modules || {}
|
||||||
|
@ -24,7 +25,13 @@ globalThis.Modules = globalThis.Modules || {}
|
||||||
/**
|
/**
|
||||||
* Base class for global APIs in runtime.
|
* 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.
|
* @param {Object.<string, (Interface|string|number|boolean)>} initializationParameters - List of parameters for the initialize function.
|
||||||
*/
|
*/
|
||||||
constructor(name, initializationParameters = {}) {
|
constructor(name, initializationParameters = {}) {
|
||||||
|
super()
|
||||||
console.log(`Loading module ${name}...`)
|
console.log(`Loading module ${name}...`)
|
||||||
this.__name = name
|
this.#name = name
|
||||||
this.__initializationParameters = initializationParameters
|
this.#initializationParameters = initializationParameters
|
||||||
this.initialized = false
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this.#name;
|
||||||
|
}
|
||||||
|
|
||||||
|
get initialized() {
|
||||||
|
return this.#initialized
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -44,17 +58,17 @@ export class Module {
|
||||||
* @param {Object.<string, any>} options
|
* @param {Object.<string, any>} options
|
||||||
*/
|
*/
|
||||||
initialize(options) {
|
initialize(options) {
|
||||||
if(this.initialized)
|
if(this.#initialized)
|
||||||
throw new Error(`Cannot reinitialize module ${this.__name}.`)
|
throw new Error(`Cannot reinitialize module ${this.#name}.`)
|
||||||
console.log(`Initializing ${this.__name}...`)
|
console.log(`Initializing ${this.#name}...`)
|
||||||
for(const [name, value] of Object.entries(this.__initializationParameters)) {
|
for(const [name, value] of Object.entries(this.#initializationParameters)) {
|
||||||
if(!options.hasOwnProperty(name))
|
if(!options.hasOwnProperty(name))
|
||||||
throw new Error(`Option '${name}' of initialize of module ${this.__name} does not exist.`)
|
throw new Error(`Option '${name}' of initialize of module ${this.#name} does not exist.`)
|
||||||
if(typeof value === "function" && value.prototype instanceof Interface)
|
if(typeof value === "function" && value.prototype instanceof Interface)
|
||||||
Interface.check_implementation(value, options[name])
|
Interface.checkImplementation(value, options[name])
|
||||||
else if(typeof value !== typeof options[name])
|
else if(typeof value !== typeof options[name])
|
||||||
throw new Error(`Option '${name}' of initialize of module ${this.__name} is not a '${value}' (${typeof options[name]}).`)
|
throw new Error(`Option '${name}' of initialize of module ${this.#name} is not a '${value}' (${typeof options[name]}).`)
|
||||||
}
|
}
|
||||||
this.initialized = true
|
this.#initialized = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
import { Module } from "./common.mjs"
|
import { Module } from "./common.mjs"
|
||||||
import { Parser } from "../lib/expr-eval/parser.mjs"
|
import { Parser } from "../lib/expr-eval/parser.mjs"
|
||||||
|
|
||||||
const evalVariables = {
|
const EVAL_VARIABLES = {
|
||||||
// Variables not provided by expr-eval.js, needs to be provided manually
|
// Variables not provided by expr-eval.js, needs to be provided manually
|
||||||
"pi": Math.PI,
|
"pi": Math.PI,
|
||||||
"PI": Math.PI,
|
"PI": Math.PI,
|
||||||
|
@ -35,15 +35,17 @@ const evalVariables = {
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExprParserAPI extends Module {
|
class ExprParserAPI extends Module {
|
||||||
|
#parser = new Parser()
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super("ExprParser")
|
super("ExprParser")
|
||||||
this.currentVars = {}
|
this.currentVars = {}
|
||||||
this._parser = new Parser()
|
this.#parser = new Parser()
|
||||||
|
|
||||||
this._parser.consts = Object.assign({}, this._parser.consts, evalVariables)
|
this.#parser.consts = Object.assign({}, this.#parser.consts, EVAL_VARIABLES)
|
||||||
|
|
||||||
this._parser.functions.integral = this.integral.bind(this)
|
this.#parser.functions.integral = this.integral.bind(this)
|
||||||
this._parser.functions.derivative = this.derivative.bind(this)
|
this.#parser.functions.derivative = this.derivative.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -68,7 +70,7 @@ class ExprParserAPI extends Module {
|
||||||
[f, variable] = args
|
[f, variable] = args
|
||||||
if(typeof f !== "string" || typeof variable !== "string")
|
if(typeof f !== "string" || typeof variable !== "string")
|
||||||
throw EvalError(qsTranslate("usage", "Usage:\n%1").arg(usage2))
|
throw EvalError(qsTranslate("usage", "Usage:\n%1").arg(usage2))
|
||||||
f = this._parser.parse(f).toJSFunction(variable, this.currentVars)
|
f = this.#parser.parse(f).toJSFunction(variable, this.currentVars)
|
||||||
} else
|
} else
|
||||||
throw EvalError(qsTranslate("usage", "Usage:\n%1\n%2").arg(usage1).arg(usage2))
|
throw EvalError(qsTranslate("usage", "Usage:\n%1\n%2").arg(usage1).arg(usage2))
|
||||||
return f
|
return f
|
||||||
|
@ -79,14 +81,14 @@ class ExprParserAPI extends Module {
|
||||||
* @returns {ExprEvalExpression}
|
* @returns {ExprEvalExpression}
|
||||||
*/
|
*/
|
||||||
parse(expression) {
|
parse(expression) {
|
||||||
return this._parser.parse(expression)
|
return this.#parser.parse(expression)
|
||||||
}
|
}
|
||||||
|
|
||||||
integral(a, b, ...args) {
|
integral(a = null, b = null, ...args) {
|
||||||
let usage1 = qsTranslate("usage", "integral(<from: number>, <to: number>, <f: ExecutableObject>)")
|
let usage1 = qsTranslate("usage", "integral(<from: number>, <to: number>, <f: ExecutableObject>)")
|
||||||
let usage2 = qsTranslate("usage", "integral(<from: number>, <to: number>, <f: string>, <variable: string>)")
|
let usage2 = qsTranslate("usage", "integral(<from: number>, <to: number>, <f: string>, <variable: string>)")
|
||||||
let f = this.parseArgumentsForFunction(args, usage1, usage2)
|
let f = this.parseArgumentsForFunction(args, usage1, usage2)
|
||||||
if(a == null || b == null)
|
if(typeof a !== "number" || typeof b !== "number")
|
||||||
throw EvalError(qsTranslate("usage", "Usage:\n%1\n%2").arg(usage1).arg(usage2))
|
throw EvalError(qsTranslate("usage", "Usage:\n%1\n%2").arg(usage1).arg(usage2))
|
||||||
|
|
||||||
// https://en.wikipedia.org/wiki/Simpson%27s_rule
|
// https://en.wikipedia.org/wiki/Simpson%27s_rule
|
||||||
|
@ -99,10 +101,10 @@ class ExprParserAPI extends Module {
|
||||||
let usage2 = qsTranslate("usage", "derivative(<f: string>, <variable: string>, <x: number>)")
|
let usage2 = qsTranslate("usage", "derivative(<f: string>, <variable: string>, <x: number>)")
|
||||||
let x = args.pop()
|
let x = args.pop()
|
||||||
let f = this.parseArgumentsForFunction(args, usage1, usage2)
|
let f = this.parseArgumentsForFunction(args, usage1, usage2)
|
||||||
if(x == null)
|
if(typeof x !== "number")
|
||||||
throw EvalError(qsTranslate("usage", "Usage:\n%1\n%2").arg(usage1).arg(usage2))
|
throw EvalError(qsTranslate("usage", "Usage:\n%1\n%2").arg(usage1).arg(usage2))
|
||||||
|
|
||||||
let derivative_precision = x / 10
|
let derivative_precision = 1e-8
|
||||||
return (f(x + derivative_precision / 2) - f(x - derivative_precision / 2)) / derivative_precision
|
return (f(x + derivative_precision / 2) - f(x - derivative_precision / 2)) / derivative_precision
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,60 +17,164 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Module } from "./common.mjs"
|
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 {
|
class HistoryAPI extends Module {
|
||||||
|
static emits = ["cleared", "loaded", "added", "undone", "redone"]
|
||||||
|
|
||||||
|
#helper
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super("History", {
|
super("History", {
|
||||||
historyObj: HistoryInterface,
|
helper: HelperInterface,
|
||||||
themeTextColor: STRING,
|
themeTextColor: STRING,
|
||||||
imageDepth: NUMBER,
|
imageDepth: NUMBER,
|
||||||
fontSize: NUMBER
|
fontSize: NUMBER
|
||||||
})
|
})
|
||||||
// History QML object
|
// History QML object
|
||||||
this.history = null
|
/** @type {Action[]} */
|
||||||
|
this.undoStack = []
|
||||||
|
/** @type {Action[]} */
|
||||||
|
this.redoStack = []
|
||||||
|
|
||||||
this.themeTextColor = "#FF0000"
|
this.themeTextColor = "#FF0000"
|
||||||
this.imageDepth = 2
|
this.imageDepth = 2
|
||||||
this.fontSize = 28
|
this.fontSize = 28
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize({ historyObj, themeTextColor, imageDepth, fontSize }) {
|
/**
|
||||||
super.initialize({ historyObj, themeTextColor, imageDepth, fontSize })
|
* @param {HelperInterface} historyObj
|
||||||
this.history = 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.themeTextColor = themeTextColor
|
||||||
this.imageDepth = imageDepth
|
this.imageDepth = imageDepth
|
||||||
this.fontSize = fontSize
|
this.fontSize = fontSize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Undoes the Action at the top of the undo stack and pushes it to the top of the redo stack.
|
||||||
|
*/
|
||||||
undo() {
|
undo() {
|
||||||
if(!this.initialized) throw new Error("Attempting undo before initialize!")
|
if(!this.initialized) throw new Error("Attempting undo before initialize!")
|
||||||
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() {
|
redo() {
|
||||||
if(!this.initialized) throw new Error("Attempting redo before initialize!")
|
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() {
|
clear() {
|
||||||
if(!this.initialized) throw new Error("Attempting clear before initialize!")
|
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) {
|
addToHistory(action) {
|
||||||
if(!this.initialized) throw new Error("Attempting addToHistory before initialize!")
|
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!")
|
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() {
|
serialize() {
|
||||||
if(!this.initialized) throw new Error("Attempting serialize before initialize!")
|
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]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Objects from "./objects.mjs"
|
import Objects from "./objects.mjs"
|
||||||
|
import Settings from "./settings.mjs"
|
||||||
import ExprParser from "./expreval.mjs"
|
import ExprParser from "./expreval.mjs"
|
||||||
import Latex from "./latex.mjs"
|
import Latex from "./latex.mjs"
|
||||||
import History from "./history.mjs"
|
import History from "./history.mjs"
|
||||||
|
@ -26,6 +27,7 @@ import Preferences from "./preferences.mjs"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Objects,
|
Objects,
|
||||||
|
Settings,
|
||||||
ExprParser,
|
ExprParser,
|
||||||
Latex,
|
Latex,
|
||||||
History,
|
History,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/*!
|
/**
|
||||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||||
*
|
*
|
||||||
* @author Ad5001 <mail@ad5001.eu>
|
* @author Ad5001 <mail@ad5001.eu>
|
||||||
|
@ -35,9 +35,8 @@ export class Interface {
|
||||||
* Throws an error if the implementation does not conform to the interface.
|
* Throws an error if the implementation does not conform to the interface.
|
||||||
* @param {typeof Interface} interface_
|
* @param {typeof Interface} interface_
|
||||||
* @param {object} classToCheck
|
* @param {object} classToCheck
|
||||||
* @return {boolean}
|
|
||||||
*/
|
*/
|
||||||
static check_implementation(interface_, classToCheck) {
|
static checkImplementation(interface_, classToCheck) {
|
||||||
const properties = new interface_()
|
const properties = new interface_()
|
||||||
const interfaceName = interface_.name
|
const interfaceName = interface_.name
|
||||||
const toCheckName = classToCheck.constructor.name
|
const toCheckName = classToCheck.constructor.name
|
||||||
|
@ -52,7 +51,7 @@ export class Interface {
|
||||||
else if((typeof value) === "object")
|
else if((typeof value) === "object")
|
||||||
// Test type of object.
|
// Test type of object.
|
||||||
if(value instanceof Interface)
|
if(value instanceof Interface)
|
||||||
Interface.check_implementation(value, classToCheck[property])
|
Interface.checkImplementation(value, classToCheck[property])
|
||||||
else if(value.prototype && !(classToCheck[property] instanceof value))
|
else if(value.prototype && !(classToCheck[property] instanceof value))
|
||||||
throw new Error(`Property '${property}' of ${interfaceName} implementation ${toCheckName} is not '${value.constructor.name}'.`)
|
throw new Error(`Property '${property}' of ${interfaceName} implementation ${toCheckName} is not '${value.constructor.name}'.`)
|
||||||
}
|
}
|
||||||
|
@ -60,32 +59,13 @@ export class Interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class SettingsInterface extends Interface {
|
export class CanvasInterface extends Interface {
|
||||||
width = NUMBER
|
|
||||||
height = NUMBER
|
|
||||||
xmin = NUMBER
|
|
||||||
ymax = NUMBER
|
|
||||||
xzoom = NUMBER
|
|
||||||
yzoom = NUMBER
|
|
||||||
xaxisstep = STRING
|
|
||||||
yaxisstep = STRING
|
|
||||||
xlabel = STRING
|
|
||||||
ylabel = STRING
|
|
||||||
linewidth = NUMBER
|
|
||||||
textsize = NUMBER
|
|
||||||
logscalex = BOOLEAN
|
|
||||||
showxgrad = BOOLEAN
|
|
||||||
showygrad = BOOLEAN
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CanvasInterface extends SettingsInterface {
|
|
||||||
imageLoaders = OBJECT
|
|
||||||
/** @type {function(string): CanvasRenderingContext2D} */
|
/** @type {function(string): CanvasRenderingContext2D} */
|
||||||
getContext = FUNCTION
|
getContext = FUNCTION
|
||||||
/** @type {function(rect)} */
|
/** @type {function(rect)} */
|
||||||
markDirty = FUNCTION
|
markDirty = FUNCTION
|
||||||
/** @type {function(string)} */
|
/** @type {function(string): Promise} */
|
||||||
loadImage = FUNCTION
|
loadImageAsync = FUNCTION
|
||||||
/** @type {function(string)} */
|
/** @type {function(string)} */
|
||||||
isImageLoading = FUNCTION
|
isImageLoading = FUNCTION
|
||||||
/** @type {function(string)} */
|
/** @type {function(string)} */
|
||||||
|
@ -97,30 +77,28 @@ export class CanvasInterface extends SettingsInterface {
|
||||||
export class RootInterface extends Interface {
|
export class RootInterface extends Interface {
|
||||||
width = NUMBER
|
width = NUMBER
|
||||||
height = NUMBER
|
height = NUMBER
|
||||||
updateObjectsLists = FUNCTION
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DialogInterface extends Interface {
|
export class DialogInterface extends Interface {
|
||||||
show = FUNCTION
|
show = FUNCTION
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HistoryInterface extends Interface {
|
|
||||||
undo = FUNCTION
|
|
||||||
redo = FUNCTION
|
|
||||||
clear = FUNCTION
|
|
||||||
addToHistory = FUNCTION
|
|
||||||
unserialize = FUNCTION
|
|
||||||
serialize = FUNCTION
|
|
||||||
}
|
|
||||||
|
|
||||||
export class LatexInterface extends Interface {
|
export class LatexInterface extends Interface {
|
||||||
|
supportsAsyncRender = BOOLEAN
|
||||||
/**
|
/**
|
||||||
* @param {string} markup - LaTeX markup to render
|
* @param {string} markup - LaTeX markup to render
|
||||||
* @param {number} fontSize - Font size (in pt) to render
|
* @param {number} fontSize - Font size (in pt) to render
|
||||||
* @param {string} color - Color of the text to render
|
* @param {string} color - Color of the text to render
|
||||||
* @returns {string} - Comma separated data of the image (source, width, height)
|
* @returns {string} - Comma separated data of the image (source, width, height)
|
||||||
*/
|
*/
|
||||||
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 {string} markup - LaTeX markup to render
|
||||||
* @param {number} fontSize - Font size (in pt) 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
|
* Gets a setting from the config
|
||||||
* @param {string} settingName - Setting (and its dot-separated namespace) to get (e.g. "default_graph.xmin")
|
* @param {string} settingName - Setting (and its dot-separated namespace) to get (e.g. "default_graph.xmin")
|
||||||
* @returns {boolean} Value of the setting
|
* @returns {string|number|boolean} Value of the setting
|
||||||
*/
|
|
||||||
getSettingBool = FUNCTION
|
|
||||||
/**
|
|
||||||
* Gets a setting from the config
|
|
||||||
* @param {string} settingName - Setting (and its dot-separated namespace) to get (e.g. "default_graph.xmin")
|
|
||||||
* @returns {number} Value of the setting
|
|
||||||
*/
|
|
||||||
getSettingInt = FUNCTION
|
|
||||||
/**
|
|
||||||
* Gets a setting from the config
|
|
||||||
* @param {string} settingName - Setting (and its dot-separated namespace) to get (e.g. "default_graph.xmin")
|
|
||||||
* @returns {string} Value of the setting
|
|
||||||
*/
|
*/
|
||||||
getSetting = FUNCTION
|
getSetting = FUNCTION
|
||||||
/**
|
/**
|
||||||
* Sets a setting in the config
|
* Sets a setting in the config
|
||||||
* @param {string} settingName - Setting (and its dot-separated namespace) to set (e.g. "default_graph.xmin")
|
* @param {string} settingName - Setting (and its dot-separated namespace) to set (e.g. "default_graph.xmin")
|
||||||
* @param {boolean} value
|
* @param {string|number|boolean} value
|
||||||
*/
|
|
||||||
setSettingBool = FUNCTION
|
|
||||||
/**
|
|
||||||
* Sets a setting in the config
|
|
||||||
* @param {string} settingName - Setting (and its dot-separated namespace) to set (e.g. "default_graph.xmin")
|
|
||||||
* @param {number} value
|
|
||||||
*/
|
|
||||||
setSettingInt = FUNCTION
|
|
||||||
/**
|
|
||||||
* Sets a setting in the config
|
|
||||||
* @param {string} settingName - Setting (and its dot-separated namespace) to set (e.g. "default_graph.xmin")
|
|
||||||
* @param {string} value
|
|
||||||
*/
|
*/
|
||||||
setSetting = FUNCTION
|
setSetting = FUNCTION
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -20,35 +20,69 @@ import { Module } from "./common.mjs"
|
||||||
import Objects from "./objects.mjs"
|
import Objects from "./objects.mjs"
|
||||||
import History from "./history.mjs"
|
import History from "./history.mjs"
|
||||||
import Canvas from "./canvas.mjs"
|
import Canvas from "./canvas.mjs"
|
||||||
import { 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 {
|
class IOAPI extends Module {
|
||||||
|
static emits = ["loaded", "saved", "modified"]
|
||||||
|
|
||||||
|
/** @type {RootInterface} */
|
||||||
|
#rootElement
|
||||||
|
/** @type {{show: function(string)}} */
|
||||||
|
#alert
|
||||||
|
#saved = true
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super("IO", {
|
super("IO", {
|
||||||
alert: DialogInterface,
|
alert: DialogInterface,
|
||||||
root: RootInterface,
|
root: RootInterface
|
||||||
settings: SettingsInterface
|
|
||||||
})
|
})
|
||||||
/**
|
|
||||||
* Path of the currently opened file. Empty if no file is opened.
|
// Settings.on("changed", this.__emitModified.bind(this))
|
||||||
* @type {string}
|
History.on("added undone redone", this.__emitModified.bind(this))
|
||||||
*/
|
|
||||||
this.saveFileName = ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
__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.
|
* Initializes module with QML elements.
|
||||||
* @param {RootInterface} root
|
* @param {RootInterface} root
|
||||||
* @param {SettingsInterface} settings
|
|
||||||
* @param {{show: function(string)}} alert
|
* @param {{show: function(string)}} alert
|
||||||
*/
|
*/
|
||||||
initialize({ root, settings, alert }) {
|
initialize({ root, alert }) {
|
||||||
super.initialize({ root, settings, alert })
|
super.initialize({ root, alert })
|
||||||
this.rootElement = root
|
this.#rootElement = root
|
||||||
this.settings = settings
|
this.#alert = alert
|
||||||
this.alert = alert
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -60,7 +94,7 @@ class IOAPI extends Module {
|
||||||
// Add extension if necessary
|
// Add extension if necessary
|
||||||
if(["lpf"].indexOf(filename.split(".")[filename.split(".").length - 1]) === -1)
|
if(["lpf"].indexOf(filename.split(".")[filename.split(".").length - 1]) === -1)
|
||||||
filename += ".lpf"
|
filename += ".lpf"
|
||||||
this.saveFilename = filename
|
Settings.set("saveFilename", filename, false)
|
||||||
let objs = {}
|
let objs = {}
|
||||||
for(let objType in Objects.currentObjects) {
|
for(let objType in Objects.currentObjects) {
|
||||||
objs[objType] = []
|
objs[objType] = []
|
||||||
|
@ -69,28 +103,29 @@ class IOAPI extends Module {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let settings = {
|
let settings = {
|
||||||
"xzoom": this.settings.xzoom,
|
"xzoom": Settings.xzoom,
|
||||||
"yzoom": this.settings.yzoom,
|
"yzoom": Settings.yzoom,
|
||||||
"xmin": this.settings.xmin,
|
"xmin": Settings.xmin,
|
||||||
"ymax": this.settings.ymax,
|
"ymax": Settings.ymax,
|
||||||
"xaxisstep": this.settings.xaxisstep,
|
"xaxisstep": Settings.xaxisstep,
|
||||||
"yaxisstep": this.settings.yaxisstep,
|
"yaxisstep": Settings.yaxisstep,
|
||||||
"xaxislabel": this.settings.xlabel,
|
"xaxislabel": Settings.xlabel,
|
||||||
"yaxislabel": this.settings.ylabel,
|
"yaxislabel": Settings.ylabel,
|
||||||
"logscalex": this.settings.logscalex,
|
"logscalex": Settings.logscalex,
|
||||||
"linewidth": this.settings.linewidth,
|
"linewidth": Settings.linewidth,
|
||||||
"showxgrad": this.settings.showxgrad,
|
"showxgrad": Settings.showxgrad,
|
||||||
"showygrad": this.settings.showygrad,
|
"showygrad": Settings.showygrad,
|
||||||
"textsize": this.settings.textsize,
|
"textsize": Settings.textsize,
|
||||||
"history": History.serialize(),
|
"history": History.serialize(),
|
||||||
"width": this.rootElement.width,
|
"width": this.#rootElement.width,
|
||||||
"height": this.rootElement.height,
|
"height": this.#rootElement.height,
|
||||||
"objects": objs,
|
"objects": objs,
|
||||||
"type": "logplotv1"
|
"type": "logplotv1"
|
||||||
}
|
}
|
||||||
Helper.write(filename, JSON.stringify(settings))
|
Helper.write(filename, JSON.stringify(settings))
|
||||||
this.alert.show(qsTranslate("io", "Saved plot to '%1'.").arg(filename.split("/").pop()))
|
this.#alert.show(qsTranslate("io", "Saved plot to '%1'.").arg(filename.split("/").pop()))
|
||||||
History.history.saved = true
|
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(!this.initialized) throw new Error("Attempting loadDiagram before initialize!")
|
||||||
if(!History.initialized) throw new Error("Attempting loadDiagram before history is initialized!")
|
if(!History.initialized) throw new Error("Attempting loadDiagram before history is initialized!")
|
||||||
let basename = filename.split("/").pop()
|
let basename = filename.split("/").pop()
|
||||||
this.alert.show(qsTranslate("io", "Loading file '%1'.").arg(basename))
|
this.#alert.show(qsTranslate("io", "Loading file '%1'.").arg(basename))
|
||||||
let data = JSON.parse(Helper.load(filename))
|
let data = JSON.parse(Helper.load(filename))
|
||||||
let error = ""
|
let error = ""
|
||||||
if(data.hasOwnProperty("type") && data["type"] === "logplotv1") {
|
if(data.hasOwnProperty("type") && data["type"] === "logplotv1") {
|
||||||
History.clear()
|
History.clear()
|
||||||
// Importing settings
|
// Importing settings
|
||||||
this.settings.saveFilename = filename
|
Settings.set("saveFilename", filename, false)
|
||||||
this.settings.xzoom = parseFloat(data["xzoom"]) || 100
|
Settings.set("xzoom", parseFloat(data["xzoom"]) || 100, false)
|
||||||
this.settings.yzoom = parseFloat(data["yzoom"]) || 10
|
Settings.set("yzoom", parseFloat(data["yzoom"]) || 10, false)
|
||||||
this.settings.xmin = parseFloat(data["xmin"]) || 5 / 10
|
Settings.set("xmin", parseFloat(data["xmin"]) || 5 / 10, false)
|
||||||
this.settings.ymax = parseFloat(data["ymax"]) || 24
|
Settings.set("ymax", parseFloat(data["ymax"]) || 24, false)
|
||||||
this.settings.xaxisstep = data["xaxisstep"] || "4"
|
Settings.set("xaxisstep", data["xaxisstep"] || "4", false)
|
||||||
this.settings.yaxisstep = data["yaxisstep"] || "4"
|
Settings.set("yaxisstep", data["yaxisstep"] || "4", false)
|
||||||
this.settings.xlabel = data["xaxislabel"] || ""
|
Settings.set("xlabel", data["xaxislabel"] || "", false)
|
||||||
this.settings.ylabel = data["yaxislabel"] || ""
|
Settings.set("ylabel", data["yaxislabel"] || "", false)
|
||||||
this.settings.logscalex = data["logscalex"] === true
|
Settings.set("logscalex", data["logscalex"] === true, false)
|
||||||
if("showxgrad" in data)
|
if("showxgrad" in data)
|
||||||
this.settings.showxgrad = data["showxgrad"]
|
Settings.set("showxgrad", data["showxgrad"], false)
|
||||||
if("showygrad" in data)
|
if("showygrad" in data)
|
||||||
this.settings.textsize = data["showygrad"]
|
Settings.set("showygrad", data["showygrad"], false)
|
||||||
if("linewidth" in data)
|
if("linewidth" in data)
|
||||||
this.settings.linewidth = data["linewidth"]
|
Settings.set("linewidth", data["linewidth"], false)
|
||||||
if("textsize" in data)
|
if("textsize" in data)
|
||||||
this.settings.textsize = data["textsize"]
|
Settings.set("textsize", data["textsize"], false)
|
||||||
this.rootElement.height = parseFloat(data["height"]) || 500
|
this.#rootElement.height = parseFloat(data["height"]) || 500
|
||||||
this.rootElement.width = parseFloat(data["width"]) || 1000
|
this.#rootElement.width = parseFloat(data["width"]) || 1000
|
||||||
|
|
||||||
// Importing objects
|
// Importing objects
|
||||||
Objects.currentObjects = {}
|
Objects.currentObjects = {}
|
||||||
|
@ -157,20 +192,18 @@ class IOAPI extends Module {
|
||||||
if("history" in data)
|
if("history" in data)
|
||||||
History.unserialize(...data["history"])
|
History.unserialize(...data["history"])
|
||||||
|
|
||||||
// Refreshing sidebar
|
|
||||||
this.rootElement.updateObjectsLists()
|
|
||||||
} else {
|
} else {
|
||||||
error = qsTranslate("io", "Invalid file provided.")
|
error = qsTranslate("io", "Invalid file provided.")
|
||||||
}
|
}
|
||||||
if(error !== "") {
|
if(error !== "") {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
this.alert.show(qsTranslate("io", "Could not load file: ") + error)
|
this.#alert.show(qsTranslate("io", "Could not load file: ") + error)
|
||||||
// TODO: Error handling
|
// TODO: Error handling
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Canvas.redraw()
|
this.#alert.show(qsTranslate("io", "Loaded file '%1'.").arg(basename))
|
||||||
this.alert.show(qsTranslate("io", "Loaded file '%1'.").arg(basename))
|
this.#saved = true
|
||||||
History.history.saved = true
|
this.emit(new LoadedEvent())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ import * as Instruction from "../lib/expr-eval/instruction.mjs"
|
||||||
import { escapeValue } from "../lib/expr-eval/expression.mjs"
|
import { escapeValue } from "../lib/expr-eval/expression.mjs"
|
||||||
import { HelperInterface, LatexInterface } from "./interface.mjs"
|
import { HelperInterface, LatexInterface } from "./interface.mjs"
|
||||||
|
|
||||||
const unicodechars = [
|
const unicodechars = ["pi", "∞",
|
||||||
"α", "β", "γ", "δ", "ε", "ζ", "η",
|
"α", "β", "γ", "δ", "ε", "ζ", "η",
|
||||||
"π", "θ", "κ", "λ", "μ", "ξ", "ρ",
|
"π", "θ", "κ", "λ", "μ", "ξ", "ρ",
|
||||||
"ς", "σ", "τ", "φ", "χ", "ψ", "ω",
|
"ς", "σ", "τ", "φ", "χ", "ψ", "ω",
|
||||||
|
@ -30,9 +30,9 @@ const unicodechars = [
|
||||||
"ₕ", "ₖ", "ₗ", "ₘ", "ₙ", "ₚ", "ₛ",
|
"ₕ", "ₖ", "ₗ", "ₘ", "ₙ", "ₚ", "ₛ",
|
||||||
"ₜ", "¹", "²", "³", "⁴", "⁵", "⁶",
|
"ₜ", "¹", "²", "³", "⁴", "⁵", "⁶",
|
||||||
"⁷", "⁸", "⁹", "⁰", "₁", "₂", "₃",
|
"⁷", "⁸", "⁹", "⁰", "₁", "₂", "₃",
|
||||||
"₄", "₅", "₆", "₇", "₈", "₉", "₀",
|
"₄", "₅", "₆", "₇", "₈", "₉", "₀"
|
||||||
"pi", "∞"]
|
]
|
||||||
const equivalchars = [
|
const equivalchars = ["\\pi", "\\infty",
|
||||||
"\\alpha", "\\beta", "\\gamma", "\\delta", "\\epsilon", "\\zeta", "\\eta",
|
"\\alpha", "\\beta", "\\gamma", "\\delta", "\\epsilon", "\\zeta", "\\eta",
|
||||||
"\\pi", "\\theta", "\\kappa", "\\lambda", "\\mu", "\\xi", "\\rho",
|
"\\pi", "\\theta", "\\kappa", "\\lambda", "\\mu", "\\xi", "\\rho",
|
||||||
"\\sigma", "\\sigma", "\\tau", "\\phi", "\\chi", "\\psi", "\\omega",
|
"\\sigma", "\\sigma", "\\tau", "\\phi", "\\chi", "\\psi", "\\omega",
|
||||||
|
@ -42,7 +42,7 @@ const equivalchars = [
|
||||||
"{}_{t}", "{}^{1}", "{}^{2}", "{}^{3}", "{}^{4}", "{}^{5}", "{}^{6}",
|
"{}_{t}", "{}^{1}", "{}^{2}", "{}^{3}", "{}^{4}", "{}^{5}", "{}^{6}",
|
||||||
"{}^{7}", "{}^{8}", "{}^{9}", "{}^{0}", "{}_{1}", "{}_{2}", "{}_{3}",
|
"{}^{7}", "{}^{8}", "{}^{9}", "{}^{0}", "{}_{1}", "{}_{2}", "{}_{3}",
|
||||||
"{}_{4}", "{}_{5}", "{}_{6}", "{}_{7}", "{}_{8}", "{}_{9}", "{}_{0}",
|
"{}_{4}", "{}_{5}", "{}_{6}", "{}_{7}", "{}_{8}", "{}_{9}", "{}_{0}",
|
||||||
"\\pi", "\\infty"]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class containing the result of a LaTeX render.
|
* Class containing the result of a LaTeX render.
|
||||||
|
@ -60,6 +60,9 @@ class LatexRenderResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
class LatexAPI extends Module {
|
class LatexAPI extends Module {
|
||||||
|
/** @type {LatexInterface} */
|
||||||
|
#latex = null
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super("Latex", {
|
super("Latex", {
|
||||||
latex: LatexInterface,
|
latex: LatexInterface,
|
||||||
|
@ -77,9 +80,8 @@ class LatexAPI extends Module {
|
||||||
*/
|
*/
|
||||||
initialize({ latex, helper }) {
|
initialize({ latex, helper }) {
|
||||||
super.initialize({ latex, helper })
|
super.initialize({ latex, helper })
|
||||||
this.latex = latex
|
this.#latex = latex
|
||||||
this.helper = helper
|
this.enabled = helper.getSetting("enable_latex")
|
||||||
this.enabled = helper.getSettingBool("enable_latex")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -93,7 +95,7 @@ class LatexAPI extends Module {
|
||||||
*/
|
*/
|
||||||
findPrerendered(markup, fontSize, color) {
|
findPrerendered(markup, fontSize, color) {
|
||||||
if(!this.initialized) throw new Error("Attempting findPrerendered before initialize!")
|
if(!this.initialized) throw new Error("Attempting findPrerendered before initialize!")
|
||||||
const data = this.latex.findPrerendered(markup, fontSize, color)
|
const data = this.#latex.findPrerendered(markup, fontSize, color)
|
||||||
let ret = null
|
let ret = null
|
||||||
if(data !== "")
|
if(data !== "")
|
||||||
ret = new LatexRenderResult(...data.split(","))
|
ret = new LatexRenderResult(...data.split(","))
|
||||||
|
@ -110,7 +112,12 @@ class LatexAPI extends Module {
|
||||||
*/
|
*/
|
||||||
async requestAsyncRender(markup, fontSize, color) {
|
async requestAsyncRender(markup, fontSize, color) {
|
||||||
if(!this.initialized) throw new Error("Attempting requestAsyncRender before initialize!")
|
if(!this.initialized) throw new Error("Attempting requestAsyncRender before initialize!")
|
||||||
let 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)
|
return new LatexRenderResult(...args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,9 +142,10 @@ class LatexAPI extends Module {
|
||||||
*/
|
*/
|
||||||
parif(elem, contents) {
|
parif(elem, contents) {
|
||||||
elem = elem.toString()
|
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)
|
return this.par(elem)
|
||||||
if(elem[0] === "(" && elem[elem.length - 1] === ")")
|
if(!contains && elem[0] === "(" && elem.at(-1) === ")")
|
||||||
return elem.removeEnclosure()
|
return elem.removeEnclosure()
|
||||||
return elem
|
return elem
|
||||||
}
|
}
|
||||||
|
@ -155,20 +163,21 @@ class LatexAPI extends Module {
|
||||||
if(args.length === 3)
|
if(args.length === 3)
|
||||||
return `\\frac{d${args[0].removeEnclosure().replaceAll(args[1].removeEnclosure(), "x")}}{dx}`
|
return `\\frac{d${args[0].removeEnclosure().replaceAll(args[1].removeEnclosure(), "x")}}{dx}`
|
||||||
else
|
else
|
||||||
return `\\frac{d${args[0]}}{dx}(x)`
|
return `\\frac{d${args[0]}}{dx}(${args[1]})`
|
||||||
case "integral":
|
case "integral":
|
||||||
if(args.length === 4)
|
if(args.length === 4)
|
||||||
return `\\int\\limits_{${args[0]}}^{${args[1]}}${args[2].removeEnclosure()} d${args[3].removeEnclosure()}`
|
return `\\int\\limits_{${args[0]}}^{${args[1]}}${args[2].removeEnclosure()} d${args[3].removeEnclosure()}`
|
||||||
else
|
else
|
||||||
return `\\int\\limits_{${args[0]}}^{${args[1]}}${args[2]}(t) dt`
|
return `\\int\\limits_{${args[0]}}^{${args[1]}}${args[2]}(t) dt`
|
||||||
case "sqrt":
|
case "sqrt":
|
||||||
return `\\sqrt\\left(${args.join(", ")}\\right)`
|
const arg = this.parif(args.join(", "), [])
|
||||||
|
return `\\sqrt{${arg}}`
|
||||||
case "abs":
|
case "abs":
|
||||||
return `\\left|${args.join(", ")}\\right|`
|
return `\\left|${args.join(", ")}\\right|`
|
||||||
case "floor":
|
case "floor":
|
||||||
return `\\left\\lfloor${args.join(", ")}\\right\\rfloor`
|
return `\\left\\lfloor{${args.join(", ")}}\\right\\rfloor`
|
||||||
case "ceil":
|
case "ceil":
|
||||||
return `\\left\\lceil${args.join(", ")}\\right\\rceil`
|
return `\\left\\lceil{${args.join(", ")}}\\right\\rceil`
|
||||||
default:
|
default:
|
||||||
return `\\mathrm{${f}}\\left(${args.join(", ")}\\right)`
|
return `\\mathrm{${f}}\\left(${args.join(", ")}\\right)`
|
||||||
}
|
}
|
||||||
|
@ -182,16 +191,17 @@ class LatexAPI extends Module {
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
variable(vari, wrapIn$ = false) {
|
variable(vari, wrapIn$ = false) {
|
||||||
if(wrapIn$)
|
if(wrapIn$) {
|
||||||
for(let i = 0; i < unicodechars.length; i++) {
|
for(let i = 0; i < unicodechars.length; i++) {
|
||||||
if(vari.includes(unicodechars[i]))
|
if(vari.includes(unicodechars[i]))
|
||||||
vari = vari.replaceAll(unicodechars[i], "$" + equivalchars[i] + "$")
|
vari = vari.replaceAll(unicodechars[i], "$" + equivalchars[i] + "$")
|
||||||
}
|
}
|
||||||
else
|
} else {
|
||||||
for(let i = 0; i < unicodechars.length; i++) {
|
for(let i = 0; i < unicodechars.length; i++) {
|
||||||
if(vari.includes(unicodechars[i]))
|
if(vari.includes(unicodechars[i]))
|
||||||
vari = vari.replaceAll(unicodechars[i], equivalchars[i])
|
vari = vari.replaceAll(unicodechars[i], equivalchars[i])
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return vari
|
return vari
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -259,7 +269,7 @@ class LatexAPI extends Module {
|
||||||
throw new EvalError("Unknown operator " + item.value + ".")
|
throw new EvalError("Unknown operator " + item.value + ".")
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case Instruction.IOP3: // Thirdiary operator
|
case Instruction.IOP3: // Ternary operator
|
||||||
n3 = nstack.pop()
|
n3 = nstack.pop()
|
||||||
n2 = nstack.pop()
|
n2 = nstack.pop()
|
||||||
n1 = nstack.pop()
|
n1 = nstack.pop()
|
||||||
|
@ -286,7 +296,7 @@ class LatexAPI extends Module {
|
||||||
nstack.push(this.parif(n1, ["+", "-", "*", "/", "^"]) + "!")
|
nstack.push(this.parif(n1, ["+", "-", "*", "/", "^"]) + "!")
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
nstack.push(f + this.parif(n1, ["+", "-", "*", "/", "^"]))
|
nstack.push(this.functionToLatex(f, [this.parif(n1, ["+", "-", "*", "/", "^"])]))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
@ -321,9 +331,6 @@ class LatexAPI extends Module {
|
||||||
throw new EvalError("invalid Expression")
|
throw new EvalError("invalid Expression")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(nstack.length > 1) {
|
|
||||||
nstack = [nstack.join(";")]
|
|
||||||
}
|
|
||||||
return String(nstack[0])
|
return String(nstack[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,10 @@ class ObjectsAPI extends Module {
|
||||||
constructor() {
|
constructor() {
|
||||||
super("Objects")
|
super("Objects")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of object constructors.
|
||||||
|
* @type {Object.<string,typeof DrawableObject>}
|
||||||
|
*/
|
||||||
this.types = {}
|
this.types = {}
|
||||||
/**
|
/**
|
||||||
* List of objects for each type of object.
|
* List of objects for each type of object.
|
||||||
|
@ -65,7 +69,7 @@ class ObjectsAPI extends Module {
|
||||||
* @param {string} newName - Name to rename the object to.
|
* @param {string} newName - Name to rename the object to.
|
||||||
*/
|
*/
|
||||||
renameObject(oldName, newName) {
|
renameObject(oldName, newName) {
|
||||||
let obj = this.currentObjectsByName[oldName]
|
const obj = this.currentObjectsByName[oldName]
|
||||||
delete this.currentObjectsByName[oldName]
|
delete this.currentObjectsByName[oldName]
|
||||||
this.currentObjectsByName[newName] = obj
|
this.currentObjectsByName[newName] = obj
|
||||||
obj.name = newName
|
obj.name = newName
|
||||||
|
@ -76,7 +80,7 @@ class ObjectsAPI extends Module {
|
||||||
* @param {string} objName - Current name of the object.
|
* @param {string} objName - Current name of the object.
|
||||||
*/
|
*/
|
||||||
deleteObject(objName) {
|
deleteObject(objName) {
|
||||||
let obj = this.currentObjectsByName[objName]
|
const obj = this.currentObjectsByName[objName]
|
||||||
if(obj !== undefined) {
|
if(obj !== undefined) {
|
||||||
this.currentObjects[obj.type].splice(this.currentObjects[obj.type].indexOf(obj), 1)
|
this.currentObjects[obj.type].splice(this.currentObjects[obj.type].indexOf(obj), 1)
|
||||||
obj.delete()
|
obj.delete()
|
||||||
|
|
|
@ -20,6 +20,9 @@ import General from "../preferences/general.mjs"
|
||||||
import Editor from "../preferences/expression.mjs"
|
import Editor from "../preferences/expression.mjs"
|
||||||
import DefaultGraph from "../preferences/default.mjs"
|
import DefaultGraph from "../preferences/default.mjs"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module for application wide settings.
|
||||||
|
*/
|
||||||
class PreferencesAPI extends Module {
|
class PreferencesAPI extends Module {
|
||||||
constructor() {
|
constructor() {
|
||||||
super("Preferences")
|
super("Preferences")
|
||||||
|
|
186
common/src/module/settings.mjs
Normal file
186
common/src/module/settings.mjs
Normal 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
|
||||||
|
|
|
@ -63,7 +63,7 @@ export default class BodePhase extends ExecutableObject {
|
||||||
// Create new point
|
// Create new point
|
||||||
om_0 = Objects.createNewRegisteredObject("Point", [Objects.getNewName("ω"), this.color, "name"])
|
om_0 = Objects.createNewRegisteredObject("Point", [Objects.getNewName("ω"), this.color, "name"])
|
||||||
om_0.labelPosition = this.phase.execute() >= 0 ? "above" : "below"
|
om_0.labelPosition = this.phase.execute() >= 0 ? "above" : "below"
|
||||||
History.history.addToHistory(new CreateNewObject(om_0.name, "Point", om_0.export()))
|
History.addToHistory(new CreateNewObject(om_0.name, "Point", om_0.export()))
|
||||||
labelPosition = "below"
|
labelPosition = "below"
|
||||||
}
|
}
|
||||||
om_0.requiredBy.push(this)
|
om_0.requiredBy.push(this)
|
||||||
|
|
|
@ -53,11 +53,11 @@ export class BoolSetting extends Setting {
|
||||||
}
|
}
|
||||||
|
|
||||||
value() {
|
value() {
|
||||||
return Helper.getSettingBool(this.nameInConfig)
|
return Helper.getSetting(this.nameInConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
set(value) {
|
set(value) {
|
||||||
Helper.setSettingBool(this.nameInConfig, value)
|
Helper.setSetting(this.nameInConfig, value === true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,11 +69,11 @@ export class NumberSetting extends Setting {
|
||||||
}
|
}
|
||||||
|
|
||||||
value() {
|
value() {
|
||||||
return Helper.getSettingInt(this.nameInConfig)
|
return Helper.getSetting(this.nameInConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
set(value) {
|
set(value) {
|
||||||
Helper.setSettingInt(this.nameInConfig, value)
|
Helper.setSetting(this.nameInConfig, +value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,11 +84,11 @@ export class EnumIntSetting extends Setting {
|
||||||
}
|
}
|
||||||
|
|
||||||
value() {
|
value() {
|
||||||
return Helper.getSettingInt(this.nameInConfig)
|
return Helper.getSetting(this.nameInConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
set(value) {
|
set(value) {
|
||||||
Helper.setSettingInt(this.nameInConfig, value)
|
Helper.setSetting(this.nameInConfig, +value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,6 +131,6 @@ export class StringSetting extends Setting {
|
||||||
}
|
}
|
||||||
|
|
||||||
set(value) {
|
set(value) {
|
||||||
Helper.setSetting(this.nameInConfig, value)
|
Helper.setSetting(this.nameInConfig, ""+value)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -28,7 +28,7 @@ const XZOOM = new NumberSetting(
|
||||||
|
|
||||||
const YZOOM = new NumberSetting(
|
const YZOOM = new NumberSetting(
|
||||||
qsTranslate("Settings", "Y Zoom"),
|
qsTranslate("Settings", "Y Zoom"),
|
||||||
"default_graph.xzoom",
|
"default_graph.yzoom",
|
||||||
"yzoom",
|
"yzoom",
|
||||||
0.1
|
0.1
|
||||||
)
|
)
|
||||||
|
@ -37,7 +37,7 @@ const XMIN = new NumberSetting(
|
||||||
qsTranslate("Settings", "Min X"),
|
qsTranslate("Settings", "Min X"),
|
||||||
"default_graph.xmin",
|
"default_graph.xmin",
|
||||||
"xmin",
|
"xmin",
|
||||||
() => Helper.getSettingBool("default_graph.logscalex") ? 1e-100 : -Infinity
|
() => Helper.getSetting("default_graph.logscalex") ? 1e-100 : -Infinity
|
||||||
)
|
)
|
||||||
|
|
||||||
const YMAX = new NumberSetting(
|
const YMAX = new NumberSetting(
|
||||||
|
|
|
@ -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 [
|
export default [
|
||||||
CHECK_FOR_UPDATES,
|
CHECK_FOR_UPDATES,
|
||||||
RESET_REDO_STACK,
|
RESET_REDO_STACK,
|
||||||
new EnableLatex()
|
new EnableLatex(),
|
||||||
|
ENABLE_LATEX_ASYNC
|
||||||
]
|
]
|
||||||
|
|
|
@ -21,135 +21,150 @@
|
||||||
* Replaces latin characters with their uppercase versions.
|
* Replaces latin characters with their uppercase versions.
|
||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
String.prototype.toLatinUppercase = String.prototype.toLatinUppercase || function() {
|
String.prototype.toLatinUppercase = function() {
|
||||||
return this.replace(/[a-z]/g, function(match) {
|
return this.replace(/[a-z]/g, function(match) {
|
||||||
return match.toUpperCase()
|
return match.toUpperCase()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the '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}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
String.prototype.removeEnclosure = function() {
|
String.prototype.removeEnclosure = function() {
|
||||||
return this.substring(1, this.length - 1)
|
return this.substring(1, this.length - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const powerpos = {
|
/**
|
||||||
"-": "⁻",
|
* Rounds to a certain number of decimal places.
|
||||||
"+": "⁺",
|
* From https://stackoverflow.com/a/48764436
|
||||||
"=": "⁼",
|
*
|
||||||
" ": " ",
|
* @param {number} decimalPlaces
|
||||||
"(": "⁽",
|
* @return {number}
|
||||||
")": "⁾",
|
*/
|
||||||
"0": "⁰",
|
Number.prototype.toDecimalPrecision = function(decimalPlaces = 0) {
|
||||||
"1": "¹",
|
const p = Math.pow(10, decimalPlaces)
|
||||||
"2": "²",
|
const n = (this * p) * (1 + Number.EPSILON)
|
||||||
"3": "³",
|
return Math.round(n) / p
|
||||||
"4": "⁴",
|
|
||||||
"5": "⁵",
|
|
||||||
"6": "⁶",
|
|
||||||
"7": "⁷",
|
|
||||||
"8": "⁸",
|
|
||||||
"9": "⁹",
|
|
||||||
"a": "ᵃ",
|
|
||||||
"b": "ᵇ",
|
|
||||||
"c": "ᶜ",
|
|
||||||
"d": "ᵈ",
|
|
||||||
"e": "ᵉ",
|
|
||||||
"f": "ᶠ",
|
|
||||||
"g": "ᵍ",
|
|
||||||
"h": "ʰ",
|
|
||||||
"i": "ⁱ",
|
|
||||||
"j": "ʲ",
|
|
||||||
"k": "ᵏ",
|
|
||||||
"l": "ˡ",
|
|
||||||
"m": "ᵐ",
|
|
||||||
"n": "ⁿ",
|
|
||||||
"o": "ᵒ",
|
|
||||||
"p": "ᵖ",
|
|
||||||
"r": "ʳ",
|
|
||||||
"s": "ˢ",
|
|
||||||
"t": "ᵗ",
|
|
||||||
"u": "ᵘ",
|
|
||||||
"v": "ᵛ",
|
|
||||||
"w": "ʷ",
|
|
||||||
"x": "ˣ",
|
|
||||||
"y": "ʸ",
|
|
||||||
"z": "ᶻ"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const exponents = [
|
const 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 = {
|
/**
|
||||||
"-": "₋",
|
* Put a text in sup position
|
||||||
"+": "₊",
|
* @param {string} text
|
||||||
"=": "₌",
|
* @return {string}
|
||||||
"(": "₍",
|
*/
|
||||||
")": "₎",
|
|
||||||
" ": " ",
|
|
||||||
"0": "₀",
|
|
||||||
"1": "₁",
|
|
||||||
"2": "₂",
|
|
||||||
"3": "₃",
|
|
||||||
"4": "₄",
|
|
||||||
"5": "₅",
|
|
||||||
"6": "₆",
|
|
||||||
"7": "₇",
|
|
||||||
"8": "₈",
|
|
||||||
"9": "₉",
|
|
||||||
"a": "ₐ",
|
|
||||||
"e": "ₑ",
|
|
||||||
"h": "ₕ",
|
|
||||||
"i": "ᵢ",
|
|
||||||
"j": "ⱼ",
|
|
||||||
"k": "ₖ",
|
|
||||||
"l": "ₗ",
|
|
||||||
"m": "ₘ",
|
|
||||||
"n": "ₙ",
|
|
||||||
"o": "ₒ",
|
|
||||||
"p": "ₚ",
|
|
||||||
"r": "ᵣ",
|
|
||||||
"s": "ₛ",
|
|
||||||
"t": "ₜ",
|
|
||||||
"u": "ᵤ",
|
|
||||||
"v": "ᵥ",
|
|
||||||
"x": "ₓ",
|
|
||||||
}
|
|
||||||
// Put a text in sup position
|
|
||||||
export function textsup(text) {
|
export function textsup(text) {
|
||||||
let ret = ""
|
let ret = ""
|
||||||
text = text.toString()
|
text = text.toString()
|
||||||
for (let i = 0; i < text.length; i++) {
|
for(let letter of text)
|
||||||
if(Object.keys(powerpos).indexOf(text[i]) >= 0) {
|
ret += CHARACTER_TO_POWER.has(letter) ? CHARACTER_TO_POWER.get(letter) : letter
|
||||||
ret += powerpos[text[i]]
|
|
||||||
} else {
|
|
||||||
ret += text[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put a text in sub position
|
/**
|
||||||
|
* Put a text in sub position
|
||||||
|
* @param {string} text
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
export function textsub(text) {
|
export function textsub(text) {
|
||||||
let ret = ""
|
let ret = ""
|
||||||
text = text.toString()
|
text = text.toString()
|
||||||
for (let i = 0; i < text.length; i++) {
|
for(let letter of text)
|
||||||
if(Object.keys(indicepos).indexOf(text[i]) >= 0) {
|
ret += CHARACTER_TO_INDICE.has(letter) ? CHARACTER_TO_INDICE.get(letter) : letter
|
||||||
ret += indicepos[text[i]]
|
|
||||||
} else {
|
|
||||||
ret += text[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simplifies (mathematically) a mathematical expression.
|
* Simplifies (mathematically) a mathematical expression.
|
||||||
|
* @deprecated
|
||||||
* @param {string} str - Expression to parse
|
* @param {string} str - Expression to parse
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
|
@ -172,37 +187,43 @@ export function simplifyExpression(str) {
|
||||||
// n1 & n3 are multiplied, opeM is the main operation (- or +).
|
// n1 & n3 are multiplied, opeM is the main operation (- or +).
|
||||||
// Putting all n in form of number
|
// Putting all n in form of number
|
||||||
//n2 = n2 == undefined ? 1 : parseFloat(n)
|
//n2 = n2 == undefined ? 1 : parseFloat(n)
|
||||||
n1 = m1 === undefined ? 1 : eval(m1 + '1')
|
n1 = m1 === undefined ? 1 : eval(m1 + "1")
|
||||||
n2 = m2 === undefined ? 1 : eval('1' + m2)
|
n2 = m2 === undefined ? 1 : eval("1" + m2)
|
||||||
n3 = m3 === undefined ? 1 : eval(m3 + '1')
|
n3 = m3 === undefined ? 1 : eval(m3 + "1")
|
||||||
n4 = m4 === undefined ? 1 : eval('1' + m4)
|
n4 = m4 === undefined ? 1 : eval("1" + m4)
|
||||||
//let [n1, n2, n3, n4] = [n1, n2, n3, n4].map(n => n == undefined ? 1 : parseFloat(n))
|
//let [n1, n2, n3, n4] = [n1, n2, n3, n4].map(n => n == undefined ? 1 : parseFloat(n))
|
||||||
// Falling back to * in case it does not exist (the corresponding n would be 1)
|
// Falling back to * in case it does not exist (the corresponding n would be 1)
|
||||||
[ope2, ope4] = [ope2, ope4].map(ope => ope === '/' ? '/' : '*')
|
[ope2, ope4] = [ope2, ope4].map(ope => ope === "/" ? "/" : "*")
|
||||||
let coeff1 = n1 * n2
|
let coeff1 = n1 * n2
|
||||||
let coeff2 = n3 * n4
|
let coeff2 = n3 * n4
|
||||||
let coefficient = coeff1+coeff2-(opeM === '-' ? 2*coeff2 : 0)
|
let coefficient = coeff1 + coeff2 - (opeM === "-" ? 2 * coeff2 : 0)
|
||||||
|
|
||||||
return `${coefficient} * π`
|
return `${coefficient} * π`
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[ // Removing parenthesis when content is only added from both sides.
|
[ // Removing parenthesis when content is only added from both sides.
|
||||||
/(^|[+-] |\()\(([^)(]+)\)($| [+-]|\))/g,
|
/(^|[+-] |\()\(([^)(]+)\)($| [+-]|\))/g,
|
||||||
function(match, b4, middle, after) {return `${b4}${middle}${after}`}
|
function(match, b4, middle, after) {
|
||||||
|
return `${b4}${middle}${after}`
|
||||||
|
}
|
||||||
],
|
],
|
||||||
[ // Removing parenthesis when content is only multiplied.
|
[ // Removing parenthesis when content is only multiplied.
|
||||||
/(^|[*\/] |\()\(([^)(+-]+)\)($| [*\/+-]|\))/g,
|
/(^|[*\/] |\()\(([^)(+-]+)\)($| [*\/+-]|\))/g,
|
||||||
function(match, b4, middle, after) {return `${b4}${middle}${after}`}
|
function(match, b4, middle, after) {
|
||||||
|
return `${b4}${middle}${after}`
|
||||||
|
}
|
||||||
],
|
],
|
||||||
[ // Removing parenthesis when content is only multiplied.
|
[ // Removing parenthesis when content is only multiplied.
|
||||||
/(^|[*\/+-] |\()\(([^)(+-]+)\)($| [*\/]|\))/g,
|
/(^|[*\/+-] |\()\(([^)(+-]+)\)($| [*\/]|\))/g,
|
||||||
function(match, b4, middle, after) {return `${b4}${middle}${after}`}
|
function(match, b4, middle, after) {
|
||||||
|
return `${b4}${middle}${after}`
|
||||||
|
}
|
||||||
],
|
],
|
||||||
[// Simplification additions/subtractions.
|
[// Simplification additions/subtractions.
|
||||||
/(^|[^*\/] |\()([-.\d]+) [+-] (\([^)(]+\)|[^)(]+) [+-] ([-.\d]+)($| [^*\/]|\))/g,
|
/(^|[^*\/] |\()([-.\d]+) [+-] (\([^)(]+\)|[^)(]+) [+-] ([-.\d]+)($| [^*\/]|\))/g,
|
||||||
function(match, b4, n1, op1, middle, op2, n2, after) {
|
function(match, b4, n1, op1, middle, op2, n2, after) {
|
||||||
let total
|
let total
|
||||||
if(op2 === '+') {
|
if(op2 === "+") {
|
||||||
total = parseFloat(n1) + parseFloat(n2)
|
total = parseFloat(n1) + parseFloat(n2)
|
||||||
} else {
|
} else {
|
||||||
total = parseFloat(n1) - parseFloat(n2)
|
total = parseFloat(n1) - parseFloat(n2)
|
||||||
|
@ -213,12 +234,12 @@ export function simplifyExpression(str) {
|
||||||
[// Simplification multiplications/divisions.
|
[// Simplification multiplications/divisions.
|
||||||
/([-.\d]+) [*\/] (\([^)(]+\)|[^)(+-]+) [*\/] ([-.\d]+)/g,
|
/([-.\d]+) [*\/] (\([^)(]+\)|[^)(+-]+) [*\/] ([-.\d]+)/g,
|
||||||
function(match, n1, op1, middle, op2, n2) {
|
function(match, n1, op1, middle, op2, n2) {
|
||||||
if(parseInt(n1) === n1 && parseInt(n2) === n2 && op2 === '/' &&
|
if(parseInt(n1) === n1 && parseInt(n2) === n2 && op2 === "/" &&
|
||||||
(parseInt(n1) / parseInt(n2)) % 1 !== 0) {
|
(parseInt(n1) / parseInt(n2)) % 1 !== 0) {
|
||||||
// Non int result for int division.
|
// Non int result for int division.
|
||||||
return `(${n1} / ${n2}) ${op1} ${middle}`
|
return `(${n1} / ${n2}) ${op1} ${middle}`
|
||||||
} else {
|
} else {
|
||||||
if(op2 === '*') {
|
if(op2 === "*") {
|
||||||
return `${parseFloat(n1) * parseFloat(n2)} ${op1} ${middle}`
|
return `${parseFloat(n1) * parseFloat(n2)} ${op1} ${middle}`
|
||||||
} else {
|
} else {
|
||||||
return `${parseFloat(n1) / parseFloat(n2)} ${op1} ${middle}`
|
return `${parseFloat(n1) / parseFloat(n2)} ${op1} ${middle}`
|
||||||
|
@ -232,17 +253,17 @@ export function simplifyExpression(str) {
|
||||||
let str = middle
|
let str = middle
|
||||||
// Replace all groups
|
// Replace all groups
|
||||||
while(/\([^)(]+\)/g.test(str))
|
while(/\([^)(]+\)/g.test(str))
|
||||||
str = str.replace(/\([^)(]+\)/g, '')
|
str = str.replace(/\([^)(]+\)/g, "")
|
||||||
// There shouldn't be any more parenthesis
|
// There shouldn't be any more parenthesis
|
||||||
// If there is, that means the 2 parenthesis are needed.
|
// If there is, that means the 2 parenthesis are needed.
|
||||||
if(!str.includes(')') && !str.includes('(')) {
|
if(!str.includes(")") && !str.includes("(")) {
|
||||||
return middle
|
return middle
|
||||||
} else {
|
} else {
|
||||||
return `(${middle})`
|
return `(${middle})`
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
// Simple simplifications
|
// Simple simplifications
|
||||||
// [/(\s|^|\()0(\.0+)? \* (\([^)(]+\))/g, '$10'],
|
// [/(\s|^|\()0(\.0+)? \* (\([^)(]+\))/g, '$10'],
|
||||||
// [/(\s|^|\()0(\.0+)? \* ([^)(+-]+)/g, '$10'],
|
// [/(\s|^|\()0(\.0+)? \* ([^)(+-]+)/g, '$10'],
|
||||||
|
@ -273,24 +294,35 @@ export function simplifyExpression(str) {
|
||||||
/**
|
/**
|
||||||
* Transforms a mathematical expression to make it readable by humans.
|
* Transforms a mathematical expression to make it readable by humans.
|
||||||
* NOTE: Will break parsing of expression.
|
* NOTE: Will break parsing of expression.
|
||||||
|
* @deprecated
|
||||||
* @param {string} str - Expression to parse.
|
* @param {string} str - Expression to parse.
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export function makeExpressionReadable(str) {
|
export function makeExpressionReadable(str) {
|
||||||
let replacements = [
|
let replacements = [
|
||||||
// letiables
|
// letiables
|
||||||
[/pi/g, 'π'],
|
[/pi/g, "π"],
|
||||||
[/Infinity/g, '∞'],
|
[/Infinity/g, "∞"],
|
||||||
[/inf/g, '∞'],
|
[/inf/g, "∞"],
|
||||||
// Other
|
// Other
|
||||||
[/ \* /g, '×'],
|
[/ \* /g, "×"],
|
||||||
[/ \^ /g, '^'],
|
[/ \^ /g, "^"],
|
||||||
[/\^\(([\d\w+-]+)\)/g, function(match, p1) { return textsup(p1) }],
|
[/\^\(([\d\w+-]+)\)/g, function(match, p1) {
|
||||||
[/\^([\d\w+-]+)/g, function(match, p1) { return textsup(p1) }],
|
return textsup(p1)
|
||||||
[/_\(([\d\w+-]+)\)/g, function(match, p1) { return textsub(p1) }],
|
}],
|
||||||
[/_([\d\w+-]+)/g, function(match, p1) { return textsub(p1) }],
|
[/\^([\d\w+-]+)/g, function(match, p1) {
|
||||||
[/\[([^\[\]]+)\]/g, function(match, p1) { return textsub(p1) }],
|
return textsup(p1)
|
||||||
[/(\d|\))×/g, '$1'],
|
}],
|
||||||
|
[/_\(([\d\w+-]+)\)/g, function(match, p1) {
|
||||||
|
return textsub(p1)
|
||||||
|
}],
|
||||||
|
[/_([\d\w+-]+)/g, function(match, p1) {
|
||||||
|
return textsub(p1)
|
||||||
|
}],
|
||||||
|
[/\[([^\[\]]+)\]/g, function(match, p1) {
|
||||||
|
return textsub(p1)
|
||||||
|
}],
|
||||||
|
[/(\d|\))×/g, "$1"],
|
||||||
[/integral\((.+),\s?(.+),\s?["'](.+)["'],\s?["'](.+)["']\)/g, function(match, a, b, p1, body, p2, p3, by, p4) {
|
[/integral\((.+),\s?(.+),\s?["'](.+)["'],\s?["'](.+)["']\)/g, function(match, a, b, p1, body, p2, p3, by, p4) {
|
||||||
if(a.length < b.length) {
|
if(a.length < b.length) {
|
||||||
return `∫${textsub(a)}${textsup(b)} ${body} d${by}`
|
return `∫${textsub(a)}${textsup(b)} ${body} d${by}`
|
||||||
|
@ -299,7 +331,7 @@ export function makeExpressionReadable(str) {
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
[/derivative\(?["'](.+)["'], ?["'](.+)["'], ?(.+)\)?/g, function(match, p1, body, p2, p3, by, p4, x) {
|
[/derivative\(?["'](.+)["'], ?["'](.+)["'], ?(.+)\)?/g, function(match, p1, body, p2, p3, by, p4, x) {
|
||||||
return `d(${body.replace(new RegExp(by, 'g'), 'x')})/dx`
|
return `d(${body.replace(new RegExp(by, "g"), "x")})/dx`
|
||||||
}]
|
}]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -311,6 +343,48 @@ export function makeExpressionReadable(str) {
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @type {[RegExp, string][]} */
|
||||||
|
const replacements = [
|
||||||
|
// Greek letters
|
||||||
|
[/(\W|^)al(pha)?(\W|$)/g, "$1α$3"],
|
||||||
|
[/(\W|^)be(ta)?(\W|$)/g, "$1β$3"],
|
||||||
|
[/(\W|^)ga(mma)?(\W|$)/g, "$1γ$3"],
|
||||||
|
[/(\W|^)de(lta)?(\W|$)/g, "$1δ$3"],
|
||||||
|
[/(\W|^)ep(silon)?(\W|$)/g, "$1ε$3"],
|
||||||
|
[/(\W|^)ze(ta)?(\W|$)/g, "$1ζ$3"],
|
||||||
|
[/(\W|^)et(a)?(\W|$)/g, "$1η$3"],
|
||||||
|
[/(\W|^)th(eta)?(\W|$)/g, "$1θ$3"],
|
||||||
|
[/(\W|^)io(ta)?(\W|$)/g, "$1ι$3"],
|
||||||
|
[/(\W|^)ka(ppa)?(\W|$)/g, "$1κ$3"],
|
||||||
|
[/(\W|^)la(mbda)?(\W|$)/g, "$1λ$3"],
|
||||||
|
[/(\W|^)mu(\W|$)/g, "$1μ$2"],
|
||||||
|
[/(\W|^)nu(\W|$)/g, "$1ν$2"],
|
||||||
|
[/(\W|^)xi(\W|$)/g, "$1ξ$2"],
|
||||||
|
[/(\W|^)rh(o)?(\W|$)/g, "$1ρ$3"],
|
||||||
|
[/(\W|^)si(gma)?(\W|$)/g, "$1σ$3"],
|
||||||
|
[/(\W|^)ta(u)?(\W|$)/g, "$1τ$3"],
|
||||||
|
[/(\W|^)up(silon)?(\W|$)/g, "$1υ$3"],
|
||||||
|
[/(\W|^)ph(i)?(\W|$)/g, "$1φ$3"],
|
||||||
|
[/(\W|^)ch(i)?(\W|$)/g, "$1χ$3"],
|
||||||
|
[/(\W|^)ps(i)?(\W|$)/g, "$1ψ$3"],
|
||||||
|
[/(\W|^)om(ega)?(\W|$)/g, "$1ω$3"],
|
||||||
|
// Capital greek letters
|
||||||
|
[/(\W|^)gga(mma)?(\W|$)/g, "$1Γ$3"],
|
||||||
|
[/(\W|^)gde(lta)?(\W|$)/g, "$1Δ$3"],
|
||||||
|
[/(\W|^)gth(eta)?(\W|$)/g, "$1Θ$3"],
|
||||||
|
[/(\W|^)gla(mbda)?(\W|$)/g, "$1Λ$3"],
|
||||||
|
[/(\W|^)gxi(\W|$)/g, "$1Ξ$2"],
|
||||||
|
[/(\W|^)gpi(\W|$)/g, "$1Π$2"],
|
||||||
|
[/(\W|^)gsi(gma)?(\W|$)/g, "$1Σ$3"],
|
||||||
|
[/(\W|^)gph(i)?(\W|$)/g, "$1Φ$3"],
|
||||||
|
[/(\W|^)gps(i)?(\W|$)/g, "$1Ψ$3"],
|
||||||
|
[/(\W|^)gom(ega)?(\W|$)/g, "$1Ω$3"],
|
||||||
|
// Array elements
|
||||||
|
[/\[([^\]\[]+)\]/g, function(match, p1) {
|
||||||
|
return textsub(p1)
|
||||||
|
}]
|
||||||
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a variable name to make it readable by humans.
|
* Parses a variable name to make it readable by humans.
|
||||||
*
|
*
|
||||||
|
@ -319,59 +393,18 @@ export function makeExpressionReadable(str) {
|
||||||
* @returns {string} - The parsed name
|
* @returns {string} - The parsed name
|
||||||
*/
|
*/
|
||||||
export function parseName(str, removeUnallowed = true) {
|
export function parseName(str, removeUnallowed = true) {
|
||||||
let replacements = [
|
for(const replacement of replacements)
|
||||||
// Greek letters
|
|
||||||
[/([^a-z]|^)al(pha)?([^a-z]|$)/g, '$1α$3'],
|
|
||||||
[/([^a-z]|^)be(ta)?([^a-z]|$)/g, '$1β$3'],
|
|
||||||
[/([^a-z]|^)ga(mma)?([^a-z]|$)/g, '$1γ$3'],
|
|
||||||
[/([^a-z]|^)de(lta)?([^a-z]|$)/g, '$1δ$3'],
|
|
||||||
[/([^a-z]|^)ep(silon)?([^a-z]|$)/g, '$1ε$3'],
|
|
||||||
[/([^a-z]|^)ze(ta)?([^a-z]|$)/g, '$1ζ$3'],
|
|
||||||
[/([^a-z]|^)et(a)?([^a-z]|$)/g, '$1η$3'],
|
|
||||||
[/([^a-z]|^)th(eta)?([^a-z]|$)/g, '$1θ$3'],
|
|
||||||
[/([^a-z]|^)io(ta)?([^a-z]|$)/g, '$1ι$3'],
|
|
||||||
[/([^a-z]|^)ka(ppa)([^a-z]|$)?/g, '$1κ$3'],
|
|
||||||
[/([^a-z]|^)la(mbda)?([^a-z]|$)/g, '$1λ$3'],
|
|
||||||
[/([^a-z]|^)mu([^a-z]|$)/g, '$1μ$2'],
|
|
||||||
[/([^a-z]|^)nu([^a-z]|$)/g, '$1ν$2'],
|
|
||||||
[/([^a-z]|^)xi([^a-z]|$)/g, '$1ξ$2'],
|
|
||||||
[/([^a-z]|^)rh(o)?([^a-z]|$)/g, '$1ρ$3'],
|
|
||||||
[/([^a-z]|^)si(gma)?([^a-z]|$)/g, '$1σ$3'],
|
|
||||||
[/([^a-z]|^)ta(u)?([^a-z]|$)/g, '$1τ$3'],
|
|
||||||
[/([^a-z]|^)up(silon)?([^a-z]|$)/g, '$1υ$3'],
|
|
||||||
[/([^a-z]|^)ph(i)?([^a-z]|$)/g, '$1φ$3'],
|
|
||||||
[/([^a-z]|^)ch(i)?([^a-z]|$)/g, '$1χ$3'],
|
|
||||||
[/([^a-z]|^)ps(i)?([^a-z]|$)/g, '$1ψ$3'],
|
|
||||||
[/([^a-z]|^)om(ega)?([^a-z]|$)/g, '$1ω$3'],
|
|
||||||
// Capital greek letters
|
|
||||||
[/([^a-z]|^)gga(mma)?([^a-z]|$)/g, '$1Γ$3'],
|
|
||||||
[/([^a-z]|^)gde(lta)?([^a-z]|$)/g, '$1Δ$3'],
|
|
||||||
[/([^a-z]|^)gth(eta)?([^a-z]|$)/g, '$1Θ$3'],
|
|
||||||
[/([^a-z]|^)gla(mbda)?([^a-z]|$)/g, '$1Λ$3'],
|
|
||||||
[/([^a-z]|^)gxi([^a-z]|$)/g, '$1Ξ$2'],
|
|
||||||
[/([^a-z]|^)gpi([^a-z]|$)/g, '$1Π$2'],
|
|
||||||
[/([^a-z]|^)gsi(gma)([^a-z]|$)?/g, '$1Σ$3'],
|
|
||||||
[/([^a-z]|^)gph(i)?([^a-z]|$)/g, '$1Φ$3'],
|
|
||||||
[/([^a-z]|^)gps(i)?([^a-z]|$)/g, '$1Ψ$3'],
|
|
||||||
[/([^a-z]|^)gom(ega)?([^a-z]|$)/g, '$1Ω$3'],
|
|
||||||
// Underscores
|
|
||||||
// [/_\(([^_]+)\)/g, function(match, p1) { return textsub(p1) }],
|
|
||||||
// [/_([^" ]+)/g, function(match, p1) { return textsub(p1) }],
|
|
||||||
// Array elements
|
|
||||||
[/\[([^\]\[]+)\]/g, function(match, p1) { return textsub(p1) }],
|
|
||||||
// Removing
|
|
||||||
[/[xπℝℕ\\∪∩\]\[ ()^/÷*×+=\d-]/g , ''],
|
|
||||||
]
|
|
||||||
if(!removeUnallowed) replacements.pop()
|
|
||||||
// Replacements
|
|
||||||
for(let replacement of replacements)
|
|
||||||
str = str.replace(replacement[0], replacement[1])
|
str = str.replace(replacement[0], replacement[1])
|
||||||
|
if(removeUnallowed)
|
||||||
|
str = str.replace(/[xnπℝℕ\\∪∩\]\[ ()^/÷*×+=\d¹²³⁴⁵⁶⁷⁸⁹⁰-]/g, "")
|
||||||
|
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transforms camel case strings to a space separated one.
|
* Transforms camel case strings to a space separated one.
|
||||||
*
|
*
|
||||||
|
* @deprecated
|
||||||
* @param {string} label - Camel case to parse
|
* @param {string} label - Camel case to parse
|
||||||
* @returns {string} Parsed label.
|
* @returns {string} Parsed label.
|
||||||
*/
|
*/
|
||||||
|
@ -385,12 +418,12 @@ export function camelCase2readable(label) {
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export function getRandomColor() {
|
export function getRandomColor() {
|
||||||
let clrs = '0123456789ABCDEF';
|
let clrs = "0123456789ABCDEF"
|
||||||
let color = '#';
|
let color = "#"
|
||||||
for(let i = 0; i < 6; i++) {
|
for(let i = 0; i < 6; i++) {
|
||||||
color += clrs[Math.floor(Math.random() * (16-5*(i%2===0)))];
|
color += clrs[Math.floor(Math.random() * (16 - 5 * (i % 2 === 0)))]
|
||||||
}
|
}
|
||||||
return color;
|
return color
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -399,16 +432,15 @@ export function getRandomColor() {
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export function escapeHTML(str) {
|
export function escapeHTML(str) {
|
||||||
return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') ;
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses exponents and replaces them with expression values
|
* Parses exponents and replaces them with expression values
|
||||||
* @param {string} expression - The expression to replace in.
|
* @param {string} expression - The expression to replace in.
|
||||||
* @return {string} The parsed expression
|
* @return {string} The parsed expression
|
||||||
*/
|
*/
|
||||||
export function exponentsToExpression(expression) {
|
export function exponentsToExpression(expression) {
|
||||||
return expression.replace(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(""))
|
||||||
}
|
}
|
||||||
|
|
118
common/test/basics/events.mjs
Normal file
118
common/test/basics/events.mjs
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
58
common/test/basics/interface.mjs
Normal file
58
common/test/basics/interface.mjs
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
231
common/test/basics/polyfill.mjs
Normal file
231
common/test/basics/polyfill.mjs
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
183
common/test/basics/utils.mjs
Normal file
183
common/test/basics/utils.mjs
Normal 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("&")
|
||||||
|
expect(escapeHTML("High & Mighty")).to.equal("High & Mighty")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("escapes injected HTML tags", function() {
|
||||||
|
expect(escapeHTML("<script>alert('Injected!')</script>")).to.equal("<script>alert('Injected!')</script>")
|
||||||
|
expect(escapeHTML('<a href="javascript:alert()">Link</a>')).to.equal('<a href="javascript:alert()">Link</a>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -15,16 +15,22 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import * as fs from "./mock/fs.mjs";
|
import * as fs from "./mock/fs.mjs"
|
||||||
import Qt from "./mock/qt.mjs";
|
import Qt from "./mock/qt.mjs"
|
||||||
import { MockHelper } from "./mock/helper.mjs";
|
import { MockHelper } from "./mock/helper.mjs"
|
||||||
import { MockLatex } from "./mock/latex.mjs";
|
import { MockLatex } from "./mock/latex.mjs"
|
||||||
import Modules from "../src/module/index.mjs";
|
|
||||||
|
import { use } from "chai"
|
||||||
|
import spies from "chai-spies"
|
||||||
|
import promised from "chai-as-promised"
|
||||||
|
|
||||||
function setup() {
|
function setup() {
|
||||||
|
use(promised)
|
||||||
|
const { spy } = use(spies)
|
||||||
|
|
||||||
globalThis.Helper = new MockHelper()
|
globalThis.Helper = new MockHelper()
|
||||||
globalThis.Latex = new MockLatex()
|
globalThis.Latex = new MockLatex()
|
||||||
Modules.Latex.initialize({ latex: Latex, helper: Helper })
|
globalThis.chaiPlugins = { spy }
|
||||||
}
|
}
|
||||||
|
|
||||||
setup()
|
setup()
|
||||||
|
|
|
@ -19,46 +19,46 @@
|
||||||
import { describe, it } from "mocha"
|
import { describe, it } from "mocha"
|
||||||
import { expect } from "chai"
|
import { expect } from "chai"
|
||||||
|
|
||||||
import { Domain, parseDomainSimple } from "../../src/math/domain.mjs"
|
// import { Domain, parseDomainSimple } from "../../src/math/domain.mjs"
|
||||||
|
//
|
||||||
describe("math.domain", function() {
|
// describe("math.domain", function() {
|
||||||
describe("#parseDomainSimple", function() {
|
// describe("#parseDomainSimple", function() {
|
||||||
it("returns predefined domains", function() {
|
// it("returns predefined domains", function() {
|
||||||
const predefinedToCheck = [
|
// const predefinedToCheck = [
|
||||||
// Real domains
|
// // Real domains
|
||||||
{ domain: Domain.R, shortcuts: ["R", "ℝ"] },
|
// { domain: Domain.R, shortcuts: ["R", "ℝ"] },
|
||||||
// Zero exclusive real domains
|
// // Zero exclusive real domains
|
||||||
{ domain: Domain.RE, shortcuts: ["RE", "R*", "ℝ*"] },
|
// { domain: Domain.RE, shortcuts: ["RE", "R*", "ℝ*"] },
|
||||||
// Real positive domains
|
// // Real positive domains
|
||||||
{ domain: Domain.RP, shortcuts: ["RP", "R+", "ℝ⁺", "ℝ+"] },
|
// { domain: Domain.RP, shortcuts: ["RP", "R+", "ℝ⁺", "ℝ+"] },
|
||||||
// Zero-exclusive real positive domains
|
// // Zero-exclusive real positive domains
|
||||||
{ domain: Domain.RPE, shortcuts: ["RPE", "REP", "R+*", "R*+", "ℝ*⁺", "ℝ⁺*", "ℝ*+", "ℝ+*"] },
|
// { domain: Domain.RPE, shortcuts: ["RPE", "REP", "R+*", "R*+", "ℝ*⁺", "ℝ⁺*", "ℝ*+", "ℝ+*"] },
|
||||||
// Real negative domain
|
// // Real negative domain
|
||||||
{ domain: Domain.RM, shortcuts: ["RM", "R-", "ℝ⁻", "ℝ-"] },
|
// { domain: Domain.RM, shortcuts: ["RM", "R-", "ℝ⁻", "ℝ-"] },
|
||||||
// Zero-exclusive real negative domains
|
// // Zero-exclusive real negative domains
|
||||||
{ domain: Domain.RME, shortcuts: ["RME", "REM", "R-*", "R*-", "ℝ⁻*", "ℝ*⁻", "ℝ-*", "ℝ*-"] },
|
// { domain: Domain.RME, shortcuts: ["RME", "REM", "R-*", "R*-", "ℝ⁻*", "ℝ*⁻", "ℝ-*", "ℝ*-"] },
|
||||||
// Natural integers domain
|
// // Natural integers domain
|
||||||
{ domain: Domain.N, shortcuts: ["ℕ", "N", "ZP", "Z+", "ℤ⁺", "ℤ+"] },
|
// { domain: Domain.N, shortcuts: ["ℕ", "N", "ZP", "Z+", "ℤ⁺", "ℤ+"] },
|
||||||
// Zero-exclusive natural integers domain
|
// // Zero-exclusive natural integers domain
|
||||||
{ domain: Domain.NE, shortcuts: ["NE", "NP", "N*", "N+", "ℕ*", "ℕ⁺", "ℕ+", "ZPE", "ZEP", "Z+*", "Z*+", "ℤ⁺*", "ℤ*⁺", "ℤ+*", "ℤ*+"] },
|
// { domain: Domain.NE, shortcuts: ["NE", "NP", "N*", "N+", "ℕ*", "ℕ⁺", "ℕ+", "ZPE", "ZEP", "Z+*", "Z*+", "ℤ⁺*", "ℤ*⁺", "ℤ+*", "ℤ*+"] },
|
||||||
// Logarithmic natural domains
|
// // Logarithmic natural domains
|
||||||
{ domain: Domain.NLog, shortcuts: ["NLOG", "ℕˡᵒᵍ", "ℕLOG"] },
|
// { domain: Domain.NLog, shortcuts: ["NLOG", "ℕˡᵒᵍ", "ℕLOG"] },
|
||||||
// All integers domains
|
// // All integers domains
|
||||||
{ domain: Domain.Z, shortcuts: ["Z", "ℤ"] },
|
// { domain: Domain.Z, shortcuts: ["Z", "ℤ"] },
|
||||||
// Zero-exclusive all integers domain
|
// // Zero-exclusive all integers domain
|
||||||
{ domain: Domain.ZE, shortcuts: ["ZE", "Z*", "ℤ*"] },
|
// { domain: Domain.ZE, shortcuts: ["ZE", "Z*", "ℤ*"] },
|
||||||
// Negative integers domain
|
// // Negative integers domain
|
||||||
{ domain: Domain.ZM, shortcuts: ["ZM", "Z-", "ℤ⁻", "ℤ-"] },
|
// { domain: Domain.ZM, shortcuts: ["ZM", "Z-", "ℤ⁻", "ℤ-"] },
|
||||||
// Zero-exclusive negative integers domain
|
// // Zero-exclusive negative integers domain
|
||||||
{ domain: Domain.ZME, shortcuts: ["ZME", "ZEM", "Z-*", "Z*-", "ℤ⁻*", "ℤ*⁻", "ℤ-*", "ℤ*-"] },
|
// { domain: Domain.ZME, shortcuts: ["ZME", "ZEM", "Z-*", "Z*-", "ℤ⁻*", "ℤ*⁻", "ℤ-*", "ℤ*-"] },
|
||||||
]
|
// ]
|
||||||
|
//
|
||||||
// Real domains
|
// // Real domains
|
||||||
for(const { domain, shortcuts } of predefinedToCheck)
|
// for(const { domain, shortcuts } of predefinedToCheck)
|
||||||
for(const shortcut of shortcuts)
|
// for(const shortcut of shortcuts)
|
||||||
expect(parseDomainSimple(shortcut)).to.be.equal(domain)
|
// expect(parseDomainSimple(shortcut)).to.be.equal(domain)
|
||||||
})
|
// })
|
||||||
|
//
|
||||||
it("")
|
// it("")
|
||||||
})
|
// })
|
||||||
})
|
// })
|
||||||
|
|
59
common/test/mock/canvas.mjs
Normal file
59
common/test/mock/canvas.mjs
Normal 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() {
|
||||||
|
}
|
||||||
|
}
|
23
common/test/mock/dialog.mjs
Normal file
23
common/test/mock/dialog.mjs
Normal 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() {}
|
||||||
|
}
|
|
@ -22,7 +22,8 @@ const DEFAULT_SETTINGS = {
|
||||||
"check_for_updates": true,
|
"check_for_updates": true,
|
||||||
"reset_redo_stack": true,
|
"reset_redo_stack": true,
|
||||||
"last_install_greet": "0",
|
"last_install_greet": "0",
|
||||||
"enable_latex": false,
|
"enable_latex": true,
|
||||||
|
"enable_latex_async": true,
|
||||||
"expression_editor": {
|
"expression_editor": {
|
||||||
"autoclose": true,
|
"autoclose": true,
|
||||||
"colorize": true,
|
"colorize": true,
|
||||||
|
@ -53,7 +54,13 @@ export class MockHelper {
|
||||||
this.__settings = { ...DEFAULT_SETTINGS }
|
this.__settings = { ...DEFAULT_SETTINGS }
|
||||||
}
|
}
|
||||||
|
|
||||||
__getSetting(settingName) {
|
|
||||||
|
/**
|
||||||
|
* Gets a setting from the config
|
||||||
|
* @param {string} settingName - Setting (and its dot-separated namespace) to get (e.g. "default_graph.xmin")
|
||||||
|
* @returns {string|number|boolean} Value of the setting
|
||||||
|
*/
|
||||||
|
getSetting(settingName) {
|
||||||
const namespace = settingName.split(".")
|
const namespace = settingName.split(".")
|
||||||
let data = this.__settings
|
let data = this.__settings
|
||||||
for(const name of namespace)
|
for(const name of namespace)
|
||||||
|
@ -64,7 +71,12 @@ export class MockHelper {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
__setSetting(settingName, value) {
|
/**
|
||||||
|
* Sets a setting in the config
|
||||||
|
* @param {string} settingName - Setting (and its dot-separated namespace) to set (e.g. "default_graph.xmin")
|
||||||
|
* @param {string|number|boolean} value
|
||||||
|
*/
|
||||||
|
setSetting(settingName, value) {
|
||||||
const namespace = settingName.split(".")
|
const namespace = settingName.split(".")
|
||||||
const finalName = namespace.pop()
|
const finalName = namespace.pop()
|
||||||
let data = this.__settings
|
let data = this.__settings
|
||||||
|
@ -76,60 +88,6 @@ export class MockHelper {
|
||||||
data[finalName] = value
|
data[finalName] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a setting from the config
|
|
||||||
* @param {string} settingName - Setting (and its dot-separated namespace) to get (e.g. "default_graph.xmin")
|
|
||||||
* @returns {boolean} Value of the setting
|
|
||||||
*/
|
|
||||||
getSettingBool(settingName) {
|
|
||||||
return this.__getSetting(settingName) === true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a setting from the config
|
|
||||||
* @param {string} settingName - Setting (and its dot-separated namespace) to get (e.g. "default_graph.xmin")
|
|
||||||
* @returns {number} Value of the setting
|
|
||||||
*/
|
|
||||||
getSettingInt(settingName) {
|
|
||||||
return +(this.__getSetting(settingName))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a setting from the config
|
|
||||||
* @param {string} settingName - Setting (and its dot-separated namespace) to get (e.g. "default_graph.xmin")
|
|
||||||
* @returns {string} Value of the setting
|
|
||||||
*/
|
|
||||||
getSetting(settingName) {
|
|
||||||
return this.__getSetting(settingName).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets a setting in the config
|
|
||||||
* @param {string} settingName - Setting (and its dot-separated namespace) to set (e.g. "default_graph.xmin")
|
|
||||||
* @param {boolean} value
|
|
||||||
*/
|
|
||||||
setSettingBool(settingName, value) {
|
|
||||||
return this.__setSetting(settingName, value === true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets a setting in the config
|
|
||||||
* @param {string} settingName - Setting (and its dot-separated namespace) to set (e.g. "default_graph.xmin")
|
|
||||||
* @param {number} value
|
|
||||||
*/
|
|
||||||
setSettingInt(settingName, value) {
|
|
||||||
return this.__setSetting(settingName, +(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets a setting in the config
|
|
||||||
* @param {string} settingName - Setting (and its dot-separated namespace) to set (e.g. "default_graph.xmin")
|
|
||||||
* @param {string} value
|
|
||||||
*/
|
|
||||||
setSetting(settingName, value) {
|
|
||||||
return this.__setSetting(settingName, value.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends data to be written
|
* Sends data to be written
|
||||||
* @param {string} file
|
* @param {string} file
|
||||||
|
|
|
@ -22,6 +22,7 @@ const PIXEL = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAACklEQVR4AWNgAAAAA
|
||||||
|
|
||||||
export class MockLatex {
|
export class MockLatex {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
this.supportsAsyncRender = true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -55,13 +56,23 @@ export class MockLatex {
|
||||||
return `${TMP}/${name}.png`
|
return `${TMP}/${name}.png`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} markup - LaTeX markup to render
|
||||||
|
* @param {number} fontSize - Font size (in pt) to render
|
||||||
|
* @param {string} color - Color of the text to render
|
||||||
|
* @returns {Promise<string>} - Comma separated data of the image (source, width, height)
|
||||||
|
*/
|
||||||
|
async renderAsync(markup, fontSize, color) {
|
||||||
|
return this.renderSync(markup, fontSize, color)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} markup - LaTeX markup to render
|
* @param {string} markup - LaTeX markup to render
|
||||||
* @param {number} fontSize - Font size (in pt) to render
|
* @param {number} fontSize - Font size (in pt) to render
|
||||||
* @param {string} color - Color of the text to render
|
* @param {string} color - Color of the text to render
|
||||||
* @returns {string} - Comma separated data of the image (source, width, height)
|
* @returns {string} - Comma separated data of the image (source, width, height)
|
||||||
*/
|
*/
|
||||||
render(markup, fontSize, color) {
|
renderSync(markup, fontSize, color) {
|
||||||
const file = this.__getFileName(markup, fontSize, color)
|
const file = this.__getFileName(markup, fontSize, color)
|
||||||
writeFileSync(file, PIXEL, "base64")
|
writeFileSync(file, PIXEL, "base64")
|
||||||
return `${file},1,1`
|
return `${file},1,1`
|
||||||
|
|
61
common/test/mock/root.mjs
Normal file
61
common/test/mock/root.mjs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
/**
|
||||||
|
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||||
|
* Copyright (C) 2021-2024 Ad5001
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
}
|
89
common/test/module/base.mjs
Normal file
89
common/test/module/base.mjs
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
389
common/test/module/expreval.mjs
Normal file
389
common/test/module/expreval.mjs
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
231
common/test/module/latex.mjs
Normal file
231
common/test/module/latex.mjs
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
30
common/test/module/objects.mjs
Normal file
30
common/test/module/objects.mjs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/**
|
||||||
|
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||||
|
* Copyright (C) 2021-2024 Ad5001
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 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() {
|
||||||
|
//
|
||||||
|
// })
|
101
common/test/module/settings.mjs
Normal file
101
common/test/module/settings.mjs
Normal 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")
|
||||||
|
})
|
||||||
|
})
|
|
@ -29,7 +29,7 @@ if not is_release and which('git') is not None:
|
||||||
|
|
||||||
# Command to check date of latest git commit
|
# Command to check date of latest git commit
|
||||||
cmd = ['git', 'log', '--format=%ci', '-n 1']
|
cmd = ['git', 'log', '--format=%ci', '-n 1']
|
||||||
cwd = realpath(join(dirname(__file__), '..')) # Root AccountFree directory.
|
cwd = realpath(join(dirname(__file__), '..', '..', '..')) # Root LogarithmPlotter directory.
|
||||||
if exists(join(cwd, '.git')):
|
if exists(join(cwd, '.git')):
|
||||||
date_str = check_output(cmd, cwd=cwd).decode('utf-8').split(' ')[0]
|
date_str = check_output(cmd, cwd=cwd).decode('utf-8').split(' ')[0]
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -17,9 +17,9 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from os import getcwd, chdir, environ, path
|
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 path as sys_path
|
||||||
from sys import platform, argv, exit
|
from sys import argv, exit
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
|
@ -49,6 +49,18 @@ from LogarithmPlotter.util.helper import Helper
|
||||||
from LogarithmPlotter.util.latex import Latex
|
from LogarithmPlotter.util.latex import Latex
|
||||||
from LogarithmPlotter.util.js import PyJSValue
|
from LogarithmPlotter.util.js import PyJSValue
|
||||||
|
|
||||||
|
OS_NAME = os_name()
|
||||||
|
|
||||||
|
|
||||||
|
CACHE_PATH = {
|
||||||
|
"Linux": path.join(environ["XDG_CONFIG_HOME"], "LogarithmPlotter")
|
||||||
|
if "XDG_CONFIG_HOME" in environ else
|
||||||
|
path.join(path.expanduser("~"), ".cache", "LogarithmPlotter"),
|
||||||
|
"Windows": path.join(path.expandvars('%APPDATA%'), "LogarithmPlotter", "cache"),
|
||||||
|
"Darwin": path.join(path.expanduser("~"), "Library", "Caches", "LogarithmPlotter"),
|
||||||
|
}[OS_NAME]
|
||||||
|
|
||||||
|
|
||||||
LINUX_THEMES = { # See https://specifications.freedesktop.org/menu-spec/latest/onlyshowin-registry.html
|
LINUX_THEMES = { # See https://specifications.freedesktop.org/menu-spec/latest/onlyshowin-registry.html
|
||||||
"COSMIC": "Basic",
|
"COSMIC": "Basic",
|
||||||
"GNOME": "Basic",
|
"GNOME": "Basic",
|
||||||
|
@ -82,11 +94,10 @@ def get_linux_theme() -> str:
|
||||||
|
|
||||||
def get_platform_qt_style(os) -> str:
|
def get_platform_qt_style(os) -> str:
|
||||||
return {
|
return {
|
||||||
"linux": get_linux_theme(),
|
"Linux": get_linux_theme(),
|
||||||
"freebsd": get_linux_theme(),
|
"Windows": "Universal" if OS_RELEASE() in ["10", "11", "12", "13", "14"] else "Windows",
|
||||||
"win32": "Universal" if os_release() in ["10", "11", "12", "13", "14"] else "Windows",
|
"Darwin": "macOS",
|
||||||
"cygwin": "Fusion",
|
"Android": "Material"
|
||||||
"darwin": "macOS"
|
|
||||||
}[os]
|
}[os]
|
||||||
|
|
||||||
|
|
||||||
|
@ -147,7 +158,7 @@ def run():
|
||||||
config.init()
|
config.init()
|
||||||
|
|
||||||
if not 'QT_QUICK_CONTROLS_STYLE' in environ:
|
if not 'QT_QUICK_CONTROLS_STYLE' in environ:
|
||||||
QQuickStyle.setStyle(get_platform_qt_style(platform))
|
QQuickStyle.setStyle(get_platform_qt_style(OS_NAME))
|
||||||
|
|
||||||
dep_time = time()
|
dep_time = time()
|
||||||
print("Loaded dependencies in " + str((dep_time - start_time) * 1000) + "ms.")
|
print("Loaded dependencies in " + str((dep_time - start_time) * 1000) + "ms.")
|
||||||
|
@ -159,12 +170,12 @@ def run():
|
||||||
|
|
||||||
# Installing macOS file handler.
|
# Installing macOS file handler.
|
||||||
macos_file_open_handler = None
|
macos_file_open_handler = None
|
||||||
if platform == "darwin":
|
if OS_NAME == "Darwin":
|
||||||
macos_file_open_handler = native.MacOSFileOpenHandler()
|
macos_file_open_handler = native.MacOSFileOpenHandler()
|
||||||
app.installEventFilter(macos_file_open_handler)
|
app.installEventFilter(macos_file_open_handler)
|
||||||
|
|
||||||
helper = Helper(pwd, tmpfile)
|
helper = Helper(pwd, tmpfile)
|
||||||
latex = Latex(tempdir)
|
latex = Latex(CACHE_PATH)
|
||||||
engine, js_globals = create_engine(helper, latex, dep_time)
|
engine, js_globals = create_engine(helper, latex, dep_time)
|
||||||
|
|
||||||
if len(engine.rootObjects()) == 0: # No root objects loaded
|
if len(engine.rootObjects()) == 0: # No root objects loaded
|
||||||
|
@ -177,7 +188,7 @@ def run():
|
||||||
js_globals.Modules.IO.loadDiagram(argv[-1])
|
js_globals.Modules.IO.loadDiagram(argv[-1])
|
||||||
chdir(path.dirname(path.realpath(__file__)))
|
chdir(path.dirname(path.realpath(__file__)))
|
||||||
|
|
||||||
if platform == "darwin":
|
if OS_NAME == "Darwin":
|
||||||
macos_file_open_handler.init_io(js_globals.Modules.IO)
|
macos_file_open_handler.init_io(js_globals.Modules.IO)
|
||||||
|
|
||||||
# Check for LaTeX installation if LaTeX support is enabled
|
# Check for LaTeX installation if LaTeX support is enabled
|
||||||
|
|
|
@ -76,18 +76,16 @@ MenuBar {
|
||||||
Action {
|
Action {
|
||||||
text: qsTr("&Undo")
|
text: qsTr("&Undo")
|
||||||
shortcut: StandardKey.Undo
|
shortcut: StandardKey.Undo
|
||||||
onTriggered: history.undo()
|
onTriggered: Modules.History.undo()
|
||||||
icon.name: 'edit-undo'
|
icon.name: 'edit-undo'
|
||||||
icon.color: enabled ? sysPalette.windowText : sysPaletteIn.windowText
|
icon.color: enabled ? sysPalette.windowText : sysPaletteIn.windowText
|
||||||
enabled: history.undoCount > 0
|
|
||||||
}
|
}
|
||||||
Action {
|
Action {
|
||||||
text: qsTr("&Redo")
|
text: qsTr("&Redo")
|
||||||
shortcut: StandardKey.Redo
|
shortcut: StandardKey.Redo
|
||||||
onTriggered: history.redo()
|
onTriggered: Modules.History.redo()
|
||||||
icon.name: 'edit-redo'
|
icon.name: 'edit-redo'
|
||||||
icon.color: enabled ? sysPalette.windowText : sysPaletteIn.windowText
|
icon.color: enabled ? sysPalette.windowText : sysPaletteIn.windowText
|
||||||
enabled: history.redoCount > 0
|
|
||||||
}
|
}
|
||||||
Action {
|
Action {
|
||||||
text: qsTr("&Copy plot")
|
text: qsTr("&Copy plot")
|
||||||
|
@ -119,7 +117,7 @@ MenuBar {
|
||||||
icon.color: sysPalette.buttonText
|
icon.color: sysPalette.buttonText
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
var newObj = Modules.Objects.createNewRegisteredObject(modelData)
|
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()
|
objectLists.update()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -16,6 +16,8 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import eu.ad5001.LogarithmPlotter.Setting 1.0 as Setting
|
import eu.ad5001.LogarithmPlotter.Setting 1.0 as Setting
|
||||||
|
@ -47,6 +49,18 @@ Item {
|
||||||
*/
|
*/
|
||||||
property bool darkTheme: isDarkTheme()
|
property bool darkTheme: isDarkTheme()
|
||||||
|
|
||||||
|
/*!
|
||||||
|
\qmlproperty int HistoryBrowser::undoCount
|
||||||
|
Number of actions in the undo stack.
|
||||||
|
*/
|
||||||
|
property int undoCount: 0
|
||||||
|
|
||||||
|
/*!
|
||||||
|
\qmlproperty int HistoryBrowser::redoCount
|
||||||
|
Number of actions in the redo stack.
|
||||||
|
*/
|
||||||
|
property int redoCount: 0
|
||||||
|
|
||||||
Setting.TextSetting {
|
Setting.TextSetting {
|
||||||
id: filterInput
|
id: filterInput
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
@ -76,19 +90,22 @@ Item {
|
||||||
id: redoColumn
|
id: redoColumn
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
width: actionWidth
|
width: historyBrowser.actionWidth
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: history.redoCount
|
model: historyBrowser.redoCount
|
||||||
|
|
||||||
HistoryItem {
|
HistoryItem {
|
||||||
id: redoButton
|
id: redoButton
|
||||||
width: actionWidth
|
width: historyBrowser.actionWidth
|
||||||
//height: actionHeight
|
//height: actionHeight
|
||||||
isRedo: true
|
isRedo: true
|
||||||
idx: index
|
|
||||||
darkTheme: historyBrowser.darkTheme
|
darkTheme: historyBrowser.darkTheme
|
||||||
hidden: !(filterInput.value == "" || content.includes(filterInput.value))
|
hidden: !(filterInput.value == "" || content.includes(filterInput.value))
|
||||||
|
onClicked: {
|
||||||
|
redoTimer.toRedoCount = Modules.History.redoStack.length-index
|
||||||
|
redoTimer.start()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,14 +118,14 @@ Item {
|
||||||
transform: Rotation { origin.x: 30; origin.y: 30; angle: 270}
|
transform: Rotation { origin.x: 30; origin.y: 30; angle: 270}
|
||||||
height: 70
|
height: 70
|
||||||
width: 20
|
width: 20
|
||||||
visible: history.redoCount > 0
|
visible: historyBrowser.redoCount > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: nowRect
|
id: nowRect
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.top: redoColumn.bottom
|
anchors.top: redoColumn.bottom
|
||||||
width: actionWidth
|
width: historyBrowser.actionWidth
|
||||||
height: 40
|
height: 40
|
||||||
color: sysPalette.highlight
|
color: sysPalette.highlight
|
||||||
Text {
|
Text {
|
||||||
|
@ -124,20 +141,24 @@ Item {
|
||||||
id: undoColumn
|
id: undoColumn
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.top: nowRect.bottom
|
anchors.top: nowRect.bottom
|
||||||
width: actionWidth
|
width: historyBrowser.actionWidth
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: history.undoCount
|
model: historyBrowser.undoCount
|
||||||
|
|
||||||
|
|
||||||
HistoryItem {
|
HistoryItem {
|
||||||
id: undoButton
|
id: undoButton
|
||||||
width: actionWidth
|
width: historyBrowser.actionWidth
|
||||||
//height: actionHeight
|
//height: actionHeight
|
||||||
isRedo: false
|
isRedo: false
|
||||||
idx: index
|
|
||||||
darkTheme: historyBrowser.darkTheme
|
darkTheme: historyBrowser.darkTheme
|
||||||
hidden: !(filterInput.value == "" || content.includes(filterInput.value))
|
hidden: !(filterInput.value == "" || content.includes(filterInput.value))
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
undoTimer.toUndoCount = +index+1
|
||||||
|
undoTimer.start()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -150,7 +171,39 @@ Item {
|
||||||
transform: Rotation { origin.x: 30; origin.y: 30; angle: 270}
|
transform: Rotation { origin.x: 30; origin.y: 30; angle: 270}
|
||||||
height: 60
|
height: 60
|
||||||
width: 20
|
width: 20
|
||||||
visible: 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()
|
let hex = sysPalette.windowText.toString()
|
||||||
// We only check the first parameter, as on all normal OSes, text color is grayscale.
|
// We only check the first parameter, as on all normal OSes, text color is grayscale.
|
||||||
return parseInt(hex.substr(1,2), 16) > 128
|
return parseInt(hex.substr(1,2), 16) > 128
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
Modules.History.initialize({
|
||||||
|
helper: Helper,
|
||||||
|
themeTextColor: sysPalette.windowText.toString(),
|
||||||
|
imageDepth: Screen.devicePixelRatio,
|
||||||
|
fontSize: 14
|
||||||
|
})
|
||||||
|
Modules.History.on("cleared loaded added undone redone", () => {
|
||||||
|
undoCount = Modules.History.undoStack.length
|
||||||
|
redoCount = Modules.History.redoStack.length
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,9 +16,8 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Qt5Compat.GraphicalEffects
|
import QtQuick.Controls
|
||||||
import eu.ad5001.LogarithmPlotter.Setting 1.0 as Setting
|
import eu.ad5001.LogarithmPlotter.Setting 1.0 as Setting
|
||||||
|
|
||||||
|
|
||||||
|
@ -41,17 +40,17 @@ Button {
|
||||||
\qmlproperty bool HistoryItem::isRedo
|
\qmlproperty bool HistoryItem::isRedo
|
||||||
true if the action is in the redo stack, false othewise.
|
true if the action is in the redo stack, false othewise.
|
||||||
*/
|
*/
|
||||||
property bool isRedo
|
required property bool isRedo
|
||||||
/*!
|
/*!
|
||||||
\qmlproperty int HistoryItem::idx
|
\qmlproperty int HistoryItem::index
|
||||||
Index of the item within the HistoryBrowser list.
|
Index of the item within the HistoryBrowser list.
|
||||||
*/
|
*/
|
||||||
property int idx
|
required property int index
|
||||||
/*!
|
/*!
|
||||||
\qmlproperty bool HistoryItem::darkTheme
|
\qmlproperty bool HistoryItem::darkTheme
|
||||||
true when the system is running with a dark theme, false otherwise.
|
true when the system is running with a dark theme, false otherwise.
|
||||||
*/
|
*/
|
||||||
property bool darkTheme
|
required property bool darkTheme
|
||||||
/*!
|
/*!
|
||||||
\qmlproperty bool HistoryItem::hidden
|
\qmlproperty bool HistoryItem::hidden
|
||||||
true when the item is filtered out, false otherwise.
|
true when the item is filtered out, false otherwise.
|
||||||
|
@ -61,7 +60,7 @@ Button {
|
||||||
\qmlproperty int HistoryItem::historyAction
|
\qmlproperty int HistoryItem::historyAction
|
||||||
Associated history action.
|
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
|
\qmlproperty int HistoryItem::actionHeight
|
||||||
|
@ -82,12 +81,11 @@ Button {
|
||||||
height: hidden ? 8 : Math.max(actionHeight, label.height + 15)
|
height: hidden ? 8 : Math.max(actionHeight, label.height + 15)
|
||||||
|
|
||||||
|
|
||||||
LinearGradient {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
//opacity: hidden ? 0.6 : 1
|
//opacity: hidden ? 0.6 : 1
|
||||||
start: Qt.point(0, 0)
|
|
||||||
end: Qt.point(parent.width, 0)
|
|
||||||
gradient: Gradient {
|
gradient: Gradient {
|
||||||
|
orientation: Gradient.Horizontal
|
||||||
GradientStop { position: 0.1; color: "transparent" }
|
GradientStop { position: 0.1; color: "transparent" }
|
||||||
GradientStop { position: 1.5; color: clr }
|
GradientStop { position: 1.5; color: clr }
|
||||||
}
|
}
|
||||||
|
@ -147,13 +145,6 @@ Button {
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
ToolTip.delay: 200
|
ToolTip.delay: 200
|
||||||
ToolTip.text: content
|
ToolTip.text: content
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
if(isRedo)
|
|
||||||
history.redoMultipleDefered(history.redoCount-idx)
|
|
||||||
else
|
|
||||||
history.undoMultipleDefered(+idx+1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
module eu.ad5001.LogarithmPlotter.History
|
module eu.ad5001.LogarithmPlotter.History
|
||||||
|
|
||||||
History 1.0 History.qml
|
|
||||||
HistoryBrowser 1.0 HistoryBrowser.qml
|
HistoryBrowser 1.0 HistoryBrowser.qml
|
||||||
HistoryItem 1.0 HistoryItem.qml
|
HistoryItem 1.0 HistoryItem.qml
|
||||||
|
|
|
@ -30,104 +30,15 @@ import Qt.labs.platform as Native
|
||||||
*/
|
*/
|
||||||
Canvas {
|
Canvas {
|
||||||
id: canvas
|
id: canvas
|
||||||
anchors.top: separator.bottom
|
anchors.top: parent.top
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
height: parent.height - 90
|
height: parent.height - 90
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|
||||||
/*!
|
|
||||||
\qmlproperty double LogGraphCanvas::xmin
|
|
||||||
Minimum x of the diagram, provided from settings.
|
|
||||||
\sa Settings
|
|
||||||
*/
|
|
||||||
property double xmin: 0
|
|
||||||
/*!
|
|
||||||
\qmlproperty double LogGraphCanvas::ymax
|
|
||||||
Maximum y of the diagram, provided from settings.
|
|
||||||
\sa Settings
|
|
||||||
*/
|
|
||||||
property double ymax: 0
|
|
||||||
/*!
|
|
||||||
\qmlproperty double LogGraphCanvas::xzoom
|
|
||||||
Zoom on the x axis of the diagram, provided from settings.
|
|
||||||
\sa Settings
|
|
||||||
*/
|
|
||||||
property double xzoom: 10
|
|
||||||
/*!
|
|
||||||
\qmlproperty double LogGraphCanvas::yzoom
|
|
||||||
Zoom on the y axis of the diagram, provided from settings.
|
|
||||||
\sa Settings
|
|
||||||
*/
|
|
||||||
property double yzoom: 10
|
|
||||||
/*!
|
|
||||||
\qmlproperty string LogGraphCanvas::xaxisstep
|
|
||||||
Step of the x axis graduation, provided from settings.
|
|
||||||
\note: Only available in non-logarithmic mode.
|
|
||||||
\sa Settings
|
|
||||||
*/
|
|
||||||
property string xaxisstep: "4"
|
|
||||||
/*!
|
|
||||||
\qmlproperty string LogGraphCanvas::yaxisstep
|
|
||||||
Step of the y axis graduation, provided from settings.
|
|
||||||
\sa Settings
|
|
||||||
*/
|
|
||||||
property string yaxisstep: "4"
|
|
||||||
/*!
|
|
||||||
\qmlproperty string LogGraphCanvas::xlabel
|
|
||||||
Label used on the x axis, provided from settings.
|
|
||||||
\sa Settings
|
|
||||||
*/
|
|
||||||
property string xlabel: ""
|
|
||||||
/*!
|
|
||||||
\qmlproperty string LogGraphCanvas::ylabel
|
|
||||||
Label used on the y axis, provided from settings.
|
|
||||||
\sa Settings
|
|
||||||
*/
|
|
||||||
property string ylabel: ""
|
|
||||||
/*!
|
|
||||||
\qmlproperty double LogGraphCanvas::linewidth
|
|
||||||
Width of lines that will be drawn into the canvas, provided from settings.
|
|
||||||
\sa Settings
|
|
||||||
*/
|
|
||||||
property double linewidth: 1
|
|
||||||
/*!
|
|
||||||
\qmlproperty double LogGraphCanvas::textsize
|
|
||||||
Font size of the text that will be drawn into the canvas, provided from settings.
|
|
||||||
\sa Settings
|
|
||||||
*/
|
|
||||||
property double textsize: 14
|
|
||||||
/*!
|
|
||||||
\qmlproperty bool LogGraphCanvas::logscalex
|
|
||||||
true if the canvas should be in logarithmic mode, false otherwise.
|
|
||||||
Provided from settings.
|
|
||||||
\sa Settings
|
|
||||||
*/
|
|
||||||
property bool logscalex: false
|
|
||||||
/*!
|
|
||||||
\qmlproperty bool LogGraphCanvas::showxgrad
|
|
||||||
true if the x graduation should be shown, false otherwise.
|
|
||||||
Provided from settings.
|
|
||||||
\sa Settings
|
|
||||||
*/
|
|
||||||
property bool showxgrad: false
|
|
||||||
/*!
|
|
||||||
\qmlproperty bool LogGraphCanvas::showygrad
|
|
||||||
true if the y graduation should be shown, false otherwise.
|
|
||||||
Provided from settings.
|
|
||||||
\sa Settings
|
|
||||||
*/
|
|
||||||
property bool showygrad: false
|
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
\qmlproperty var LogGraphCanvas::imageLoaders
|
\qmlproperty var LogGraphCanvas::imageLoaders
|
||||||
Dictionary of format {image: [callback.image data]} containing data for defered image loading.
|
Dictionary of format {image: callback} containing data for deferred image loading.
|
||||||
*/
|
*/
|
||||||
property var imageLoaders: {}
|
property var imageLoaders: {}
|
||||||
/*!
|
|
||||||
\qmlproperty var LogGraphCanvas::ctx
|
|
||||||
Cache for the 2D context so that it may be used asynchronously.
|
|
||||||
*/
|
|
||||||
property var ctx
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
imageLoaders = {}
|
imageLoaders = {}
|
||||||
|
@ -155,9 +66,21 @@ Canvas {
|
||||||
Object.keys(imageLoaders).forEach((key) => {
|
Object.keys(imageLoaders).forEach((key) => {
|
||||||
if(isImageLoaded(key)) {
|
if(isImageLoaded(key)) {
|
||||||
// Calling callback
|
// Calling callback
|
||||||
imageLoaders[key][0](canvas, ctx, imageLoaders[key][1])
|
imageLoaders[key]()
|
||||||
delete imageLoaders[key]
|
delete imageLoaders[key]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
\qmlmethod void LogGraphCanvas::loadImageAsync(string imageSource)
|
||||||
|
Loads an image data onto the canvas asynchronously.
|
||||||
|
Returns a Promise that is resolved when the image is loaded.
|
||||||
|
*/
|
||||||
|
function loadImageAsync(imageSource) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.loadImage(imageSource)
|
||||||
|
this.imageLoaders[imageSource] = resolve
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ ApplicationWindow {
|
||||||
width: 1000
|
width: 1000
|
||||||
height: 500
|
height: 500
|
||||||
color: sysPalette.window
|
color: sysPalette.window
|
||||||
title: "LogarithmPlotter " + (settings.saveFilename != "" ? " - " + settings.saveFilename.split('/').pop() : "") + (history.saved ? "" : "*")
|
title: qsTr("untitled")
|
||||||
|
|
||||||
SystemPalette { id: sysPalette; colorGroup: SystemPalette.Active }
|
SystemPalette { id: sysPalette; colorGroup: SystemPalette.Active }
|
||||||
SystemPalette { id: sysPaletteIn; colorGroup: SystemPalette.Disabled }
|
SystemPalette { id: sysPaletteIn; colorGroup: SystemPalette.Disabled }
|
||||||
|
@ -51,8 +51,6 @@ ApplicationWindow {
|
||||||
|
|
||||||
AppMenuBar {id: appMenu}
|
AppMenuBar {id: appMenu}
|
||||||
|
|
||||||
History { id: history }
|
|
||||||
|
|
||||||
Popup.GreetScreen {}
|
Popup.GreetScreen {}
|
||||||
|
|
||||||
Popup.Preferences {id: preferences}
|
Popup.Preferences {id: preferences}
|
||||||
|
@ -145,20 +143,6 @@ ApplicationWindow {
|
||||||
width: sidebar.inPortrait ? parent.width : parent.width - sidebar.width//*sidebar.position
|
width: sidebar.inPortrait ? parent.width : parent.width - sidebar.width//*sidebar.position
|
||||||
x: sidebar.width//*sidebar.position
|
x: sidebar.width//*sidebar.position
|
||||||
|
|
||||||
xmin: settings.xmin
|
|
||||||
ymax: settings.ymax
|
|
||||||
xzoom: settings.xzoom
|
|
||||||
yzoom: settings.yzoom
|
|
||||||
xlabel: settings.xlabel
|
|
||||||
ylabel: settings.ylabel
|
|
||||||
yaxisstep: settings.yaxisstep
|
|
||||||
xaxisstep: settings.xaxisstep
|
|
||||||
logscalex: settings.logscalex
|
|
||||||
linewidth: settings.linewidth
|
|
||||||
textsize: settings.textsize
|
|
||||||
showxgrad: settings.showxgrad
|
|
||||||
showygrad: settings.showygrad
|
|
||||||
|
|
||||||
property bool firstDrawDone: false
|
property bool firstDrawDone: false
|
||||||
|
|
||||||
onPainted: if(!firstDrawDone) {
|
onPainted: if(!firstDrawDone) {
|
||||||
|
@ -199,7 +183,7 @@ ApplicationWindow {
|
||||||
}
|
}
|
||||||
|
|
||||||
onClosing: function(close) {
|
onClosing: function(close) {
|
||||||
if(!history.saved) {
|
if(!Modules.IO.saved) {
|
||||||
close.accepted = false
|
close.accepted = false
|
||||||
appMenu.openSaveUnsavedChangesDialog()
|
appMenu.openSaveUnsavedChangesDialog()
|
||||||
}
|
}
|
||||||
|
@ -264,5 +248,22 @@ ApplicationWindow {
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
Modules.IO.initialize({ root, settings, alert })
|
Modules.IO.initialize({ root, settings, alert })
|
||||||
Modules.Latex.initialize({ latex: Latex, helper: Helper })
|
Modules.Latex.initialize({ latex: Latex, helper: Helper })
|
||||||
|
Modules.Settings.on("changed", (evt) => {
|
||||||
|
if(evt.property === "saveFilename") {
|
||||||
|
const fileName = evt.newValue.split('/').pop().split('\\').pop()
|
||||||
|
if(fileName !== "")
|
||||||
|
title = fileName
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Modules.IO.on("saved loaded", (evt) => {
|
||||||
|
// Refreshing sidebar
|
||||||
|
updateObjectsLists()
|
||||||
|
if(title.endsWith("*"))
|
||||||
|
title = title.substring(0, title.length-1)
|
||||||
|
})
|
||||||
|
Modules.IO.on("modified", () => {
|
||||||
|
if(!title.endsWith("*"))
|
||||||
|
title = title+"*"
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,7 @@ Repeater {
|
||||||
variables: propertyType.variables
|
variables: propertyType.variables
|
||||||
onChanged: function(newExpr) {
|
onChanged: function(newExpr) {
|
||||||
if(obj[propertyName].toString() != newExpr.toString()) {
|
if(obj[propertyName].toString() != newExpr.toString()) {
|
||||||
history.addToHistory(new JS.HistoryLib.EditedProperty(
|
Modules.History.addToHistory(new JS.HistoryLib.EditedProperty(
|
||||||
obj.name, objType, propertyName,
|
obj.name, objType, propertyName,
|
||||||
obj[propertyName], newExpr
|
obj[propertyName], newExpr
|
||||||
))
|
))
|
||||||
|
@ -123,7 +123,7 @@ Repeater {
|
||||||
|
|
||||||
// Ensuring old and new values are different to prevent useless adding to history.
|
// Ensuring old and new values are different to prevent useless adding to history.
|
||||||
if(obj[propertyName] != newValueParsed) {
|
if(obj[propertyName] != newValueParsed) {
|
||||||
history.addToHistory(new JS.HistoryLib.EditedProperty(
|
Modules.History.addToHistory(new JS.HistoryLib.EditedProperty(
|
||||||
obj.name, objType, propertyName,
|
obj.name, objType, propertyName,
|
||||||
obj[propertyName], newValueParsed
|
obj[propertyName], newValueParsed
|
||||||
))
|
))
|
||||||
|
@ -168,7 +168,7 @@ Repeater {
|
||||||
return obj[propertyName]
|
return obj[propertyName]
|
||||||
}
|
}
|
||||||
onClicked: {
|
onClicked: {
|
||||||
history.addToHistory(new JS.HistoryLib.EditedProperty(
|
Modules.History.addToHistory(new JS.HistoryLib.EditedProperty(
|
||||||
obj.name, objType, propertyName,
|
obj.name, objType, propertyName,
|
||||||
obj[propertyName], this.checked
|
obj[propertyName], this.checked
|
||||||
))
|
))
|
||||||
|
@ -209,7 +209,9 @@ Repeater {
|
||||||
if(selectedObj == null) {
|
if(selectedObj == null) {
|
||||||
// Creating new object.
|
// Creating new object.
|
||||||
selectedObj = Modules.Objects.createNewRegisteredObject(propertyType.objType)
|
selectedObj = Modules.Objects.createNewRegisteredObject(propertyType.objType)
|
||||||
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(
|
baseModel = Modules.Objects.getObjectsName(propertyType.objType).concat(
|
||||||
isRealObject ? [qsTr("+ Create new %1").arg(Modules.Objects.types[propertyType.objType].displayType())] :
|
isRealObject ? [qsTr("+ Create new %1").arg(Modules.Objects.types[propertyType.objType].displayType())] :
|
||||||
[])
|
[])
|
||||||
|
@ -219,14 +221,14 @@ Repeater {
|
||||||
//Modules.Objects.currentObjects[objType][objIndex].requiredBy = obj[propertyName].filter((obj) => obj.name != obj.name)
|
//Modules.Objects.currentObjects[objType][objIndex].requiredBy = obj[propertyName].filter((obj) => obj.name != obj.name)
|
||||||
}
|
}
|
||||||
obj.requiredBy = obj.requiredBy.filter((obj) => obj.name != obj.name)
|
obj.requiredBy = obj.requiredBy.filter((obj) => obj.name != obj.name)
|
||||||
history.addToHistory(new JS.HistoryLib.EditedProperty(
|
Modules.History.addToHistory(new JS.HistoryLib.EditedProperty(
|
||||||
obj.name, objType, propertyName,
|
obj.name, objType, propertyName,
|
||||||
obj[propertyName], selectedObj
|
obj[propertyName], selectedObj
|
||||||
))
|
))
|
||||||
obj[propertyName] = selectedObj
|
obj[propertyName] = selectedObj
|
||||||
} else if(baseModel[newIndex] != obj[propertyName]) {
|
} else if(baseModel[newIndex] != obj[propertyName]) {
|
||||||
// Ensuring new property is different to not add useless history entries.
|
// Ensuring new property is different to not add useless history entries.
|
||||||
history.addToHistory(new JS.HistoryLib.EditedProperty(
|
Modules.History.addToHistory(new JS.HistoryLib.EditedProperty(
|
||||||
obj.name, objType, propertyName,
|
obj.name, objType, propertyName,
|
||||||
obj[propertyName], baseModel[newIndex]
|
obj[propertyName], baseModel[newIndex]
|
||||||
))
|
))
|
||||||
|
@ -256,7 +258,7 @@ Repeater {
|
||||||
|
|
||||||
onChanged: {
|
onChanged: {
|
||||||
var exported = exportModel()
|
var exported = exportModel()
|
||||||
history.addToHistory(new JS.HistoryLib.EditedProperty(
|
Modules.History.addToHistory(new JS.HistoryLib.EditedProperty(
|
||||||
obj.name, objType, propertyName,
|
obj.name, objType, propertyName,
|
||||||
obj[propertyName], exported
|
obj[propertyName], exported
|
||||||
))
|
))
|
||||||
|
|
|
@ -112,7 +112,7 @@ Popup.BaseDialog {
|
||||||
if(newName in Modules.Objects.currentObjectsByName) {
|
if(newName in Modules.Objects.currentObjectsByName) {
|
||||||
invalidNameDialog.showDialog(newName)
|
invalidNameDialog.showDialog(newName)
|
||||||
} else {
|
} else {
|
||||||
history.addToHistory(new JS.HistoryLib.NameChanged(
|
Modules.History.addToHistory(new JS.HistoryLib.NameChanged(
|
||||||
objEditor.obj.name, objEditor.objType, newName
|
objEditor.obj.name, objEditor.objType, newName
|
||||||
))
|
))
|
||||||
Modules.Objects.renameObject(obj.name, newName)
|
Modules.Objects.renameObject(obj.name, newName)
|
||||||
|
@ -127,13 +127,17 @@ Popup.BaseDialog {
|
||||||
id: labelContentProperty
|
id: labelContentProperty
|
||||||
height: 30
|
height: 30
|
||||||
width: dlgProperties.width
|
width: dlgProperties.width
|
||||||
label: qsTr("Label content")
|
label: qsTranslate("prop", "labelContent")
|
||||||
model: [qsTr("null"), qsTr("name"), qsTr("name + value")]
|
model: [qsTr("null"), qsTr("name"), qsTr("name + value")]
|
||||||
property var idModel: ["null", "name", "name + value"]
|
property var idModel: ["null", "name", "name + value"]
|
||||||
icon: "common/label.svg"
|
icon: "common/label.svg"
|
||||||
currentIndex: idModel.indexOf(objEditor.obj.labelContent)
|
currentIndex: idModel.indexOf(objEditor.obj.labelContent)
|
||||||
onActivated: function(newIndex) {
|
onActivated: function(newIndex) {
|
||||||
if(idModel[newIndex] != objEditor.obj.labelContent) {
|
if(idModel[newIndex] != objEditor.obj.labelContent) {
|
||||||
|
Modules.History.addToHistory(new JS.HistoryLib.EditedProperty(
|
||||||
|
obj.name, objType, "labelContent",
|
||||||
|
objEditor.obj.labelContent, idModel[newIndex]
|
||||||
|
))
|
||||||
objEditor.obj.labelContent = idModel[newIndex]
|
objEditor.obj.labelContent = idModel[newIndex]
|
||||||
objEditor.obj.update()
|
objEditor.obj.update()
|
||||||
objectListList.update()
|
objectListList.update()
|
||||||
|
|
|
@ -104,7 +104,9 @@ Column {
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
let newObj = Modules.Objects.createNewRegisteredObject(modelData)
|
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()
|
objectLists.update()
|
||||||
|
|
||||||
let hasXProp = newObj.constructor.properties().hasOwnProperty('x')
|
let hasXProp = newObj.constructor.properties().hasOwnProperty('x')
|
||||||
|
|
|
@ -71,8 +71,8 @@ ScrollView {
|
||||||
id: typeVisibilityCheckBox
|
id: typeVisibilityCheckBox
|
||||||
checked: Modules.Objects.currentObjects[objType] != undefined ? Modules.Objects.currentObjects[objType].every(obj => obj.visible) : true
|
checked: Modules.Objects.currentObjects[objType] != undefined ? Modules.Objects.currentObjects[objType].every(obj => obj.visible) : true
|
||||||
onClicked: {
|
onClicked: {
|
||||||
for(var obj of Modules.Objects.currentObjects[objType]) obj.visible = this.checked
|
for(const obj of Modules.Objects.currentObjects[objType]) obj.visible = this.checked
|
||||||
for(var obj of objTypeList.editingRows) obj.objVisible = this.checked
|
for(const obj of objTypeList.editingRows) obj.objVisible = this.checked
|
||||||
objectListList.changed()
|
objectListList.changed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -72,7 +72,7 @@ Item {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.leftMargin: 5
|
anchors.leftMargin: 5
|
||||||
onClicked: {
|
onClicked: {
|
||||||
history.addToHistory(new JS.HistoryLib.EditedVisibility(
|
Modules.History.addToHistory(new JS.HistoryLib.EditedVisibility(
|
||||||
obj.name, obj.type, this.checked
|
obj.name, obj.type, this.checked
|
||||||
))
|
))
|
||||||
obj.visible = this.checked
|
obj.visible = this.checked
|
||||||
|
@ -212,7 +212,7 @@ Item {
|
||||||
selectedColor: obj.color
|
selectedColor: obj.color
|
||||||
title: qsTr("Pick new color for %1 %2").arg(obj.constructor.displayType()).arg(obj.name)
|
title: qsTr("Pick new color for %1 %2").arg(obj.constructor.displayType()).arg(obj.name)
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
history.addToHistory(new JS.HistoryLib.ColorChanged(
|
Modules.History.addToHistory(new JS.HistoryLib.ColorChanged(
|
||||||
obj.name, obj.type, obj.color, selectedColor.toString()
|
obj.name, obj.type, obj.color, selectedColor.toString()
|
||||||
))
|
))
|
||||||
obj.color = selectedColor.toString()
|
obj.color = selectedColor.toString()
|
||||||
|
@ -227,11 +227,11 @@ Item {
|
||||||
function deleteRecursively(object) {
|
function deleteRecursively(object) {
|
||||||
for(let toRemove of object.requiredBy)
|
for(let toRemove of object.requiredBy)
|
||||||
deleteRecursively(toRemove)
|
deleteRecursively(toRemove)
|
||||||
if(Modules.Objects.currentObjectsByName[object.name] != undefined) {
|
if(Modules.Objects.currentObjectsByName[object.name] !== undefined) {
|
||||||
// Object still exists
|
// Object still exists
|
||||||
// Temporary fix for objects require not being propertly updated.
|
// Temporary fix for objects require not being propertly updated.
|
||||||
object.requiredBy = []
|
object.requiredBy = []
|
||||||
history.addToHistory(new JS.HistoryLib.DeleteObject(
|
Modules.History.addToHistory(new JS.HistoryLib.DeleteObject(
|
||||||
object.name, object.type, object.export()
|
object.name, object.type, object.export()
|
||||||
))
|
))
|
||||||
Modules.Objects.deleteObject(object.name)
|
Modules.Objects.deleteObject(object.name)
|
||||||
|
|
|
@ -115,7 +115,7 @@ Item {
|
||||||
let obj = Modules.Objects.currentObjectsByName[objName]
|
let obj = Modules.Objects.currentObjectsByName[objName]
|
||||||
// Set values
|
// Set values
|
||||||
if(parent.userPickX && parent.userPickY) {
|
if(parent.userPickX && parent.userPickY) {
|
||||||
history.addToHistory(new JS.HistoryLib.EditedPosition(
|
Modules.History.addToHistory(new JS.HistoryLib.EditedPosition(
|
||||||
objName, objType, obj[propertyX], newValueX, obj[propertyY], newValueY
|
objName, objType, obj[propertyX], newValueX, obj[propertyY], newValueY
|
||||||
))
|
))
|
||||||
obj[propertyX] = newValueX
|
obj[propertyX] = newValueX
|
||||||
|
@ -124,7 +124,7 @@ Item {
|
||||||
objectLists.update()
|
objectLists.update()
|
||||||
pickerRoot.picked(obj)
|
pickerRoot.picked(obj)
|
||||||
} else if(parent.userPickX) {
|
} else if(parent.userPickX) {
|
||||||
history.addToHistory(new JS.HistoryLib.EditedProperty(
|
Modules.History.addToHistory(new JS.HistoryLib.EditedProperty(
|
||||||
objName, objType, propertyX, obj[propertyX], newValueX
|
objName, objType, propertyX, obj[propertyX], newValueX
|
||||||
))
|
))
|
||||||
obj[propertyX] = newValueX
|
obj[propertyX] = newValueX
|
||||||
|
@ -132,7 +132,7 @@ Item {
|
||||||
objectLists.update()
|
objectLists.update()
|
||||||
pickerRoot.picked(obj)
|
pickerRoot.picked(obj)
|
||||||
} else if(parent.userPickY) {
|
} else if(parent.userPickY) {
|
||||||
history.addToHistory(new JS.HistoryLib.EditedProperty(
|
Modules.History.addToHistory(new JS.HistoryLib.EditedProperty(
|
||||||
objName, objType, propertyY, obj[propertyY], newValueY
|
objName, objType, propertyY, obj[propertyY], newValueY
|
||||||
))
|
))
|
||||||
obj[propertyY] = newValueY
|
obj[propertyY] = newValueY
|
||||||
|
@ -285,7 +285,7 @@ Item {
|
||||||
const axisX = Modules.Canvas.axesSteps.x.value
|
const axisX = Modules.Canvas.axesSteps.x.value
|
||||||
const xpos = Modules.Canvas.px2x(picker.mouseX)
|
const xpos = Modules.Canvas.px2x(picker.mouseX)
|
||||||
if(snapToGridCheckbox.checked) {
|
if(snapToGridCheckbox.checked) {
|
||||||
if(canvas.logscalex) {
|
if(Modules.Settings.logscalex) {
|
||||||
// Calculate the logged power
|
// Calculate the logged power
|
||||||
let pow = Math.pow(10, Math.floor(Math.log10(xpos)))
|
let pow = Math.pow(10, Math.floor(Math.log10(xpos)))
|
||||||
return pow*Math.round(xpos/pow)
|
return pow*Math.round(xpos/pow)
|
||||||
|
|
|
@ -45,17 +45,17 @@ Popup {
|
||||||
property bool changelogNeedsFetching: true
|
property bool changelogNeedsFetching: true
|
||||||
|
|
||||||
onAboutToShow: if(changelogNeedsFetching) {
|
onAboutToShow: if(changelogNeedsFetching) {
|
||||||
Helper.fetchChangelog()
|
Helper.fetchChangelog().then((fetchedText) => {
|
||||||
}
|
changelogNeedsFetching = false
|
||||||
|
changelog.text = fetchedText
|
||||||
Connections {
|
|
||||||
target: Helper
|
|
||||||
function onChangelogFetched(chl) {
|
|
||||||
changelogNeedsFetching = false;
|
|
||||||
changelog.text = chl
|
|
||||||
changelogView.contentItem.implicitHeight = changelog.height
|
changelogView.contentItem.implicitHeight = changelog.height
|
||||||
// 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 {
|
ScrollView {
|
||||||
|
|
|
@ -141,7 +141,7 @@ Popup {
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
font.pixelSize: 14
|
font.pixelSize: 14
|
||||||
text: modelData.name
|
text: modelData.name
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.Wrap
|
||||||
clip: true
|
clip: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
module eu.ad5001.LogarithmPlotter.Popup
|
module eu.ad5001.LogarithmPlotter.Popup
|
||||||
|
|
||||||
BaseDialog 1.0 BaseDialog.qml
|
|
||||||
About 1.0 About.qml
|
|
||||||
Alert 1.0 Alert.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
|
FileDialog 1.0 FileDialog.qml
|
||||||
GreetScreen 1.0 GreetScreen.qml
|
GreetScreen 1.0 GreetScreen.qml
|
||||||
Changelog 1.0 Changelog.qml
|
|
||||||
ThanksTo 1.0 ThanksTo.qml
|
|
||||||
InsertCharacter 1.0 InsertCharacter.qml
|
InsertCharacter 1.0 InsertCharacter.qml
|
||||||
Preferences 1.0 Preferences.qml
|
Preferences 1.0 Preferences.qml
|
||||||
|
ThanksTo 1.0 ThanksTo.qml
|
||||||
|
|
|
@ -175,17 +175,17 @@ Item {
|
||||||
Icon {
|
Icon {
|
||||||
id: iconLabel
|
id: iconLabel
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.topMargin: icon == "" ? 0 : 3
|
anchors.topMargin: parent.icon == "" ? 0 : 3
|
||||||
source: control.visible && icon != "" ? "../icons/" + control.icon : ""
|
source: control.visible && parent.icon != "" ? "../icons/" + control.icon : ""
|
||||||
width: height
|
width: height
|
||||||
height: icon == "" || !visible ? 0 : 24
|
height: parent.icon == "" || !visible ? 0 : 24
|
||||||
color: sysPalette.windowText
|
color: sysPalette.windowText
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
id: labelItem
|
id: labelItem
|
||||||
anchors.left: iconLabel.right
|
anchors.left: iconLabel.right
|
||||||
anchors.leftMargin: icon == "" ? 0 : 5
|
anchors.leftMargin: parent.icon == "" ? 0 : 5
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
height: parent.height
|
height: parent.height
|
||||||
width: Math.max(85, implicitWidth)
|
width: Math.max(85, implicitWidth)
|
||||||
|
@ -221,9 +221,9 @@ Item {
|
||||||
focus: true
|
focus: true
|
||||||
selectByMouse: true
|
selectByMouse: true
|
||||||
|
|
||||||
property bool autocompleteEnabled: Helper.getSettingBool("autocompletion.enabled")
|
property bool autocompleteEnabled: Helper.getSetting("autocompletion.enabled")
|
||||||
property bool syntaxHighlightingEnabled: Helper.getSettingBool("expression_editor.colorize")
|
property bool syntaxHighlightingEnabled: Helper.getSetting("expression_editor.colorize")
|
||||||
property bool autoClosing: Helper.getSettingBool("expression_editor.autoclose")
|
property bool autoClosing: Helper.getSetting("expression_editor.autoclose")
|
||||||
property var tokens: autocompleteEnabled || syntaxHighlightingEnabled ? parent.tokens(text) : []
|
property var tokens: autocompleteEnabled || syntaxHighlightingEnabled ? parent.tokens(text) : []
|
||||||
|
|
||||||
Keys.priority: Keys.BeforeItem // Required for knowing which key the user presses.
|
Keys.priority: Keys.BeforeItem // Required for knowing which key the user presses.
|
||||||
|
@ -231,8 +231,8 @@ Item {
|
||||||
onEditingFinished: {
|
onEditingFinished: {
|
||||||
if(insertButton.focus || insertPopup.focus) return
|
if(insertButton.focus || insertPopup.focus) return
|
||||||
let value = text
|
let value = text
|
||||||
if(value != "" && value.toString() != defValue) {
|
if(value != "" && value.toString() != parent.defValue) {
|
||||||
let expr = parse(value)
|
let expr = parent.parse(value)
|
||||||
if(expr != null) {
|
if(expr != null) {
|
||||||
control.changed(expr)
|
control.changed(expr)
|
||||||
defValue = expr.toEditableString()
|
defValue = expr.toEditableString()
|
||||||
|
@ -280,10 +280,10 @@ Item {
|
||||||
acPopupContent.itemSelected = 0
|
acPopupContent.itemSelected = 0
|
||||||
|
|
||||||
|
|
||||||
if(event.text in openAndCloseMatches && autoClosing) {
|
if(event.text in parent.openAndCloseMatches && autoClosing) {
|
||||||
let start = selectionStart
|
let start = selectionStart
|
||||||
insert(selectionStart, event.text)
|
insert(selectionStart, event.text)
|
||||||
insert(selectionEnd, openAndCloseMatches[event.text])
|
insert(selectionEnd, parent.openAndCloseMatches[event.text])
|
||||||
cursorPosition = start+1
|
cursorPosition = start+1
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
}
|
}
|
||||||
|
@ -600,7 +600,7 @@ Item {
|
||||||
*/
|
*/
|
||||||
function colorize(tokenList) {
|
function colorize(tokenList) {
|
||||||
let parsedText = ""
|
let parsedText = ""
|
||||||
let scheme = colorSchemes[Helper.getSettingInt("expression_editor.color_scheme")]
|
let scheme = colorSchemes[Helper.getSetting("expression_editor.color_scheme")]
|
||||||
for(let token of tokenList) {
|
for(let token of tokenList) {
|
||||||
switch(token.type) {
|
switch(token.type) {
|
||||||
case JS.Parsing.TokenType.VARIABLE:
|
case JS.Parsing.TokenType.VARIABLE:
|
||||||
|
|
|
@ -37,7 +37,7 @@ Item {
|
||||||
Emitted when the value of the text has been changed.
|
Emitted when the value of the text has been changed.
|
||||||
The corresponding handler is \c onChanged.
|
The corresponding handler is \c onChanged.
|
||||||
*/
|
*/
|
||||||
signal changed(string newValue)
|
signal changed(var newValue)
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
\qmlproperty bool TextSetting::isInt
|
\qmlproperty bool TextSetting::isInt
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
module eu.ad5001.LogarithmPlotter.Setting
|
module eu.ad5001.LogarithmPlotter.Setting
|
||||||
|
|
||||||
|
AutocompletionCategory 1.0 AutocompletionCategory.qml
|
||||||
ComboBoxSetting 1.0 ComboBoxSetting.qml
|
ComboBoxSetting 1.0 ComboBoxSetting.qml
|
||||||
|
ExpressionEditor 1.0 ExpressionEditor.qml
|
||||||
Icon 1.0 Icon.qml
|
Icon 1.0 Icon.qml
|
||||||
ListSetting 1.0 ListSetting.qml
|
ListSetting 1.0 ListSetting.qml
|
||||||
TextSetting 1.0 TextSetting.qml
|
TextSetting 1.0 TextSetting.qml
|
||||||
ExpressionEditor 1.0 ExpressionEditor.qml
|
|
||||||
AutocompletionCategory 1.0 AutocompletionCategory.qml
|
|
||||||
|
|
|
@ -44,25 +44,25 @@ ScrollView {
|
||||||
Zoom on the x axis of the diagram, provided from settings.
|
Zoom on the x axis of the diagram, provided from settings.
|
||||||
\sa Settings
|
\sa Settings
|
||||||
*/
|
*/
|
||||||
property double xzoom: Helper.getSettingInt('default_graph.xzoom')
|
property double xzoom: Helper.getSetting('default_graph.xzoom')
|
||||||
/*!
|
/*!
|
||||||
\qmlproperty double Settings::yzoom
|
\qmlproperty double Settings::yzoom
|
||||||
Zoom on the y axis of the diagram, provided from settings.
|
Zoom on the y axis of the diagram, provided from settings.
|
||||||
\sa Settings
|
\sa Settings
|
||||||
*/
|
*/
|
||||||
property double yzoom: Helper.getSettingInt('default_graph.yzoom')
|
property double yzoom: Helper.getSetting('default_graph.yzoom')
|
||||||
/*!
|
/*!
|
||||||
\qmlproperty double Settings::xmin
|
\qmlproperty double Settings::xmin
|
||||||
Minimum x of the diagram, provided from settings.
|
Minimum x of the diagram, provided from settings.
|
||||||
\sa Settings
|
\sa Settings
|
||||||
*/
|
*/
|
||||||
property double xmin: Helper.getSettingInt('default_graph.xmin')
|
property double xmin: Helper.getSetting('default_graph.xmin')
|
||||||
/*!
|
/*!
|
||||||
\qmlproperty double Settings::ymax
|
\qmlproperty double Settings::ymax
|
||||||
Maximum y of the diagram, provided from settings.
|
Maximum y of the diagram, provided from settings.
|
||||||
\sa Settings
|
\sa Settings
|
||||||
*/
|
*/
|
||||||
property double ymax: Helper.getSettingInt('default_graph.ymax')
|
property double ymax: Helper.getSetting('default_graph.ymax')
|
||||||
/*!
|
/*!
|
||||||
\qmlproperty string Settings::xaxisstep
|
\qmlproperty string Settings::xaxisstep
|
||||||
Step of the x axis graduation, provided from settings.
|
Step of the x axis graduation, provided from settings.
|
||||||
|
@ -93,39 +93,34 @@ ScrollView {
|
||||||
Width of lines that will be drawn into the canvas, provided from settings.
|
Width of lines that will be drawn into the canvas, provided from settings.
|
||||||
\sa Settings
|
\sa Settings
|
||||||
*/
|
*/
|
||||||
property double linewidth: Helper.getSettingInt('default_graph.linewidth')
|
property double linewidth: Helper.getSetting('default_graph.linewidth')
|
||||||
/*!
|
/*!
|
||||||
\qmlproperty double Settings::textsize
|
\qmlproperty double Settings::textsize
|
||||||
Font size of the text that will be drawn into the canvas, provided from settings.
|
Font size of the text that will be drawn into the canvas, provided from settings.
|
||||||
\sa Settings
|
\sa Settings
|
||||||
*/
|
*/
|
||||||
property double textsize: Helper.getSettingInt('default_graph.textsize')
|
property double textsize: Helper.getSetting('default_graph.textsize')
|
||||||
/*!
|
/*!
|
||||||
\qmlproperty bool Settings::logscalex
|
\qmlproperty bool Settings::logscalex
|
||||||
true if the canvas should be in logarithmic mode, false otherwise.
|
true if the canvas should be in logarithmic mode, false otherwise.
|
||||||
Provided from settings.
|
Provided from settings.
|
||||||
\sa Settings
|
\sa Settings
|
||||||
*/
|
*/
|
||||||
property bool logscalex: Helper.getSettingBool('default_graph.logscalex')
|
property bool logscalex: Helper.getSetting('default_graph.logscalex')
|
||||||
/*!
|
/*!
|
||||||
\qmlproperty bool Settings::showxgrad
|
\qmlproperty bool Settings::showxgrad
|
||||||
true if the x graduation should be shown, false otherwise.
|
true if the x graduation should be shown, false otherwise.
|
||||||
Provided from settings.
|
Provided from settings.
|
||||||
\sa Settings
|
\sa Settings
|
||||||
*/
|
*/
|
||||||
property bool showxgrad: Helper.getSettingBool('default_graph.showxgrad')
|
property bool showxgrad: Helper.getSetting('default_graph.showxgrad')
|
||||||
/*!
|
/*!
|
||||||
\qmlproperty bool Settings::showygrad
|
\qmlproperty bool Settings::showygrad
|
||||||
true if the y graduation should be shown, false otherwise.
|
true if the y graduation should be shown, false otherwise.
|
||||||
Provided from settings.
|
Provided from settings.
|
||||||
\sa Settings
|
\sa Settings
|
||||||
*/
|
*/
|
||||||
property bool showygrad: Helper.getSettingBool('default_graph.showygrad')
|
property bool showygrad: Helper.getSetting('default_graph.showygrad')
|
||||||
/*!
|
|
||||||
\qmlproperty bool Settings::saveFilename
|
|
||||||
Path of the currently opened file. Empty if no file is opened.
|
|
||||||
*/
|
|
||||||
property string saveFilename: ""
|
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
spacing: 10
|
spacing: 10
|
||||||
|
@ -136,15 +131,18 @@ ScrollView {
|
||||||
id: fdiag
|
id: fdiag
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
var filePath = fdiag.currentFile.toString().substr(7)
|
var filePath = fdiag.currentFile.toString().substr(7)
|
||||||
settings.saveFilename = filePath
|
Modules.Settings.set("saveFilename", filePath)
|
||||||
if(exportMode) {
|
if(exportMode) {
|
||||||
Modules.IO.saveDiagram(filePath)
|
Modules.IO.saveDiagram(filePath)
|
||||||
} else {
|
} else {
|
||||||
Modules.IO.loadDiagram(filePath)
|
Modules.IO.loadDiagram(filePath)
|
||||||
if(xAxisLabel.find(settings.xlabel) == -1) xAxisLabel.model.append({text: settings.xlabel})
|
// Adding labels.
|
||||||
xAxisLabel.editText = settings.xlabel
|
if(xAxisLabel.find(Modules.Settings.xlabel) === -1)
|
||||||
if(yAxisLabel.find(settings.ylabel) == -1) yAxisLabel.model.append({text: settings.ylabel})
|
xAxisLabel.model.append({text: Modules.Settings.xlabel})
|
||||||
yAxisLabel.editText = settings.ylabel
|
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
|
min: 0.1
|
||||||
icon: "settings/xzoom.svg"
|
icon: "settings/xzoom.svg"
|
||||||
width: settings.settingWidth
|
width: settings.settingWidth
|
||||||
value: settings.xzoom.toFixed(2)
|
|
||||||
onChanged: function(newValue) {
|
onChanged: function(newValue) {
|
||||||
settings.xzoom = newValue
|
Modules.Settings.set("xzoom", newValue, true)
|
||||||
settings.changed()
|
settings.changed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function update(newValue) {
|
||||||
|
value = Modules.Settings.xzoom.toFixed(2)
|
||||||
|
maxX.update()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Setting.TextSetting {
|
Setting.TextSetting {
|
||||||
|
@ -173,11 +176,16 @@ ScrollView {
|
||||||
label: qsTr("Y Zoom")
|
label: qsTr("Y Zoom")
|
||||||
icon: "settings/yzoom.svg"
|
icon: "settings/yzoom.svg"
|
||||||
width: settings.settingWidth
|
width: settings.settingWidth
|
||||||
value: settings.yzoom.toFixed(2)
|
|
||||||
onChanged: function(newValue) {
|
onChanged: function(newValue) {
|
||||||
settings.yzoom = newValue
|
Modules.Settings.set("yzoom", newValue, true)
|
||||||
settings.changed()
|
settings.changed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function update(newValue) {
|
||||||
|
value = Modules.Settings.yzoom.toFixed(2)
|
||||||
|
minY.update()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Positioning the graph
|
// Positioning the graph
|
||||||
|
@ -189,14 +197,18 @@ ScrollView {
|
||||||
label: qsTr("Min X")
|
label: qsTr("Min X")
|
||||||
icon: "settings/xmin.svg"
|
icon: "settings/xmin.svg"
|
||||||
width: settings.settingWidth
|
width: settings.settingWidth
|
||||||
defValue: settings.xmin
|
|
||||||
onChanged: function(newValue) {
|
onChanged: function(newValue) {
|
||||||
if(parseFloat(maxX.value) > newValue) {
|
Modules.Settings.set("xmin", newValue, true)
|
||||||
settings.xmin = newValue
|
|
||||||
settings.changed()
|
settings.changed()
|
||||||
} else {
|
|
||||||
alert.show("Minimum x value must be inferior to maximum.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function update(newValue) {
|
||||||
|
let newVal = Modules.Settings.xmin
|
||||||
|
if(newVal > 1e-5)
|
||||||
|
newVal = newVal.toDecimalPrecision(8)
|
||||||
|
value = newVal
|
||||||
|
maxX.update()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,11 +220,16 @@ ScrollView {
|
||||||
label: qsTr("Max Y")
|
label: qsTr("Max Y")
|
||||||
icon: "settings/ymax.svg"
|
icon: "settings/ymax.svg"
|
||||||
width: settings.settingWidth
|
width: settings.settingWidth
|
||||||
defValue: settings.ymax
|
|
||||||
onChanged: function(newValue) {
|
onChanged: function(newValue) {
|
||||||
settings.ymax = newValue
|
Modules.Settings.set("ymax", newValue, true)
|
||||||
settings.changed()
|
settings.changed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
value = Modules.Settings.ymax
|
||||||
|
minY.update()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Setting.TextSetting {
|
Setting.TextSetting {
|
||||||
|
@ -223,15 +240,24 @@ ScrollView {
|
||||||
label: qsTr("Max X")
|
label: qsTr("Max X")
|
||||||
icon: "settings/xmax.svg"
|
icon: "settings/xmax.svg"
|
||||||
width: settings.settingWidth
|
width: settings.settingWidth
|
||||||
defValue: Modules.Canvas.px2x(canvas.width).toFixed(2)
|
|
||||||
onChanged: function(xvaluemax) {
|
onChanged: function(xvaluemax) {
|
||||||
if(xvaluemax > settings.xmin) {
|
if(xvaluemax > Modules.Settings.xmin) {
|
||||||
settings.xzoom = settings.xzoom * canvas.width/(Modules.Canvas.x2px(xvaluemax)) // Adjusting zoom to fit. = (end)/(px of current point)
|
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()
|
settings.changed()
|
||||||
} else {
|
} else {
|
||||||
alert.show("Maximum x value must be superior to minimum.")
|
alert.show("Maximum x value must be superior to minimum.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
let newVal = Modules.Canvas.px2x(canvas.width)
|
||||||
|
if(newVal > 1e-5)
|
||||||
|
newVal = newVal.toDecimalPrecision(8)
|
||||||
|
value = newVal
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Setting.TextSetting {
|
Setting.TextSetting {
|
||||||
|
@ -242,15 +268,21 @@ ScrollView {
|
||||||
label: qsTr("Min Y")
|
label: qsTr("Min Y")
|
||||||
icon: "settings/ymin.svg"
|
icon: "settings/ymin.svg"
|
||||||
width: settings.settingWidth
|
width: settings.settingWidth
|
||||||
defValue: Modules.Canvas.px2y(canvas.height).toFixed(2)
|
|
||||||
onChanged: function(yvaluemin) {
|
onChanged: function(yvaluemin) {
|
||||||
if(yvaluemin < settings.ymax) {
|
if(yvaluemin < settings.ymax) {
|
||||||
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()
|
settings.changed()
|
||||||
} else {
|
} else {
|
||||||
alert.show("Minimum y value must be inferior to maximum.")
|
alert.show("Minimum y value must be inferior to maximum.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
value = Modules.Canvas.px2y(canvas.height).toDecimalPrecision(8)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Setting.TextSetting {
|
Setting.TextSetting {
|
||||||
|
@ -260,12 +292,16 @@ ScrollView {
|
||||||
label: qsTr("X Axis Step")
|
label: qsTr("X Axis Step")
|
||||||
icon: "settings/xaxisstep.svg"
|
icon: "settings/xaxisstep.svg"
|
||||||
width: settings.settingWidth
|
width: settings.settingWidth
|
||||||
defValue: settings.xaxisstep
|
|
||||||
visible: !settings.logscalex
|
|
||||||
onChanged: function(newValue) {
|
onChanged: function(newValue) {
|
||||||
settings.xaxisstep = newValue
|
Modules.Settings.set("xaxisstep", newValue, true)
|
||||||
settings.changed()
|
settings.changed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
value = Modules.Settings.xaxisstep
|
||||||
|
visible = !Modules.Settings.logscalex
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Setting.TextSetting {
|
Setting.TextSetting {
|
||||||
|
@ -275,11 +311,13 @@ ScrollView {
|
||||||
label: qsTr("Y Axis Step")
|
label: qsTr("Y Axis Step")
|
||||||
icon: "settings/yaxisstep.svg"
|
icon: "settings/yaxisstep.svg"
|
||||||
width: settings.settingWidth
|
width: settings.settingWidth
|
||||||
defValue: settings.yaxisstep
|
|
||||||
onChanged: function(newValue) {
|
onChanged: function(newValue) {
|
||||||
settings.yaxisstep = newValue
|
Modules.Settings.set("yaxisstep", newValue, true)
|
||||||
settings.changed()
|
settings.changed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function update() { value = Modules.Settings.yaxisstep }
|
||||||
}
|
}
|
||||||
|
|
||||||
Setting.TextSetting {
|
Setting.TextSetting {
|
||||||
|
@ -290,11 +328,13 @@ ScrollView {
|
||||||
min: 1
|
min: 1
|
||||||
icon: "settings/linewidth.svg"
|
icon: "settings/linewidth.svg"
|
||||||
width: settings.settingWidth
|
width: settings.settingWidth
|
||||||
defValue: settings.linewidth
|
|
||||||
onChanged: function(newValue) {
|
onChanged: function(newValue) {
|
||||||
settings.linewidth = newValue
|
Modules.Settings.set("linewidth", newValue, true)
|
||||||
settings.changed()
|
settings.changed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function update() { value = Modules.Settings.linewidth }
|
||||||
}
|
}
|
||||||
|
|
||||||
Setting.TextSetting {
|
Setting.TextSetting {
|
||||||
|
@ -305,11 +345,13 @@ ScrollView {
|
||||||
min: 1
|
min: 1
|
||||||
icon: "settings/textsize.svg"
|
icon: "settings/textsize.svg"
|
||||||
width: settings.settingWidth
|
width: settings.settingWidth
|
||||||
defValue: settings.textsize
|
|
||||||
onChanged: function(newValue) {
|
onChanged: function(newValue) {
|
||||||
settings.textsize = newValue
|
Modules.Settings.set("textsize", newValue, true)
|
||||||
settings.changed()
|
settings.changed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function update() { value = Modules.Settings.textsize }
|
||||||
}
|
}
|
||||||
|
|
||||||
Setting.ComboBoxSetting {
|
Setting.ComboBoxSetting {
|
||||||
|
@ -318,24 +360,31 @@ ScrollView {
|
||||||
width: settings.settingWidth
|
width: settings.settingWidth
|
||||||
label: qsTr('X Label')
|
label: qsTr('X Label')
|
||||||
icon: "settings/xlabel.svg"
|
icon: "settings/xlabel.svg"
|
||||||
|
editable: true
|
||||||
model: ListModel {
|
model: ListModel {
|
||||||
ListElement { text: "" }
|
ListElement { text: "" }
|
||||||
ListElement { text: "x" }
|
ListElement { text: "x" }
|
||||||
ListElement { text: "ω (rad/s)" }
|
ListElement { text: "ω (rad/s)" }
|
||||||
}
|
}
|
||||||
currentIndex: find(settings.xlabel)
|
|
||||||
editable: true
|
|
||||||
onAccepted: function(){
|
onAccepted: function(){
|
||||||
editText = JS.Utils.parseName(editText, false)
|
editText = JS.Utils.parseName(editText, false)
|
||||||
if(find(editText) === -1) model.append({text: editText})
|
if(find(editText) === -1) model.append({text: editText})
|
||||||
settings.xlabel = editText
|
currentIndex = find(editText)
|
||||||
|
Modules.Settings.set("xlabel", editText, true)
|
||||||
settings.changed()
|
settings.changed()
|
||||||
}
|
}
|
||||||
|
|
||||||
onActivated: function(selectedId) {
|
onActivated: function(selectedId) {
|
||||||
settings.xlabel = model.get(selectedId).text
|
Modules.Settings.set("xlabel", model.get(selectedId).text, true)
|
||||||
settings.changed()
|
settings.changed()
|
||||||
}
|
}
|
||||||
Component.onCompleted: editText = settings.xlabel
|
|
||||||
|
function update() {
|
||||||
|
editText = Modules.Settings.xlabel
|
||||||
|
if(find(editText) === -1) model.append({text: editText})
|
||||||
|
currentIndex = find(editText)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Setting.ComboBoxSetting {
|
Setting.ComboBoxSetting {
|
||||||
|
@ -344,6 +393,7 @@ ScrollView {
|
||||||
width: settings.settingWidth
|
width: settings.settingWidth
|
||||||
label: qsTr('Y Label')
|
label: qsTr('Y Label')
|
||||||
icon: "settings/ylabel.svg"
|
icon: "settings/ylabel.svg"
|
||||||
|
editable: true
|
||||||
model: ListModel {
|
model: ListModel {
|
||||||
ListElement { text: "" }
|
ListElement { text: "" }
|
||||||
ListElement { text: "y" }
|
ListElement { text: "y" }
|
||||||
|
@ -352,39 +402,52 @@ ScrollView {
|
||||||
ListElement { text: "φ (deg)" }
|
ListElement { text: "φ (deg)" }
|
||||||
ListElement { text: "φ (rad)" }
|
ListElement { text: "φ (rad)" }
|
||||||
}
|
}
|
||||||
currentIndex: find(settings.ylabel)
|
|
||||||
editable: true
|
|
||||||
onAccepted: function(){
|
onAccepted: function(){
|
||||||
editText = JS.Utils.parseName(editText, false)
|
editText = JS.Utils.parseName(editText, false)
|
||||||
if (find(editText) === -1) model.append({text: editText, yaxisstep: root.yaxisstep})
|
if(find(editText) === -1) model.append({text: editText})
|
||||||
settings.ylabel = editText
|
currentIndex = find(editText)
|
||||||
|
Modules.Settings.set("ylabel", editText, true)
|
||||||
settings.changed()
|
settings.changed()
|
||||||
}
|
}
|
||||||
|
|
||||||
onActivated: function(selectedId) {
|
onActivated: function(selectedId) {
|
||||||
settings.ylabel = model.get(selectedId).text
|
Modules.Settings.set("ylabel", model.get(selectedId).text, true)
|
||||||
settings.changed()
|
settings.changed()
|
||||||
}
|
}
|
||||||
Component.onCompleted: editText = settings.ylabel
|
|
||||||
|
function update() {
|
||||||
|
editText = Modules.Settings.ylabel
|
||||||
|
if(find(editText) === -1) model.append({text: editText})
|
||||||
|
currentIndex = find(editText)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: logScaleX
|
id: logScaleX
|
||||||
checked: settings.logscalex
|
|
||||||
text: qsTr('X Log scale')
|
text: qsTr('X Log scale')
|
||||||
onClicked: {
|
onClicked: {
|
||||||
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()
|
settings.changed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
checked = Modules.Settings.logscalex
|
||||||
|
xAxisStep.update()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: showXGrad
|
id: showXGrad
|
||||||
checked: settings.showxgrad
|
|
||||||
text: qsTr('Show X graduation')
|
text: qsTr('Show X graduation')
|
||||||
onClicked: {
|
onClicked: {
|
||||||
settings.showxgrad = checked
|
Modules.Settings.set("showxgrad", checked, true)
|
||||||
settings.changed()
|
settings.changed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function update() { checked = Modules.Settings.showxgrad }
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
|
@ -392,9 +455,10 @@ ScrollView {
|
||||||
checked: settings.showygrad
|
checked: settings.showygrad
|
||||||
text: qsTr('Show Y graduation')
|
text: qsTr('Show Y graduation')
|
||||||
onClicked: {
|
onClicked: {
|
||||||
settings.showygrad = checked
|
Modules.Settings.set("showygrad", checked, true)
|
||||||
settings.changed()
|
settings.changed()
|
||||||
}
|
}
|
||||||
|
function update() { checked = Modules.Settings.showygrad }
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
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.
|
Saves the current canvas in the opened file. If no file is currently opened, prompts to pick a save location.
|
||||||
*/
|
*/
|
||||||
function save() {
|
function save() {
|
||||||
if(settings.saveFilename == "") {
|
if(Modules.Settings.saveFilename == "") {
|
||||||
saveAs()
|
saveAs()
|
||||||
} else {
|
} else {
|
||||||
Modules.IO.saveDiagram(settings.saveFilename)
|
Modules.IO.saveDiagram(Modules.Settings.saveFilename)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -464,4 +528,30 @@ ScrollView {
|
||||||
fdiag.exportMode = false
|
fdiag.exportMode = false
|
||||||
fdiag.open()
|
fdiag.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializing the settings
|
||||||
|
*/
|
||||||
|
Component.onCompleted: function() {
|
||||||
|
const matchedElements = new Map([
|
||||||
|
["xzoom", zoomX],
|
||||||
|
["yzoom", zoomY],
|
||||||
|
["xmin", minX],
|
||||||
|
["ymax", maxY],
|
||||||
|
["xaxisstep", xAxisStep],
|
||||||
|
["yaxisstep", yAxisStep],
|
||||||
|
["xlabel", xAxisLabel],
|
||||||
|
["ylabel", yAxisLabel],
|
||||||
|
["linewidth", lineWidth],
|
||||||
|
["textsize", textSize],
|
||||||
|
["logscalex", logScaleX],
|
||||||
|
["showxgrad", showXGrad],
|
||||||
|
["showygrad", showYGrad]
|
||||||
|
])
|
||||||
|
Modules.Settings.on("changed", (evt) => {
|
||||||
|
if(matchedElements.has(evt.property))
|
||||||
|
matchedElements.get(evt.property).update()
|
||||||
|
})
|
||||||
|
Modules.Settings.initialize({ helper: Helper })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
|
||||||
import eu.ad5001.LogarithmPlotter.Setting 1.0 as Setting
|
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
\qmltype ViewPositionChangeOverlay
|
\qmltype ViewPositionChangeOverlay
|
||||||
|
@ -81,7 +79,7 @@ Item {
|
||||||
property int prevY
|
property int prevY
|
||||||
/*!
|
/*!
|
||||||
\qmlproperty double ViewPositionChangeOverlay::baseZoomMultiplier
|
\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
|
property double baseZoomMultiplier: 0.1
|
||||||
|
|
||||||
|
@ -91,15 +89,15 @@ Item {
|
||||||
cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor
|
cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor
|
||||||
property int positionChangeTimer: 0
|
property int positionChangeTimer: 0
|
||||||
|
|
||||||
function updatePosition(deltaX, deltaY) {
|
function updatePosition(deltaX, deltaY, isEnd) {
|
||||||
const unauthorized = [NaN, Infinity, -Infinity]
|
const unauthorized = [NaN, Infinity, -Infinity]
|
||||||
const xmin = (Modules.Canvas.px2x(Modules.Canvas.x2px(settingsInstance.xmin)-deltaX))
|
const xmin = (Modules.Canvas.px2x(Modules.Canvas.x2px(Modules.Settings.xmin)-deltaX))
|
||||||
const ymax = settingsInstance.ymax + deltaY/canvas.yzoom
|
const ymax = Modules.Settings.ymax + deltaY/Modules.Settings.yzoom
|
||||||
if(!unauthorized.includes(xmin))
|
if(!unauthorized.includes(xmin))
|
||||||
settingsInstance.xmin = xmin
|
Modules.Settings.set("xmin", xmin, isEnd)
|
||||||
if(!unauthorized.includes(ymax))
|
if(!unauthorized.includes(ymax))
|
||||||
settingsInstance.ymax = ymax.toFixed(4)
|
Modules.Settings.set("ymax", ymax.toDecimalPrecision(6), isEnd)
|
||||||
settingsInstance.changed()
|
Modules.Canvas.requestPaint()
|
||||||
parent.positionChanged(deltaX, deltaY)
|
parent.positionChanged(deltaX, deltaY)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -113,9 +111,9 @@ Item {
|
||||||
onPositionChanged: function(mouse) {
|
onPositionChanged: function(mouse) {
|
||||||
positionChangeTimer++
|
positionChangeTimer++
|
||||||
if(positionChangeTimer == 3) {
|
if(positionChangeTimer == 3) {
|
||||||
let deltaX = mouse.x - prevX
|
let deltaX = mouse.x - parent.prevX
|
||||||
let deltaY = mouse.y - prevY
|
let deltaY = mouse.y - parent.prevY
|
||||||
updatePosition(deltaX, deltaY)
|
updatePosition(deltaX, deltaY, false)
|
||||||
prevX = mouse.x
|
prevX = mouse.x
|
||||||
prevY = mouse.y
|
prevY = mouse.y
|
||||||
positionChangeTimer = 0
|
positionChangeTimer = 0
|
||||||
|
@ -123,35 +121,35 @@ Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
onReleased: function(mouse) {
|
onReleased: function(mouse) {
|
||||||
let deltaX = mouse.x - prevX
|
let deltaX = mouse.x - parent.prevX
|
||||||
let deltaY = mouse.y - prevY
|
let deltaY = mouse.y - parent.prevY
|
||||||
updatePosition(deltaX, deltaY)
|
updatePosition(deltaX, deltaY, true)
|
||||||
parent.endPositionChange(deltaX, deltaY)
|
parent.endPositionChange(deltaX, deltaY)
|
||||||
}
|
}
|
||||||
|
|
||||||
onWheel: function(wheel) {
|
onWheel: function(wheel) {
|
||||||
// Scrolling
|
// Scrolling
|
||||||
let scrollSteps = Math.round(wheel.angleDelta.y / 120)
|
let scrollSteps = Math.round(wheel.angleDelta.y / 120)
|
||||||
let zoomMultiplier = Math.pow(1+baseZoomMultiplier, Math.abs(scrollSteps))
|
let zoomMultiplier = Math.pow(1+parent.baseZoomMultiplier, Math.abs(scrollSteps))
|
||||||
// Avoid floating-point rounding errors by removing the zoom *after*
|
// Avoid floating-point rounding errors by removing the zoom *after*
|
||||||
let xZoomDelta = (settingsInstance.xzoom*zoomMultiplier - settingsInstance.xzoom)
|
let xZoomDelta = (Modules.Settings.xzoom*zoomMultiplier - Modules.Settings.xzoom)
|
||||||
let yZoomDelta = (settingsInstance.yzoom*zoomMultiplier - settingsInstance.yzoom)
|
let yZoomDelta = (Modules.Settings.yzoom*zoomMultiplier - Modules.Settings.yzoom)
|
||||||
if(scrollSteps < 0) { // Negative scroll
|
if(scrollSteps < 0) { // Negative scroll
|
||||||
xZoomDelta *= -1
|
xZoomDelta *= -1
|
||||||
yZoomDelta *= -1
|
yZoomDelta *= -1
|
||||||
}
|
}
|
||||||
let newXZoom = (settingsInstance.xzoom+xZoomDelta).toFixed(0)
|
let newXZoom = (Modules.Settings.xzoom+xZoomDelta).toDecimalPrecision(0)
|
||||||
let newYZoom = (settingsInstance.yzoom+yZoomDelta).toFixed(0)
|
let newYZoom = (Modules.Settings.yzoom+yZoomDelta).toDecimalPrecision(0)
|
||||||
// Check if we need to have more precision
|
// Check if we need to have more precision
|
||||||
if(newXZoom < 10)
|
if(newXZoom < 10)
|
||||||
newXZoom = (settingsInstance.xzoom+xZoomDelta).toFixed(4)
|
newXZoom = (Modules.Settings.xzoom+xZoomDelta).toDecimalPrecision(4)
|
||||||
if(newYZoom < 10)
|
if(newYZoom < 10)
|
||||||
newYZoom = (settingsInstance.yzoom+yZoomDelta).toFixed(4)
|
newYZoom = (Modules.Settings.yzoom+yZoomDelta).toDecimalPrecision(4)
|
||||||
if(newXZoom > 0.5)
|
if(newXZoom > 0.5)
|
||||||
settingsInstance.xzoom = newXZoom
|
Modules.Settings.set("xzoom", newXZoom)
|
||||||
if(newYZoom > 0.5)
|
if(newYZoom > 0.5)
|
||||||
settingsInstance.yzoom = newYZoom
|
Modules.Settings.set("yzoom", newYZoom)
|
||||||
settingsInstance.changed()
|
Modules.Canvas.requestPaint()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
module eu.ad5001.LogarithmPlotter
|
module eu.ad5001.LogarithmPlotter
|
||||||
|
|
||||||
|
AppMenuBar 1.0 AppMenuBar.qml
|
||||||
|
LogGraphCanvas 1.0 LogGraphCanvas.qml
|
||||||
|
PickLocationOverlay 1.0 PickLocationOverlay.qml
|
||||||
Settings 1.0 Settings.qml
|
Settings 1.0 Settings.qml
|
||||||
Alert 1.0 Alert.qml
|
ViewPositionChangeOverlay 1.0 ViewPositionChangeOverlay.qml
|
|
@ -19,13 +19,16 @@
|
||||||
from os import path, environ, makedirs
|
from os import path, environ, makedirs
|
||||||
from platform import system
|
from platform import system
|
||||||
from json import load, dumps
|
from json import load, dumps
|
||||||
|
from shutil import which
|
||||||
|
|
||||||
from PySide6.QtCore import QLocale, QTranslator
|
from PySide6.QtCore import QLocale, QTranslator
|
||||||
|
|
||||||
DEFAULT_SETTINGS = {
|
DEFAULT_SETTINGS = {
|
||||||
"check_for_updates": True,
|
"check_for_updates": True,
|
||||||
"reset_redo_stack": True,
|
"reset_redo_stack": True,
|
||||||
"last_install_greet": "0",
|
"last_install_greet": "0",
|
||||||
"enable_latex": False,
|
"enable_latex": which("latex") is not None and which("dvipng") is not None,
|
||||||
|
"enable_latex_async": True,
|
||||||
"expression_editor": {
|
"expression_editor": {
|
||||||
"autoclose": True,
|
"autoclose": True,
|
||||||
"colorize": True,
|
"colorize": True,
|
||||||
|
|
|
@ -19,10 +19,12 @@
|
||||||
from PySide6.QtCore import QtMsgType, qInstallMessageHandler, QMessageLogContext
|
from PySide6.QtCore import QtMsgType, qInstallMessageHandler, QMessageLogContext
|
||||||
from math import ceil, log10
|
from math import ceil, log10
|
||||||
from os import path
|
from os import path
|
||||||
|
from re import compile
|
||||||
|
|
||||||
CURRENT_PATH = path.dirname(path.realpath(__file__))
|
CURRENT_PATH = path.dirname(path.realpath(__file__))
|
||||||
SOURCEMAP_PATH = path.realpath(f"{CURRENT_PATH}/../qml/eu/ad5001/LogarithmPlotter/js/index.mjs.map")
|
SOURCEMAP_PATH = path.realpath(f"{CURRENT_PATH}/../qml/eu/ad5001/LogarithmPlotter/js/index.mjs.map")
|
||||||
SOURCEMAP_INDEX = None
|
SOURCEMAP_INDEX = None
|
||||||
|
INDEX_REG = compile(r"build\/runtime-pyside6\/LogarithmPlotter\/qml\/eu\/ad5001\/LogarithmPlotter\/js\/index.mjs:(\d+)")
|
||||||
|
|
||||||
|
|
||||||
class LOG_COLORS:
|
class LOG_COLORS:
|
||||||
|
@ -77,6 +79,7 @@ def create_log_terminal_message(mode: QtMsgType, context: QMessageLogContext, me
|
||||||
# Check MJS
|
# Check MJS
|
||||||
if line is not None and source_file is not None and source_file.endswith("index.mjs"):
|
if line is not None and source_file is not None and source_file.endswith("index.mjs"):
|
||||||
source_file, line = map_javascript_source(source_file, line)
|
source_file, line = map_javascript_source(source_file, line)
|
||||||
|
# Parse message
|
||||||
prefix = f"{LOG_COLORS.INVERT}{mode[1]}[{mode[0].upper()}]{LOG_COLORS.RESET_INVERT}"
|
prefix = f"{LOG_COLORS.INVERT}{mode[1]}[{mode[0].upper()}]{LOG_COLORS.RESET_INVERT}"
|
||||||
message = message + LOG_COLORS.RESET
|
message = message + LOG_COLORS.RESET
|
||||||
context = f"{context.function} at {source_file}:{line}"
|
context = f"{context.function} at {source_file}:{line}"
|
||||||
|
|
|
@ -15,10 +15,9 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from PySide6.QtWidgets import QMessageBox, QApplication
|
from PySide6.QtWidgets import QMessageBox, QApplication
|
||||||
from PySide6.QtCore import QRunnable, QThreadPool, QThread, QObject, Signal, Slot, QCoreApplication
|
from PySide6.QtCore import QRunnable, QThreadPool, QThread, QObject, Signal, Slot, QCoreApplication
|
||||||
from PySide6.QtQml import QQmlApplicationEngine
|
from PySide6.QtQml import QJSValue
|
||||||
from PySide6.QtGui import QImage
|
from PySide6.QtGui import QImage
|
||||||
from PySide6 import __version__ as PySide6_version
|
from PySide6 import __version__ as PySide6_version
|
||||||
|
|
||||||
|
@ -30,30 +29,27 @@ from urllib.error import HTTPError, URLError
|
||||||
|
|
||||||
from LogarithmPlotter import __VERSION__
|
from LogarithmPlotter import __VERSION__
|
||||||
from LogarithmPlotter.util import config
|
from LogarithmPlotter.util import config
|
||||||
|
from LogarithmPlotter.util.promise import PyPromise
|
||||||
|
|
||||||
SHOW_GUI_MESSAGES = "--test-build" not in argv
|
SHOW_GUI_MESSAGES = "--test-build" not in argv
|
||||||
CHANGELOG_VERSION = __VERSION__
|
CHANGELOG_VERSION = __VERSION__
|
||||||
|
CHANGELOG_CACHE_PATH = path.join(path.dirname(path.realpath(__file__)), "CHANGELOG.md")
|
||||||
|
|
||||||
|
|
||||||
class InvalidFileException(Exception): pass
|
class InvalidFileException(Exception): pass
|
||||||
|
|
||||||
|
|
||||||
def show_message(msg: str) -> None:
|
def show_message(msg: str) -> None:
|
||||||
"""
|
"""
|
||||||
Shows a GUI message if GUI messages are enabled
|
Shows a GUI message if GUI messages are enabled
|
||||||
"""
|
"""
|
||||||
if SHOW_GUI_MESSAGES:
|
if SHOW_GUI_MESSAGES:
|
||||||
QMessageBox.warning(None, "LogarithmPlotter", msg, QMessageBox.OK)
|
QMessageBox.warning(None, "LogarithmPlotter", msg)
|
||||||
else:
|
else:
|
||||||
raise InvalidFileException(msg)
|
raise InvalidFileException(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_changelog():
|
||||||
class ChangelogFetcher(QRunnable):
|
|
||||||
def __init__(self, helper):
|
|
||||||
QRunnable.__init__(self)
|
|
||||||
self.helper = helper
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
msg_text = "Unknown changelog error."
|
msg_text = "Unknown changelog error."
|
||||||
try:
|
try:
|
||||||
# Fetching version
|
# Fetching version
|
||||||
|
@ -66,12 +62,17 @@ class ChangelogFetcher(QRunnable):
|
||||||
str(e.code))
|
str(e.code))
|
||||||
except URLError as e:
|
except URLError as e:
|
||||||
msg_text = QCoreApplication.translate("changelog", "Could not fetch update: {}.").format(str(e.reason))
|
msg_text = QCoreApplication.translate("changelog", "Could not fetch update: {}.").format(str(e.reason))
|
||||||
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):
|
class Helper(QObject):
|
||||||
changelogFetched = Signal(str)
|
|
||||||
|
|
||||||
def __init__(self, cwd: str, tmpfile: str):
|
def __init__(self, cwd: str, tmpfile: str):
|
||||||
QObject.__init__(self)
|
QObject.__init__(self)
|
||||||
self.cwd = cwd
|
self.cwd = cwd
|
||||||
|
@ -135,29 +136,13 @@ class Helper(QObject):
|
||||||
def getVersion(self):
|
def getVersion(self):
|
||||||
return __VERSION__
|
return __VERSION__
|
||||||
|
|
||||||
@Slot(str, result=str)
|
@Slot(str, result=QJSValue)
|
||||||
def getSetting(self, namespace):
|
def getSetting(self, namespace: str) -> QJSValue:
|
||||||
return str(config.getSetting(namespace))
|
return QJSValue(config.getSetting(namespace))
|
||||||
|
|
||||||
@Slot(str, result=float)
|
@Slot(str, QJSValue)
|
||||||
def getSettingInt(self, namespace):
|
def setSetting(self, namespace: str, value: QJSValue):
|
||||||
return float(config.getSetting(namespace))
|
return config.setSetting(namespace, value.toPrimitive().toVariant())
|
||||||
|
|
||||||
@Slot(str, result=bool)
|
|
||||||
def getSettingBool(self, namespace):
|
|
||||||
return bool(config.getSetting(namespace))
|
|
||||||
|
|
||||||
@Slot(str, str)
|
|
||||||
def setSetting(self, namespace, value):
|
|
||||||
return config.setSetting(namespace, str(value))
|
|
||||||
|
|
||||||
@Slot(str, bool)
|
|
||||||
def setSettingBool(self, namespace, value):
|
|
||||||
return config.setSetting(namespace, bool(value))
|
|
||||||
|
|
||||||
@Slot(str, float)
|
|
||||||
def setSettingInt(self, namespace, value):
|
|
||||||
return config.setSetting(namespace, float(value))
|
|
||||||
|
|
||||||
@Slot(result=str)
|
@Slot(result=str)
|
||||||
def getDebugInfos(self):
|
def getDebugInfos(self):
|
||||||
|
@ -167,15 +152,14 @@ class Helper(QObject):
|
||||||
msg = QCoreApplication.translate('main', "Built with PySide6 (Qt) v{} and python v{}")
|
msg = QCoreApplication.translate('main', "Built with PySide6 (Qt) v{} and python v{}")
|
||||||
return msg.format(PySide6_version, sys_version.split("\n")[0])
|
return msg.format(PySide6_version, sys_version.split("\n")[0])
|
||||||
|
|
||||||
@Slot()
|
@Slot(result=PyPromise)
|
||||||
def fetchChangelog(self):
|
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.
|
# We have a cached version of the changelog, for env that don't have access to the internet.
|
||||||
f = open(changelog_cache_path);
|
return PyPromise(read_changelog)
|
||||||
self.changelogFetched.emit("".join(f.readlines()).strip())
|
|
||||||
f.close()
|
|
||||||
else:
|
else:
|
||||||
# Fetch it from the internet.
|
# Fetch it from the internet.
|
||||||
runnable = ChangelogFetcher(self)
|
return PyPromise(fetch_changelog)
|
||||||
QThreadPool.globalInstance().start(runnable)
|
|
||||||
|
|
|
@ -16,13 +16,13 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
from re import Pattern
|
from re import Pattern
|
||||||
|
from typing import Callable
|
||||||
from PySide6.QtCore import QMetaObject, QObject, QDateTime
|
from PySide6.QtCore import QMetaObject, QObject, QDateTime
|
||||||
from PySide6.QtQml import QJSValue
|
from PySide6.QtQml import QJSValue
|
||||||
|
|
||||||
class InvalidAttributeValueException(Exception): pass
|
class InvalidAttributeValueException(Exception): pass
|
||||||
class NotAPrimitiveException(Exception): pass
|
class NotAPrimitiveException(Exception): pass
|
||||||
|
|
||||||
class Function: pass
|
|
||||||
class URL: pass
|
class URL: pass
|
||||||
|
|
||||||
class PyJSValue:
|
class PyJSValue:
|
||||||
|
@ -75,10 +75,11 @@ class PyJSValue:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def type(self) -> any:
|
def type(self) -> any:
|
||||||
|
ret = None
|
||||||
matcher = [
|
matcher = [
|
||||||
(lambda: self.qjs_value.isArray(), list),
|
(lambda: self.qjs_value.isArray(), list),
|
||||||
(lambda: self.qjs_value.isBool(), bool),
|
(lambda: self.qjs_value.isBool(), bool),
|
||||||
(lambda: self.qjs_value.isCallable(), Function),
|
(lambda: self.qjs_value.isCallable(), Callable),
|
||||||
(lambda: self.qjs_value.isDate(), QDateTime),
|
(lambda: self.qjs_value.isDate(), QDateTime),
|
||||||
(lambda: self.qjs_value.isError(), Exception),
|
(lambda: self.qjs_value.isError(), Exception),
|
||||||
(lambda: self.qjs_value.isNull(), None),
|
(lambda: self.qjs_value.isNull(), None),
|
||||||
|
@ -93,8 +94,9 @@ class PyJSValue:
|
||||||
]
|
]
|
||||||
for (test, value) in matcher:
|
for (test, value) in matcher:
|
||||||
if test():
|
if test():
|
||||||
return value
|
ret = value
|
||||||
return None
|
break
|
||||||
|
return ret
|
||||||
|
|
||||||
def primitive(self):
|
def primitive(self):
|
||||||
"""
|
"""
|
||||||
|
@ -104,3 +106,5 @@ class PyJSValue:
|
||||||
if self.type() not in [bool, float, str, None]:
|
if self.type() not in [bool, float, str, None]:
|
||||||
raise NotAPrimitiveException()
|
raise NotAPrimitiveException()
|
||||||
return self.qjs_value.toPrimitive().toVariant()
|
return self.qjs_value.toPrimitive().toVariant()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -15,18 +15,22 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
from PySide6.QtCore import QObject, Slot, Property, QCoreApplication
|
from PySide6.QtCore import QObject, Slot, Property, QCoreApplication, Signal
|
||||||
from PySide6.QtGui import QImage, QColor
|
from PySide6.QtGui import QImage, QColor
|
||||||
from PySide6.QtWidgets import QMessageBox
|
from PySide6.QtWidgets import QMessageBox
|
||||||
|
|
||||||
from os import path, remove
|
from os import path, remove, makedirs
|
||||||
from string import Template
|
from string import Template
|
||||||
from tempfile import TemporaryDirectory
|
|
||||||
from subprocess import Popen, TimeoutExpired, PIPE
|
from subprocess import Popen, TimeoutExpired, PIPE
|
||||||
|
from hashlib import sha512
|
||||||
from shutil import which
|
from shutil import which
|
||||||
from sys import argv
|
from sys import argv
|
||||||
|
|
||||||
|
from LogarithmPlotter.util import config
|
||||||
|
from LogarithmPlotter.util.promise import PyPromise
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Searches for a valid Latex and DVIPNG (http://savannah.nongnu.org/projects/dvipng/)
|
Searches for a valid Latex and DVIPNG (http://savannah.nongnu.org/projects/dvipng/)
|
||||||
installation and collects the binary path in the DVIPNG_PATH variable.
|
installation and collects the binary path in the DVIPNG_PATH variable.
|
||||||
|
@ -75,14 +79,20 @@ class Latex(QObject):
|
||||||
dvipng to be installed on the system.
|
dvipng to be installed on the system.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, tempdir: TemporaryDirectory):
|
def __init__(self, cache_path):
|
||||||
QObject.__init__(self)
|
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)
|
@Property(bool)
|
||||||
def latexSupported(self) -> bool:
|
def latexSupported(self) -> bool:
|
||||||
return LATEX_PATH is not None and DVIPNG_PATH is not None
|
return LATEX_PATH is not None and DVIPNG_PATH is not None
|
||||||
|
|
||||||
|
@Property(bool)
|
||||||
|
def supportsAsyncRender(self) -> bool:
|
||||||
|
return config.getSetting("enable_latex_async")
|
||||||
|
|
||||||
@Slot(result=bool)
|
@Slot(result=bool)
|
||||||
def checkLatexInstallation(self) -> bool:
|
def checkLatexInstallation(self) -> bool:
|
||||||
"""
|
"""
|
||||||
|
@ -103,21 +113,77 @@ class Latex(QObject):
|
||||||
valid_install = False
|
valid_install = False
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
self.render("", 14, QColor(0, 0, 0, 255))
|
self.renderSync("", 14, QColor(0, 0, 0, 255))
|
||||||
except MissingPackageException:
|
except MissingPackageException:
|
||||||
valid_install = False # Should have sent an error message if failed to render
|
valid_install = False # Should have sent an error message if failed to render
|
||||||
return valid_install
|
return valid_install
|
||||||
|
|
||||||
|
def lock(self, markup_hash, render_hash, promise):
|
||||||
|
"""
|
||||||
|
Locks the render pipeline for a given markup hash and render hash.
|
||||||
|
"""
|
||||||
|
# print("Locking", markup_hash, render_hash)
|
||||||
|
if markup_hash not in self.render_pipeline_locks:
|
||||||
|
self.render_pipeline_locks[markup_hash] = promise
|
||||||
|
self.render_pipeline_locks[render_hash] = promise
|
||||||
|
|
||||||
|
|
||||||
|
def release_lock(self, markup_hash, render_hash):
|
||||||
|
"""
|
||||||
|
Release locks on the markup and render hashes.
|
||||||
|
"""
|
||||||
|
# print("Releasing", markup_hash, render_hash)
|
||||||
|
if markup_hash in self.render_pipeline_locks:
|
||||||
|
del self.render_pipeline_locks[markup_hash]
|
||||||
|
del self.render_pipeline_locks[render_hash]
|
||||||
|
|
||||||
|
@Slot(str, float, QColor, result=PyPromise)
|
||||||
|
def renderAsync(self, latex_markup: str, font_size: float, color: QColor) -> PyPromise:
|
||||||
|
"""
|
||||||
|
Prepares and renders a latex string into a png file asynchronously.
|
||||||
|
"""
|
||||||
|
markup_hash, render_hash, export_path = self.create_export_path(latex_markup, font_size, color)
|
||||||
|
promise = None
|
||||||
|
if render_hash in self.render_pipeline_locks:
|
||||||
|
# A PyPromise for this specific render is already running.
|
||||||
|
# print("Already running render of", latex_markup)
|
||||||
|
promise = self.render_pipeline_locks[render_hash]
|
||||||
|
elif markup_hash in self.render_pipeline_locks:
|
||||||
|
# A PyPromise with the same markup, but not the same color or font size is already running.
|
||||||
|
# print("Chaining render of", latex_markup)
|
||||||
|
existing_promise = self.render_pipeline_locks[markup_hash]
|
||||||
|
promise = self._create_async_promise(latex_markup, font_size, color)
|
||||||
|
existing_promise.then(promise.start)
|
||||||
|
else:
|
||||||
|
# No such PyPromise is running.
|
||||||
|
promise = self._create_async_promise(latex_markup, font_size, color)
|
||||||
|
promise.start()
|
||||||
|
return promise
|
||||||
|
|
||||||
|
def _create_async_promise(self, latex_markup: str, font_size: float, color: QColor) -> PyPromise:
|
||||||
|
"""
|
||||||
|
Createsa PyPromise to render a latex string into a PNG file.
|
||||||
|
Internal method. Use renderAsync that makes use of locks.
|
||||||
|
"""
|
||||||
|
markup_hash, render_hash, export_path = self.create_export_path(latex_markup, font_size, color)
|
||||||
|
promise = PyPromise(self.renderSync, [latex_markup, font_size, color], start_automatically=False)
|
||||||
|
self.lock(markup_hash, render_hash, promise)
|
||||||
|
# Make the lock release at the end.
|
||||||
|
def unlock(data, markup_hash=markup_hash, render_hash=render_hash):
|
||||||
|
self.release_lock(markup_hash, render_hash)
|
||||||
|
promise.then(unlock, unlock)
|
||||||
|
return promise
|
||||||
|
|
||||||
@Slot(str, float, QColor, result=str)
|
@Slot(str, float, QColor, result=str)
|
||||||
def 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.
|
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"):
|
if self.latexSupported and not path.exists(export_path + ".png"):
|
||||||
print("Rendering", latex_markup, export_path)
|
print("Rendering", latex_markup)
|
||||||
# Generating file
|
# 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 the formula is just recolored or the font is just changed, no need to recreate the DVI.
|
||||||
if not path.exists(latex_path + ".dvi"):
|
if not path.exists(latex_path + ".dvi"):
|
||||||
self.create_latex_doc(latex_path, latex_markup)
|
self.create_latex_doc(latex_path, latex_markup)
|
||||||
|
@ -137,7 +203,7 @@ class Latex(QObject):
|
||||||
"""
|
"""
|
||||||
Finds a prerendered image and returns its data if possible, and an empty string if not.
|
Finds a prerendered image and returns its data if possible, and an empty string if not.
|
||||||
"""
|
"""
|
||||||
markup_hash, 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 = ""
|
data = ""
|
||||||
if path.exists(export_path + ".png"):
|
if path.exists(export_path + ".png"):
|
||||||
img = QImage(export_path)
|
img = QImage(export_path)
|
||||||
|
@ -147,10 +213,13 @@ class Latex(QObject):
|
||||||
def create_export_path(self, latex_markup: str, font_size: float, color: QColor):
|
def create_export_path(self, latex_markup: str, font_size: float, color: QColor):
|
||||||
"""
|
"""
|
||||||
Standardizes export path for renders.
|
Standardizes export path for renders.
|
||||||
|
Markup hash is unique for the markup
|
||||||
|
Render hash is unique for the markup, the font size and the color.
|
||||||
"""
|
"""
|
||||||
markup_hash = "render" + str(hash(latex_markup))
|
markup_hash = "render" + str(sha512(latex_markup.encode()).hexdigest())
|
||||||
export_path = path.join(self.tempdir.name, f'{markup_hash}_{int(font_size)}_{color.rgb()}')
|
render_hash = f'{markup_hash}_{int(font_size)}_{color.rgb()}'
|
||||||
return markup_hash, export_path
|
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):
|
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.
|
Runs a subprocess and handles exceptions and messages them to the user.
|
||||||
"""
|
"""
|
||||||
cmd = " ".join(process)
|
cmd = " ".join(process)
|
||||||
proc = Popen(process, stdout=PIPE, stderr=PIPE, cwd=self.tempdir.name)
|
proc = Popen(process, stdout=PIPE, stderr=PIPE, cwd=self.tempdir)
|
||||||
try:
|
try:
|
||||||
out, err = proc.communicate(timeout=2) # 2 seconds is already FAR too long.
|
out, err = proc.communicate(timeout=2) # 2 seconds is already FAR too long.
|
||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
|
|
|
@ -49,3 +49,7 @@ class MacOSFileOpenHandler(QObject):
|
||||||
else:
|
else:
|
||||||
# standard event processing
|
# standard event processing
|
||||||
return QObject.eventFilter(self, obj, event)
|
return QObject.eventFilter(self, obj, event)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
173
runtime-pyside6/LogarithmPlotter/util/promise.py
Normal file
173
runtime-pyside6/LogarithmPlotter/util/promise.py
Normal 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.
|
184
runtime-pyside6/poetry.lock
generated
184
runtime-pyside6/poetry.lock
generated
|
@ -24,83 +24,73 @@ files = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "coverage"
|
name = "coverage"
|
||||||
version = "7.6.1"
|
version = "7.6.2"
|
||||||
description = "Code coverage measurement for Python"
|
description = "Code coverage measurement for Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.9"
|
||||||
files = [
|
files = [
|
||||||
{file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"},
|
{file = "coverage-7.6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c9df1950fb92d49970cce38100d7e7293c84ed3606eaa16ea0b6bc27175bb667"},
|
||||||
{file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"},
|
{file = "coverage-7.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:24500f4b0e03aab60ce575c85365beab64b44d4db837021e08339f61d1fbfe52"},
|
||||||
{file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"},
|
{file = "coverage-7.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a663b180b6669c400b4630a24cc776f23a992d38ce7ae72ede2a397ce6b0f170"},
|
||||||
{file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"},
|
{file = "coverage-7.6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfde025e2793a22efe8c21f807d276bd1d6a4bcc5ba6f19dbdfc4e7a12160909"},
|
||||||
{file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"},
|
{file = "coverage-7.6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:087932079c065d7b8ebadd3a0160656c55954144af6439886c8bcf78bbbcde7f"},
|
||||||
{file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"},
|
{file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9c6b0c1cafd96213a0327cf680acb39f70e452caf8e9a25aeb05316db9c07f89"},
|
||||||
{file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"},
|
{file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6e85830eed5b5263ffa0c62428e43cb844296f3b4461f09e4bdb0d44ec190bc2"},
|
||||||
{file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"},
|
{file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62ab4231c01e156ece1b3a187c87173f31cbeee83a5e1f6dff17f288dca93345"},
|
||||||
{file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"},
|
{file = "coverage-7.6.2-cp310-cp310-win32.whl", hash = "sha256:7b80fbb0da3aebde102a37ef0138aeedff45997e22f8962e5f16ae1742852676"},
|
||||||
{file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"},
|
{file = "coverage-7.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:d20c3d1f31f14d6962a4e2f549c21d31e670b90f777ef4171be540fb7fb70f02"},
|
||||||
{file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"},
|
{file = "coverage-7.6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bb21bac7783c1bf6f4bbe68b1e0ff0d20e7e7732cfb7995bc8d96e23aa90fc7b"},
|
||||||
{file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"},
|
{file = "coverage-7.6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a7b2e437fbd8fae5bc7716b9c7ff97aecc95f0b4d56e4ca08b3c8d8adcaadb84"},
|
||||||
{file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"},
|
{file = "coverage-7.6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:536f77f2bf5797983652d1d55f1a7272a29afcc89e3ae51caa99b2db4e89d658"},
|
||||||
{file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"},
|
{file = "coverage-7.6.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f361296ca7054f0936b02525646b2731b32c8074ba6defab524b79b2b7eeac72"},
|
||||||
{file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"},
|
{file = "coverage-7.6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7926d8d034e06b479797c199747dd774d5e86179f2ce44294423327a88d66ca7"},
|
||||||
{file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"},
|
{file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0bbae11c138585c89fb4e991faefb174a80112e1a7557d507aaa07675c62e66b"},
|
||||||
{file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"},
|
{file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fcad7d5d2bbfeae1026b395036a8aa5abf67e8038ae7e6a25c7d0f88b10a8e6a"},
|
||||||
{file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"},
|
{file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f01e53575f27097d75d42de33b1b289c74b16891ce576d767ad8c48d17aeb5e0"},
|
||||||
{file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"},
|
{file = "coverage-7.6.2-cp311-cp311-win32.whl", hash = "sha256:7781f4f70c9b0b39e1b129b10c7d43a4e0c91f90c60435e6da8288efc2b73438"},
|
||||||
{file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"},
|
{file = "coverage-7.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:9bcd51eeca35a80e76dc5794a9dd7cb04b97f0e8af620d54711793bfc1fbba4b"},
|
||||||
{file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"},
|
{file = "coverage-7.6.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ebc94fadbd4a3f4215993326a6a00e47d79889391f5659bf310f55fe5d9f581c"},
|
||||||
{file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"},
|
{file = "coverage-7.6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9681516288e3dcf0aa7c26231178cc0be6cac9705cac06709f2353c5b406cfea"},
|
||||||
{file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"},
|
{file = "coverage-7.6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d9c5d13927d77af4fbe453953810db766f75401e764727e73a6ee4f82527b3e"},
|
||||||
{file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"},
|
{file = "coverage-7.6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92f9ca04b3e719d69b02dc4a69debb795af84cb7afd09c5eb5d54b4a1ae2191"},
|
||||||
{file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"},
|
{file = "coverage-7.6.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ff2ef83d6d0b527b5c9dad73819b24a2f76fdddcfd6c4e7a4d7e73ecb0656b4"},
|
||||||
{file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"},
|
{file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:47ccb6e99a3031ffbbd6e7cc041e70770b4fe405370c66a54dbf26a500ded80b"},
|
||||||
{file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"},
|
{file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a867d26f06bcd047ef716175b2696b315cb7571ccb951006d61ca80bbc356e9e"},
|
||||||
{file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"},
|
{file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cdfcf2e914e2ba653101157458afd0ad92a16731eeba9a611b5cbb3e7124e74b"},
|
||||||
{file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"},
|
{file = "coverage-7.6.2-cp312-cp312-win32.whl", hash = "sha256:f9035695dadfb397bee9eeaf1dc7fbeda483bf7664a7397a629846800ce6e276"},
|
||||||
{file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"},
|
{file = "coverage-7.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:5ed69befa9a9fc796fe015a7040c9398722d6b97df73a6b608e9e275fa0932b0"},
|
||||||
{file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"},
|
{file = "coverage-7.6.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eea60c79d36a8f39475b1af887663bc3ae4f31289cd216f514ce18d5938df40"},
|
||||||
{file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"},
|
{file = "coverage-7.6.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa68a6cdbe1bc6793a9dbfc38302c11599bbe1837392ae9b1d238b9ef3dafcf1"},
|
||||||
{file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"},
|
{file = "coverage-7.6.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ec528ae69f0a139690fad6deac8a7d33629fa61ccce693fdd07ddf7e9931fba"},
|
||||||
{file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"},
|
{file = "coverage-7.6.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed5ac02126f74d190fa2cc14a9eb2a5d9837d5863920fa472b02eb1595cdc925"},
|
||||||
{file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"},
|
{file = "coverage-7.6.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21c0ea0d4db8a36b275cb6fb2437a3715697a4ba3cb7b918d3525cc75f726304"},
|
||||||
{file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"},
|
{file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:35a51598f29b2a19e26d0908bd196f771a9b1c5d9a07bf20be0adf28f1ad4f77"},
|
||||||
{file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"},
|
{file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c9192925acc33e146864b8cf037e2ed32a91fdf7644ae875f5d46cd2ef086a5f"},
|
||||||
{file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"},
|
{file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf4eeecc9e10f5403ec06138978235af79c9a79af494eb6b1d60a50b49ed2869"},
|
||||||
{file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"},
|
{file = "coverage-7.6.2-cp313-cp313-win32.whl", hash = "sha256:e4ee15b267d2dad3e8759ca441ad450c334f3733304c55210c2a44516e8d5530"},
|
||||||
{file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"},
|
{file = "coverage-7.6.2-cp313-cp313-win_amd64.whl", hash = "sha256:c71965d1ced48bf97aab79fad56df82c566b4c498ffc09c2094605727c4b7e36"},
|
||||||
{file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"},
|
{file = "coverage-7.6.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7571e8bbecc6ac066256f9de40365ff833553e2e0c0c004f4482facb131820ef"},
|
||||||
{file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"},
|
{file = "coverage-7.6.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:078a87519057dacb5d77e333f740708ec2a8f768655f1db07f8dfd28d7a005f0"},
|
||||||
{file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"},
|
{file = "coverage-7.6.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e5e92e3e84a8718d2de36cd8387459cba9a4508337b8c5f450ce42b87a9e760"},
|
||||||
{file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"},
|
{file = "coverage-7.6.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ebabdf1c76593a09ee18c1a06cd3022919861365219ea3aca0247ededf6facd6"},
|
||||||
{file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"},
|
{file = "coverage-7.6.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12179eb0575b8900912711688e45474f04ab3934aaa7b624dea7b3c511ecc90f"},
|
||||||
{file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"},
|
{file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:39d3b964abfe1519b9d313ab28abf1d02faea26cd14b27f5283849bf59479ff5"},
|
||||||
{file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"},
|
{file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:84c4315577f7cd511d6250ffd0f695c825efe729f4205c0340f7004eda51191f"},
|
||||||
{file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"},
|
{file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ff797320dcbff57caa6b2301c3913784a010e13b1f6cf4ab3f563f3c5e7919db"},
|
||||||
{file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"},
|
{file = "coverage-7.6.2-cp313-cp313t-win32.whl", hash = "sha256:2b636a301e53964550e2f3094484fa5a96e699db318d65398cfba438c5c92171"},
|
||||||
{file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"},
|
{file = "coverage-7.6.2-cp313-cp313t-win_amd64.whl", hash = "sha256:d03a060ac1a08e10589c27d509bbdb35b65f2d7f3f8d81cf2fa199877c7bc58a"},
|
||||||
{file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"},
|
{file = "coverage-7.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c37faddc8acd826cfc5e2392531aba734b229741d3daec7f4c777a8f0d4993e5"},
|
||||||
{file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"},
|
{file = "coverage-7.6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab31fdd643f162c467cfe6a86e9cb5f1965b632e5e65c072d90854ff486d02cf"},
|
||||||
{file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"},
|
{file = "coverage-7.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97df87e1a20deb75ac7d920c812e9326096aa00a9a4b6d07679b4f1f14b06c90"},
|
||||||
{file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"},
|
{file = "coverage-7.6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:343056c5e0737487a5291f5691f4dfeb25b3e3c8699b4d36b92bb0e586219d14"},
|
||||||
{file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"},
|
{file = "coverage-7.6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4ef1c56b47b6b9024b939d503ab487231df1f722065a48f4fc61832130b90e"},
|
||||||
{file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"},
|
{file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fca4a92c8a7a73dee6946471bce6d1443d94155694b893b79e19ca2a540d86e"},
|
||||||
{file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"},
|
{file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69f251804e052fc46d29d0e7348cdc5fcbfc4861dc4a1ebedef7e78d241ad39e"},
|
||||||
{file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"},
|
{file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e8ea055b3ea046c0f66217af65bc193bbbeca1c8661dc5fd42698db5795d2627"},
|
||||||
{file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"},
|
{file = "coverage-7.6.2-cp39-cp39-win32.whl", hash = "sha256:6c2ba1e0c24d8fae8f2cf0aeb2fc0a2a7f69b6d20bd8d3749fd6b36ecef5edf0"},
|
||||||
{file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"},
|
{file = "coverage-7.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:2186369a654a15628e9c1c9921409a6b3eda833e4b91f3ca2a7d9f77abb4987c"},
|
||||||
{file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"},
|
{file = "coverage-7.6.2-pp39.pp310-none-any.whl", hash = "sha256:667952739daafe9616db19fbedbdb87917eee253ac4f31d70c7587f7ab531b4e"},
|
||||||
{file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"},
|
{file = "coverage-7.6.2.tar.gz", hash = "sha256:a5f81e68aa62bc0cfca04f7b19eaa8f9c826b53fc82ab9e2121976dc74f131f3"},
|
||||||
{file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"},
|
|
||||||
{file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"},
|
|
||||||
{file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"},
|
|
||||||
{file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"},
|
|
||||||
{file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"},
|
|
||||||
{file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"},
|
|
||||||
{file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"},
|
|
||||||
{file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"},
|
|
||||||
{file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"},
|
|
||||||
{file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
@ -261,36 +251,36 @@ setuptools = ">=42.0.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyside6-addons"
|
name = "pyside6-addons"
|
||||||
version = "6.7.3"
|
version = "6.8.0"
|
||||||
description = "Python bindings for the Qt cross-platform application and UI framework (Addons)"
|
description = "Python bindings for the Qt cross-platform application and UI framework (Addons)"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "<3.13,>=3.9"
|
python-versions = "<3.13,>=3.9"
|
||||||
files = [
|
files = [
|
||||||
{file = "PySide6_Addons-6.7.3-cp39-abi3-macosx_11_0_universal2.whl", hash = "sha256:3174cb3a373c09c98740b452e8e8f4945d64cfa18ed8d43964111d570f0dc647"},
|
{file = "PySide6_Addons-6.8.0-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:aebab1e4fe63ebceccae4068768bf20959ab78f7fe01af832458837241334b5c"},
|
||||||
{file = "PySide6_Addons-6.7.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:bde1eb03dbffd089b50cd445847aaecaf4056cea84c49ea592d00f84f247251e"},
|
{file = "PySide6_Addons-6.8.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4b3be260f9cc415d1a12b77a703ced18b8854f56985f4708cab5618a9554bbd6"},
|
||||||
{file = "PySide6_Addons-6.7.3-cp39-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:5a9e0df31345fe6caea677d916ea48b53ba86f95cc6499c57f89e392447ad6db"},
|
{file = "PySide6_Addons-6.8.0-cp39-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:7c153a685341683fac82d32926e2204747a83c13ef7b203db8ae9efe27f26a0f"},
|
||||||
{file = "PySide6_Addons-6.7.3-cp39-abi3-win_amd64.whl", hash = "sha256:d8a19c2b2446407724c81c33ebf3217eaabd092f0f72da8130c17079e04a7813"},
|
{file = "PySide6_Addons-6.8.0-cp39-abi3-win_amd64.whl", hash = "sha256:8f7f20fb3758995580f1fb8342df3479be51958eca36db2d6f6a3304f31471de"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
PySide6-Essentials = "6.7.3"
|
PySide6-Essentials = "6.8.0"
|
||||||
shiboken6 = "6.7.3"
|
shiboken6 = "6.8.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyside6-essentials"
|
name = "pyside6-essentials"
|
||||||
version = "6.7.3"
|
version = "6.8.0"
|
||||||
description = "Python bindings for the Qt cross-platform application and UI framework (Essentials)"
|
description = "Python bindings for the Qt cross-platform application and UI framework (Essentials)"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "<3.13,>=3.9"
|
python-versions = "<3.13,>=3.9"
|
||||||
files = [
|
files = [
|
||||||
{file = "PySide6_Essentials-6.7.3-cp39-abi3-macosx_11_0_universal2.whl", hash = "sha256:f9e08a4e9e7dc7b5ab72fde20abce8c97df7af1b802d9743f098f577dfe1f649"},
|
{file = "PySide6_Essentials-6.8.0-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:c2ad37de574ed911ac2dd392e95888ee7354c4bc475259dafc31978efb710a6a"},
|
||||||
{file = "PySide6_Essentials-6.7.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cda6fd26aead48f32e57f044d18aa75dc39265b49d7957f515ce7ac3989e7029"},
|
{file = "PySide6_Essentials-6.8.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:da99a94806416ec1e386426a474e7d1e514c1cdf8ad171c005376f4f633e7216"},
|
||||||
{file = "PySide6_Essentials-6.7.3-cp39-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:acdde06b74f26e7d26b4ae1461081b32a6cb17fcaa2a580050b5e0f0f12236c9"},
|
{file = "PySide6_Essentials-6.8.0-cp39-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:ae0732228e93eb882c9a93fd510819fb64b7d09d8e500912b485a604537215d6"},
|
||||||
{file = "PySide6_Essentials-6.7.3-cp39-abi3-win_amd64.whl", hash = "sha256:f0950fcdcbcd4f2443336dc6a5fe692172adc225f876839583503ded0ab2f2a7"},
|
{file = "PySide6_Essentials-6.8.0-cp39-abi3-win_amd64.whl", hash = "sha256:2ef7138dc7efb9f1153c1dda7a7bd6ac02badad1aa1971cc140d0b9bf962c3dc"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
shiboken6 = "6.7.3"
|
shiboken6 = "6.8.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
|
@ -384,15 +374,15 @@ type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shiboken6"
|
name = "shiboken6"
|
||||||
version = "6.7.3"
|
version = "6.8.0"
|
||||||
description = "Python/C++ bindings helper module"
|
description = "Python/C++ bindings helper module"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "<3.13,>=3.9"
|
python-versions = "<3.13,>=3.9"
|
||||||
files = [
|
files = [
|
||||||
{file = "shiboken6-6.7.3-cp39-abi3-macosx_11_0_universal2.whl", hash = "sha256:285fe3cf79be3135fe1ad1e2b9ff6db3a48698887425af6aa6ed7a05a9abc3d6"},
|
{file = "shiboken6-6.8.0-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:0d3171c496e7474ad29d73686e46e741317a9b29ae9fa30c421fa0360bc10af0"},
|
||||||
{file = "shiboken6-6.7.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f0852e5781de78be5b13c140ec4c7fb9734e2aaf2986eb2d6a224363e03efccc"},
|
{file = "shiboken6-6.8.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ad88c0e73c9e4de3723c6e6b846e651729433ff9d9086bb2b4e6d49965477d97"},
|
||||||
{file = "shiboken6-6.7.3-cp39-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:f0dd635178e64a45be2f84c9f33dd79ac30328da87f834f21a0baf69ae210e6e"},
|
{file = "shiboken6-6.8.0-cp39-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:0f62ee7c34337e2c39fff0985694224f7503328c450245c399846b72cd71c410"},
|
||||||
{file = "shiboken6-6.7.3-cp39-abi3-win_amd64.whl", hash = "sha256:5f29325dfa86fde0274240f1f38e421303749d3174ce3ada178715b5f4719db9"},
|
{file = "shiboken6-6.8.0-cp39-abi3-win_amd64.whl", hash = "sha256:cb98424a1f0c2d6ebf7f6be99660a121b9b22601a058e6b7efeadbc60bcd2182"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -418,13 +408,13 @@ files = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tomli"
|
name = "tomli"
|
||||||
version = "2.0.1"
|
version = "2.0.2"
|
||||||
description = "A lil' TOML parser"
|
description = "A lil' TOML parser"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
{file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"},
|
||||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
{file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -449,4 +439,4 @@ type = ["pytest-mypy"]
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = ">=3.9,<3.13"
|
python-versions = ">=3.9,<3.13"
|
||||||
content-hash = "5636605737f21954e102a0110972e6bd3df07f2d5929f41fe541c7347c3ecf08"
|
content-hash = "fad810a5ba9b4cb5ab759c9b5641ccba2b735e12064e510c0bfe0f4766c576f1"
|
||||||
|
|
|
@ -9,8 +9,8 @@ package-mode = false
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = ">=3.9,<3.13"
|
python = ">=3.9,<3.13"
|
||||||
PySide6-Essentials = "^6.7"
|
PySide6-Essentials = "^6.8"
|
||||||
PySide6-Addons = "^6.7"
|
PySide6-Addons = "^6.8"
|
||||||
|
|
||||||
[tool.poetry.group.packaging.dependencies]
|
[tool.poetry.group.packaging.dependencies]
|
||||||
pyinstaller = "^6.10.0"
|
pyinstaller = "^6.10.0"
|
||||||
|
|
|
@ -26,27 +26,27 @@ print(sys.argv)
|
||||||
current_dir = os.path.realpath(os.path.dirname(os.path.realpath(__file__)))
|
current_dir = os.path.realpath(os.path.dirname(os.path.realpath(__file__)))
|
||||||
|
|
||||||
# Check where to install by default
|
# Check where to install by default
|
||||||
if "PREFIX" not in os.environ and sys.platform == 'linux':
|
# if "PREFIX" not in os.environ and sys.platform == 'linux':
|
||||||
from getopt import getopt
|
# from getopt import getopt
|
||||||
optlist, args = getopt(sys.argv, '', ['prefix=', 'root='])
|
# optlist, args = getopt(sys.argv, '', ['prefix=', 'root='])
|
||||||
for arg,value in optlist:
|
# for arg,value in optlist:
|
||||||
if arg == "prefix" or arg == "root":
|
# if arg == "prefix" or arg == "root":
|
||||||
os.environ["PREFIX"] = value
|
# os.environ["PREFIX"] = value
|
||||||
if "PREFIX" not in os.environ and sys.platform == 'linux':
|
# if "PREFIX" not in os.environ and sys.platform == 'linux':
|
||||||
if "XDG_DATA_HOME" in os.environ:
|
# if "XDG_DATA_HOME" in os.environ:
|
||||||
os.environ["PREFIX"] = os.environ["XDG_DATA_HOME"]
|
# os.environ["PREFIX"] = os.environ["XDG_DATA_HOME"]
|
||||||
else:
|
# else:
|
||||||
try:
|
# try:
|
||||||
# Checking if we have permission to write to root.
|
# # Checking if we have permission to write to root.
|
||||||
from os import makedirs, rmdir
|
# from os import makedirs, rmdir
|
||||||
makedirs("/usr/share/applications/test")
|
# makedirs("/usr/share/applications/test")
|
||||||
rmdir("/usr/share/applications/test")
|
# rmdir("/usr/share/applications/test")
|
||||||
os.environ["PREFIX"] = "/usr/share"
|
# os.environ["PREFIX"] = "/usr/share"
|
||||||
except:
|
# except:
|
||||||
if ".pybuild" in os.environ["HOME"]: # Launchpad building.
|
# if ".pybuild" in os.environ["HOME"]: # Launchpad building.
|
||||||
os.environ["PREFIX"] = "share"
|
# os.environ["PREFIX"] = "share"
|
||||||
else:
|
# else:
|
||||||
os.environ["PREFIX"] = os.environ["HOME"] + "/.local/share"
|
# os.environ["PREFIX"] = os.environ["HOME"] + "/.local/share"
|
||||||
|
|
||||||
from LogarithmPlotter import __VERSION__ as pkg_version
|
from LogarithmPlotter import __VERSION__ as pkg_version
|
||||||
|
|
||||||
|
|
0
runtime-pyside6/tests/__init__.py
Normal file
0
runtime-pyside6/tests/__init__.py
Normal file
0
runtime-pyside6/tests/plugins/__init__.py
Normal file
0
runtime-pyside6/tests/plugins/__init__.py
Normal file
22
runtime-pyside6/tests/plugins/natural/__init__.py
Normal file
22
runtime-pyside6/tests/plugins/natural/__init__.py
Normal 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
|
||||||
|
|
|
@ -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.
|
171
runtime-pyside6/tests/plugins/natural/interfaces/base.py
Normal file
171
runtime-pyside6/tests/plugins/natural/interfaces/base.py
Normal 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
|
83
runtime-pyside6/tests/plugins/natural/interfaces/basic.py
Normal file
83
runtime-pyside6/tests/plugins/natural/interfaces/basic.py
Normal 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
|
320
runtime-pyside6/tests/plugins/natural/interfaces/int.py
Normal file
320
runtime-pyside6/tests/plugins/natural/interfaces/int.py
Normal 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)
|
218
runtime-pyside6/tests/plugins/natural/interfaces/spy.py
Normal file
218
runtime-pyside6/tests/plugins/natural/interfaces/spy.py
Normal 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
Loading…
Reference in a new issue