Compare commits

..

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

175 changed files with 5160 additions and 9302 deletions

6
.gitignore vendored
View file

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

View file

@ -1,4 +1,4 @@
# ![icon](https://apps.ad5001.eu/icons/apps/svg/logarithmplotter.svg) LogarithmPlotter # ![icon](https://git.ad5001.eu/Ad5001/LogarithmPlotter/raw/branch/master/logplotter.svg) LogarithmPlotter
[![Build Status](https://ci.ad5001.eu/api/badges/Ad5001/LogarithmPlotter/status.svg)](https://ci.ad5001.eu/Ad5001/LogarithmPlotter) [![Build Status](https://ci.ad5001.eu/api/badges/Ad5001/LogarithmPlotter/status.svg)](https://ci.ad5001.eu/Ad5001/LogarithmPlotter)
[![Translation status](https://hosted.weblate.org/widgets/logarithmplotter/-/logarithmplotter/svg-badge.svg)](https://hosted.weblate.org/engage/logarithmplotter/) [![Translation status](https://hosted.weblate.org/widgets/logarithmplotter/-/logarithmplotter/svg-badge.svg)](https://hosted.weblate.org/engage/logarithmplotter/)
@ -24,7 +24,7 @@ First, you'll need to install all the required dependencies:
- [npm](https://npmjs.com) (or [yarn](https://yarnpkg.com/)), go to the `common` directory, and run `npm install` (or `yarn install`). - [npm](https://npmjs.com) (or [yarn](https://yarnpkg.com/)), go to the `common` directory, and run `npm install` (or `yarn install`).
You can simply run LogarithmPlotter using `python3 run.py`. It automatically compiles the language files (requires You can simply run LogarithmPlotter using `python3 run.py`. It automatically compiles the language files (requires
`pyside6-lrelease` to be installed and in path), and the JavaScript modules. `lrelease` to be installed and in path), and the JavaScript modules.
If you do not wish do recompile the files again on every run, you can use the build script (`scripts/build.sh`) and run If you do not wish do recompile the files again on every run, you can use the build script (`scripts/build.sh`) and run
`python3 build/runtime-pyside6/LogarithmPlotter/logarithmplotter.py`. `python3 build/runtime-pyside6/LogarithmPlotter/logarithmplotter.py`.
@ -68,13 +68,7 @@ To run LogarithmPlotter's tests, follow these steps:
- Python - Python
- Install python3 and [poetry](https://python-poetry.org/) - Install python3 and [poetry](https://python-poetry.org/)
- Create and activate virtual env (recommended) - Run `poetry install --with test`
- Go into `runtime-pyside6` and run `poetry install --with test`
- ECMAScript
- Install node with npm
- Go into `common` and run `npm install -D`
Finally, to actually run the tests:
- Run `scripts/run-tests.sh` - Run `scripts/run-tests.sh`
## Legal notice ## Legal notice
@ -95,8 +89,8 @@ Finally, to actually run the tests:
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/>.
See LICENSE.md for more details. Language files translations located at assets/i18n are licensed under GNU GPL3.0+ and Language files translations located at LogarithmPlotter/i18n are licensed under GNU GPL3.0+ and are copyrighted by their
are copyrighted by their original authors: original authors. See LICENSE.md for more details:
- 🇭🇺 Hungarian translation by [Óvári](https://github.com/ovari) - 🇭🇺 Hungarian translation by [Óvári](https://github.com/ovari)
- 🇳🇴 Norwegian translation by [Allan Nordhøy](https://github.com/comradekingu) - 🇳🇴 Norwegian translation by [Allan Nordhøy](https://github.com/comradekingu)
@ -109,5 +103,5 @@ of [ndef.parser](https://web.archive.org/web/20111023001618/http://www.undefined
&lt;r@undefined.ch&gt;, ported to javascript by Matthew Crumley &lt;r@undefined.ch&gt;, ported to javascript by Matthew Crumley
&lt;email@matthewcrumley.com&gt; (http://silentmatt.com/), and then to QMLJS by Ad5001. &lt;email@matthewcrumley.com&gt; (http://silentmatt.com/), and then to QMLJS by Ad5001.
All files in (common/src/lib/expr-eval/) except integration.mjs are licensed All files in (LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/) except integration.mjs are licensed
under the [MIT License](https://raw.githubusercontent.com/silentmatt/expr-eval/master/LICENSE.txt). under the [MIT License](https://raw.githubusercontent.com/silentmatt/expr-eval/master/LICENSE.txt).

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -1 +0,0 @@
../common/appearance.svg

View file

@ -1 +0,0 @@
../common/appearance.svg

View file

@ -1 +0,0 @@
../common/arrow.svg

View file

@ -1 +0,0 @@
../common/position.svg

View file

@ -1 +0,0 @@
../common/angle.svg

View file

@ -1 +0,0 @@
../common/angle.svg

View file

@ -1 +0,0 @@
../common/appearance.svg

View file

@ -1 +0,0 @@
../common/target.svg

View file

@ -1 +0,0 @@
../common/position.svg

View file

@ -1 +0,0 @@
../common/label.svg

View file

@ -1 +0,0 @@
../common/angle.svg

View file

@ -1 +0,0 @@
../common/position.svg

View file

@ -1 +0,0 @@
../common/position.svg

View file

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -0,0 +1 @@
../../common/appearance.svg

View file

@ -0,0 +1 @@
../../common/appearance.svg

View file

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -0,0 +1 @@
../../common/arrow.svg

View file

@ -0,0 +1 @@
../../common/position.svg

View file

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1 @@
../../common/angle.svg

View file

@ -0,0 +1 @@
../../common/appearance.svg

View file

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1 @@
../../common/target.svg

View file

@ -0,0 +1 @@
../../common/position.svg

View file

@ -0,0 +1 @@
../../common/label.svg

View file

@ -0,0 +1 @@
../../common/angle.svg

View file

@ -0,0 +1 @@
../../common/position.svg

View file

@ -0,0 +1 @@
../../common/position.svg

View file

@ -0,0 +1 @@
../../common/angle.svg

View file

@ -1,171 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg <svg
width="48.0px" width="24.0px"
height="48.0px" height="24.0px"
viewBox="0 0 48.0 48.0" viewBox="0 0 24.0 24.0"
version="1.1" version="1.1"
id="SVGRoot" id="SVGRoot"
xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#" xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"> xmlns:dc="http://purl.org/dc/elements/1.1/"><title
<title id="title836">LogarithmPlotter Icon v1.0</title><defs
id="title38896">LogarithmPlotter Icon</title> id="defs833" /><metadata
<defs id="metadata836"><rdf:RDF><cc:Work
id="defs2254"> rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
<linearGradient rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title>LogarithmPlotter Icon v1.0</dc:title><cc:license
id="linearGradient27593"> rdf:resource="http://creativecommons.org/licenses/by-nc-sa/4.0/" /><dc:date>2021</dc:date><dc:creator><cc:Agent><dc:title>Ad5001</dc:title></cc:Agent></dc:creator><dc:rights><cc:Agent><dc:title>(c) Ad5001 2021 - All rights reserved</dc:title></cc:Agent></dc:rights></cc:Work><cc:License
<stop rdf:about="http://creativecommons.org/licenses/by-nc-sa/4.0/"><cc:permits
style="stop-color:#000000;stop-opacity:0.15000001;" rdf:resource="http://creativecommons.org/ns#Reproduction" /><cc:permits
offset="0" rdf:resource="http://creativecommons.org/ns#Distribution" /><cc:requires
id="stop27589" /> rdf:resource="http://creativecommons.org/ns#Notice" /><cc:requires
<stop rdf:resource="http://creativecommons.org/ns#Attribution" /><cc:prohibits
style="stop-color:#000000;stop-opacity:0;" rdf:resource="http://creativecommons.org/ns#CommercialUse" /><cc:permits
offset="1" rdf:resource="http://creativecommons.org/ns#DerivativeWorks" /><cc:requires
id="stop27591" /> rdf:resource="http://creativecommons.org/ns#ShareAlike" /></cc:License></rdf:RDF></metadata><g
</linearGradient> id="layer2"
<linearGradient transform="matrix(1,0,0,0.94444444,0,1.1666667)"
id="linearGradient13467"> style="fill:#666666"><rect
<stop style="fill:#666666;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
style="stop-color:#808080;stop-opacity:1;"
offset="0"
id="stop13463" />
<stop
style="stop-color:#666666;stop-opacity:1;"
offset="1"
id="stop13465" />
</linearGradient>
<linearGradient
id="linearGradient8377">
<stop
style="stop-color:#ebebeb;stop-opacity:1;"
offset="0"
id="stop8373" />
<stop
style="stop-color:#bfbfbf;stop-opacity:1;"
offset="1"
id="stop8375" />
</linearGradient>
<linearGradient
xlink:href="#linearGradient8377"
id="linearGradient8379"
x1="12"
y1="4.8570137"
x2="12"
y2="21.105883"
gradientUnits="userSpaceOnUse" />
<linearGradient
xlink:href="#linearGradient13467"
id="linearGradient13469"
x1="12"
y1="9.5647058"
x2="12"
y2="21"
gradientUnits="userSpaceOnUse" />
<linearGradient
xlink:href="#linearGradient27593"
id="linearGradient27595"
x1="28"
y1="28"
x2="42"
y2="42"
gradientUnits="userSpaceOnUse" />
</defs>
<g
id="layer1">
<rect
style="fill:url(#linearGradient13469);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect1546" id="rect1546"
width="18" width="18"
height="18.105883" height="18"
x="3"
y="2.8941176"
ry="2.3823531"
rx="2.2499998"
transform="matrix(2.2222222,0,0,2.0987654,-2.6666667,-0.07407404)" />
<rect
style="fill:url(#linearGradient8379);display:inline;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect1546-7"
width="18"
height="18.105883"
x="3" x="3"
y="3" y="3"
ry="2.3212669" ry="2.25" /></g><g
rx="2.2499998" id="layer2-6"
transform="matrix(2.2222222,0,0,2.1539961,-2.6666667,-2.4619883)" /> transform="matrix(1,0,0,0.94444444,0,0.16666668)"
</g> style="fill:#f9f9f9"><rect
<g style="fill:#f9f9f9;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="layer3" id="rect1546-7"
style="fill:#0000ff"> width="18"
<path height="18"
id="path27475" x="3"
style="fill:url(#linearGradient27595);fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" y="3"
d="M 20,8 V 36 H 10 l 7,7 h 15 7 c 2.769997,0 5,-2.230003 5,-5 v -5 -1 z" /> ry="2.25" /></g><g
</g> id="layer1"
<g style="stroke-width:2;stroke-dasharray:none"><rect
id="layer1-6"
style="stroke-width:2;stroke-dasharray:none"
transform="matrix(2,0,0,2,0,1)">
<rect
style="fill:#000000;fill-rule:evenodd;stroke-width:1.86898;stroke-dasharray:none;stroke-opacity:0" style="fill:#000000;fill-rule:evenodd;stroke-width:1.86898;stroke-dasharray:none;stroke-opacity:0"
id="rect1410" id="rect1410"
width="14" width="14"
height="2" height="2"
x="5" x="5"
y="15.5" /> y="15.5" /><rect
<rect style="fill:#000000;fill-rule:evenodd;stroke-width:2;stroke-dasharray:none;stroke-opacity:0"
style="fill:#000000;fill-rule:evenodd;stroke-width:2.06559;stroke-dasharray:none;stroke-opacity:0"
id="rect1412" id="rect1412"
width="2" width="2"
height="16" height="15"
x="8" x="9"
y="3.5" /> y="3.9768662" /><path
<path
style="fill:none;fill-rule:evenodd;stroke:#ff0000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" style="fill:none;fill-rule:evenodd;stroke:#ff0000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path1529" id="path1529"
d="m 18,3.5 c 0,7 -4,12 -13,12" /> d="M 18,4 C 18,10.017307 13.40948,15.5 5,15.5" /></g></svg>
</g>
<metadata
id="metadata38894">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:title>LogarithmPlotter Icon</dc:title>
<dc:date>2024-10-06</dc:date>
<dc:creator>
<cc:Agent>
<dc:title>Adsooi &lt;mail@ad5001.eu&gt;</dc:title>
</cc:Agent>
</dc:creator>
<dc:rights>
<cc:Agent>
<dc:title>(c) Adsooi 2021-2024</dc:title>
</cc:Agent>
</dc:rights>
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-nc-sa/4.0/" />
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-nc-sa/4.0/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" />
<cc:prohibits
rdf:resource="http://creativecommons.org/ns#CommercialUse" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#ShareAlike" />
</cc:License>
</rdf:RDF>
</metadata>
</svg>

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

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

View file

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

View file

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

View file

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

View file

@ -1 +0,0 @@
LPFv1{"xzoom":100,"yzoom":10,"xmin":0.2696454905834007,"ymax":33.115625,"xaxisstep":"4","yaxisstep":"π","xaxislabel":"","yaxislabel":"","logscalex":true,"linewidth":1,"showxgrad":true,"showygrad":true,"textsize":18,"history":[[["CreateNewObject",["A","Point",["A",true,"#941A97","name + value","1","0","above","●"]]],["EditedPosition",["A","Point","1","175.36","0","9.9"]],["CreateNewObject",["f","Function",["f",true,"#6E590E","name + value","x","ℝ⁺*","","application","above",1,true,true]]],["EditedProperty",["f","Function","expression","x","((x / 2) - 1)",true]],["CreateNewObject",["t","Text",["t",true,"#118455","null","1","0","center","New text",false]]],["EditedPosition",["t","Text","1","36.48","0","(-13.7)"]],["EditedProperty",["t","Text","text","New text","AEZA",false]],["CreateNewObject",["ω","Point",["ω",true,"#5A3A52","name","1","0","above","●"]]],["CreateNewObject",["G₀","Gain Bode",["G₀",true,"#5A3A52","name + value","ω","high","20","below",1,false]]],["EditedPosition",["ω","Point","1","17.76","0","(-8.9)"]],["EditedProperty",["G₀","Gain Bode","gain","20","10",true]],["EditedProperty",["G₀","Gain Bode","labelPosition","below","below-left",false]],["EditedProperty",["G₀","Gain Bode","pass","high","low",false]],["EditedProperty",["G₀","Gain Bode","labelX",1,62.61,false]],["CreateNewObject",["X","X Cursor",["X",true,"#5909A9","name + value","1",null,"left",true,3,"— — — — — — —","Next to target"]]],["EditedProperty",["X","X Cursor","x","1","5.04",true]],["CreateNewObject",["u","Sequence",["u",true,"#78929E","name + value",true,true,{"1":"n"},{"0":0},"above",1]]],["EditedProperty",["u","Sequence","defaultExpression",{"1":"n"},{"1":"n+1"},false]],["EditedProperty",["u","Sequence","defaultExpression",{"1":"n+1"},{"1":"n+1"},false]],["EditedProperty",["u","Sequence","baseValues",{"0":0},{"0":"-1"},false]],["EditedProperty",["u","Sequence","baseValues",{"0":"-1"},{"0":"-1"},false]],["CreateNewObject",["F_X","Repartition",["F_X",true,"#231931","name + value",{"0":"0"},"above",1]]],["EditedProperty",["F_X","Repartition","labelX",1,12.64,false]],["EditedProperty",["f","Function","labelPosition","above","right",false]],["EditedProperty",["f","Function","labelX",1,30,false]],["EditedProperty",["u","Sequence","labelX",1,3,false]],["EditedProperty",["F_X","Repartition","labelX",12.64,40,false]],["EditedProperty",["ω","Point","labelPosition","above","below",false]],["CreateNewObject",["ω₀","Point",["ω₀","#7C2981","name","name + value","1","0","above","●"]]],["CreateNewObject",["φ₀","Phase Bode",["φ₀",true,"#7C2981","name + value","ω₀","90","°","below",1]]],["EditedPosition",["ω₀","Point","1","3","0","(-8)"]],["EditedPosition",["ω₀","Point","3","2","(-8)","8"]],["EditedProperty",["ω₀","Point","labelPosition","above","above-right",false]],["EditedProperty",["u","Sequence","labelPosition","above","above-left",false]],["EditedProperty",["u","Sequence","labelX",3,20,false]],["EditedProperty",["G","Somme gains Bode","labelX",1,2,false]]],[]],"width":1000,"height":500,"objects":{"Point":[["A",true,"#941A97","name + value","175.36","9.9","above","●"],["ω",true,"#5A3A52","name","17.76","(-8.9)","below","●"],["ω₀",false,"name","name","2","8","above-right","●"]],"Function":[["f",true,"#6E590E","name + value","((x / 2) - 1)","ℝ⁺*","","application","right",30,true,true]],"Text":[["t",true,"#118455","null","36.48","(-13.7)","center","AEZA",false]],"Gain Bode":[["G₀",true,"#5A3A52","name + value","ω","low","10","below-left",62.61,false]],"Somme gains Bode":[["G",true,"#A83C72","name + value","above",2]],"X Cursor":[["X",true,"#5909A9","name + value","5.04",null,"left",true,null,"— — — — — — —","Next to target"]],"Sequence":[["u",true,"#78929E","name + value",true,true,{"1":"n+1"},{"0":"-1"},"above-left",20]],"Repartition":[["F_X",true,"#231931","name + value",{"0":"0"},"above",40]],"Phase Bode":[["φ₀",true,"#7C2981","name + value","ω₀","90","°","below",1]],"Somme phases Bode":[["φ",true,"#A08B14","name + value","above",1]]},"type":"logplotv1"}

View file

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

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
LPFv1{"xzoom":100,"yzoom":100,"xmin":0.5,"ymax":2,"xaxisstep":"4","yaxisstep":"pi/4","xaxislabel":"ω (rad/s)","yaxislabel":"φ (rad)","logscalex":true,"linewidth":2,"showxgrad":true,"showygrad":true,"textsize":20,"history":[[["CreateNewObject",["f","Function",["f",true,"#989E2D","name + value","x","ℝ⁺*","","application","above",1,true,true]]],["EditedProperty",["f","Function","expression","x","(x ^ 20)",true]],["EditedProperty",["f","Function","expression","(x ^ 20)","(20 * (log10 x))",true]],["DeleteObject",["f","Function",["f",true,"#989E2D","name + value","(20 * (log10 x))","ℝ⁺*","","application","above",1,true,true]]],["CreateNewObject",["ω","Point",["ω",true,"#995178","name","1","0","bottom","●"]]],["CreateNewObject",["φ₀","Phase Bode",["φ₀",true,"#995178","name + value","ω","90","°","below",1]]],["EditedProperty",["φ₀","Phase Bode","phase","90","0",true]],["EditedProperty",["φ₀","Phase Bode","unit","°","rad",false]],["EditedProperty",["ω","Point","y","0","((-pi) / 2)",true]],["EditedVisibility",["ω","Point","visible"]],["EditedProperty",["φ₀","Phase Bode","labelX",1,10,false]],["CreateNewObject",["ω₀","Point",["ω₀",true,"#037753","name","1","0","bottom","●"]]],["CreateNewObject",["φ₁","Phase Bode",["φ₁",true,"#037753","name + value","ω₀","90","°","below",1]]],["EditedProperty",["ω₀","Point","x","1","10",true]],["EditedProperty",["φ₁","Phase Bode","unit","°","rad",false]],["EditedProperty",["φ₁","Phase Bode","phase","90","(pi / 2)",true]],["EditedProperty",["ω₀","Point","x","10","5",true]],["EditedProperty",["ω₀","Point","labelPosition","bottom","top-left",false]],["EditedProperty",["φ₁","Phase Bode","labelX",1,2,false]],["EditedProperty",["φ","Somme phases Bode","labelX",1,2,false]],["ColorChanged",["φ","Somme phases Bode","#665B74","#550000"]],["EditedProperty",["ω₀","Point","labelPosition","top-left","above-left",false]],["EditedProperty",["ω₀","Point","labelPosition","above-left","above-right",false]]],[]],"width":1000,"height":500,"objects":{"Function":[],"Point":[["ω",false,"#995178","name","1","((-pi) / 2)","below","●"],["ω₀",true,"#037753","name","5","0","above-right","●"]],"Phase Bode":[["φ₀",true,"#995178","name","ω","0","rad","below",10],["φ₁",true,"#037753","name","ω₀","(pi / 2)","rad","below",2]],"Somme phases Bode":[["φ",true,"#550000","name + value","above",2]]},"type":"logplotv1"}

File diff suppressed because one or more lines are too long

1539
common/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

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

View file

@ -22,7 +22,7 @@ import { babel } from "@rollup/plugin-babel"
import cleanup from "rollup-plugin-cleanup" import cleanup from "rollup-plugin-cleanup"
const src = "./src/index.mjs" const src = "./src/index.mjs"
const dest = "../build/runtime-pyside6/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Common/index.mjs" const dest = "../build/runtime-pyside6/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/index.mjs"
export default { export default {
input: src, input: src,

View file

@ -1,116 +0,0 @@
/**
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 Ad5001
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* We do not inherit the DOM's Event, because not only the DOM part is unnecessary,
* but also because it does not exist within Qt environments.
*/
export class BaseEvent {
___name = ""
/**
* @property {string} name - Name of the event.
*/
constructor(name) {
this.___name = name
}
get name() {
return this.___name
}
}
/**
* Base class for all classes which can emit events.
*/
export class BaseEventEmitter {
static emits = []
/** @type {Record<string, Set<function>>} */
#listeners = {}
constructor() {
for(const eventType of this.constructor.emits) {
this.#listeners[eventType] = new Set()
}
}
/**
* Adds a listener to an event that can be emitted by this object.
*
* @param {string} eventType - Name of the event to listen to. Throws an error if this object does not emit this kind of event.
* @param {function(BaseEvent)} eventListener - The function to be called back when the event is emitted.
*/
on(eventType, eventListener) {
if(eventType.includes(" ")) // Listen to several different events with the same listener.
for(const type of eventType.split(" "))
this.on(type, eventListener)
else {
if(!this.constructor.emits.includes(eventType)) {
const className = this.constructor.name
const eventTypes = this.constructor.emits.join(", ")
throw new Error(`Cannot listen to unknown event ${eventType} in class ${className}. ${className} only emits: ${eventTypes}`)
}
if(!this.#listeners[eventType].has(eventListener))
this.#listeners[eventType].add(eventListener)
}
}
/**
* Removes a listener from an event that can be emitted by this object.
*
* @param {string} eventType - Name of the event that was listened to. Throws an error if this object does not emit this kind of event.
* @param {function(BaseEvent)} eventListener - The function previously registered as a listener.
* @returns {boolean} True if the listener was removed, false if it was not found.
*/
off(eventType, eventListener) {
if(eventType.includes(" ")) { // Unlisten to several different events with the same listener.
let found = false
for(const type of eventType.split(" "))
found ||= this.off(type, eventListener)
return found
} else {
if(!this.constructor.emits.includes(eventType)) {
const className = this.constructor.name
const eventTypes = this.constructor.emits.join(", ")
throw new Error(`Cannot listen to unknown event ${eventType} in class ${className}. ${className} only emits: ${eventTypes}`)
}
return this.#listeners[eventType].delete(eventListener)
}
}
/**
* Emits an event to all of its listeners.
*
* @param {BaseEvent} e
*/
emit(e) {
if(!(e instanceof BaseEvent))
throw new Error("Cannot emit non event object.")
if(!this.constructor.emits.includes(e.name)) {
const className = this.constructor.name
const eventTypes = this.constructor.emits.join(", ")
throw new Error(`Cannot emit event '${e.name}' from class ${className}. ${className} can only emit: ${eventTypes}`)
}
for(const listener of this.#listeners[e.name])
listener(e)
}
}

View file

@ -95,15 +95,11 @@ export class Action {
if(!Latex.enabled) if(!Latex.enabled)
throw new Error("Cannot render an item as LaTeX when LaTeX is disabled.") throw new Error("Cannot render an item as LaTeX when LaTeX is disabled.")
const imgDepth = History.imageDepth const imgDepth = History.imageDepth
const renderArguments = [ const { source, width, height } = await Latex.requestAsyncRender(
latexString, latexString,
imgDepth * (History.fontSize + 2), imgDepth * (History.fontSize + 2),
History.themeTextColor History.themeTextColor
] )
let render = Latex.findPrerendered(...renderArguments)
if(render === null)
render = await Latex.requestAsyncRender(...renderArguments)
const { source, width, height } = render
return `<img src="${source}" width="${width / imgDepth}" height="${height / imgDepth}" style="vertical-align: middle"/>` return `<img src="${source}" width="${width / imgDepth}" height="${height / imgDepth}" style="vertical-align: middle"/>`
} }

View file

@ -19,7 +19,7 @@
import Objects from "../module/objects.mjs" import Objects from "../module/objects.mjs"
import Latex from "../module/latex.mjs" import Latex from "../module/latex.mjs"
import * as MathLib from "../math/index.mjs" import * as MathLib from "../math/index.mjs"
import { escapeHTML } from "../utils/index.mjs" import { escapeHTML } from "../utils.mjs"
import { Action } from "./common.mjs" import { Action } from "./common.mjs"
/** /**

View file

@ -18,11 +18,10 @@
import js from "./lib/polyfills/js.mjs" import js from "./lib/polyfills/js.mjs"
export * as Utils from "./utils/index.mjs" import * as Modules from "./module/index.mjs"
import * as ObjsAutoload from "./objs/autoload.mjs" import * as ObjsAutoload from "./objs/autoload.mjs"
export * as Modules from "./module/index.mjs"
export * as MathLib from "./math/index.mjs" export * as MathLib from "./math/index.mjs"
export * as HistoryLib from "./history/index.mjs" export * as HistoryLib from "./history/index.mjs"
export * as Parsing from "./parsing/index.mjs" export * as Parsing from "./parsing/index.mjs"
export * as Utils from "./utils.mjs"

View file

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

View file

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

View file

@ -210,8 +210,9 @@ export function gamma(n) {
} }
export function stringOrArrayLength(s) { export function stringOrArrayLength(s) {
if(Array.isArray(s)) if(Array.isArray(s)) {
return s.length return s.length
}
return String(s).length return String(s).length
} }
@ -266,6 +267,11 @@ export function roundTo(value, exp) {
return +(value[0] + "e" + (value[1] ? (+value[1] + exp) : exp)) return +(value[0] + "e" + (value[1] ? (+value[1] + exp) : exp))
} }
export function setVar(name, value, variables) {
if(variables) variables[name] = value
return value
}
export function arrayIndex(array, index) { export function arrayIndex(array, index) {
return array[index | 0] return array[index | 0]
} }
@ -290,6 +296,58 @@ export function min(array) {
} }
} }
export function arrayMap(f, a) {
if(typeof f !== "function") {
throw new EvalError(qsTranslate("error", "First argument to map is not a function."))
}
if(!Array.isArray(a)) {
throw new EvalError(qsTranslate("error", "Second argument to map is not an array."))
}
return a.map(function(x, i) {
return f(x, i)
})
}
export function arrayFold(f, init, a) {
if(typeof f !== "function") {
throw new EvalError(qsTranslate("error", "First argument to fold is not a function."))
}
if(!Array.isArray(a)) {
throw new EvalError(qsTranslate("error", "Second argument to fold is not an array."))
}
return a.reduce(function(acc, x, i) {
return f(acc, x, i)
}, init)
}
export function arrayFilter(f, a) {
if(typeof f !== "function") {
throw new EvalError(qsTranslate("error", "First argument to filter is not a function."))
}
if(!Array.isArray(a)) {
throw new EvalError(qsTranslate("error", "Second argument to filter is not an array."))
}
return a.filter(function(x, i) {
return f(x, i)
})
}
export function stringOrArrayIndexOf(target, s) {
if(!(Array.isArray(s) || typeof s === "string")) {
throw new Error(qsTranslate("error", "Second argument to indexOf is not a string or array."))
}
return s.indexOf(target)
}
export function arrayJoin(sep, a) {
if(!Array.isArray(a)) {
throw new Error(qsTranslate("error", "Second argument to join is not an array."))
}
return a.join(sep)
}
export function sign(x) { export function sign(x) {
return ((x > 0) - (x < 0)) || +x return ((x > 0) - (x < 0)) || +x
} }

View file

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

View file

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

View file

@ -17,38 +17,28 @@
*/ */
import * as Utils from "../utils/index.mjs" import * as Utils from "../utils.mjs"
import { ExprEvalExpression } from "../lib/expr-eval/expression.mjs"
import Latex from "../module/latex.mjs" import Latex from "../module/latex.mjs"
import ExprParser from "../module/expreval.mjs" import ExprParser from "../module/expreval.mjs"
import Objects from "../module/objects.mjs" import Objects from "../module/objects.mjs"
const NUMBER_MATCHER = /^\d*\.\d+(e[+-]\d+)?$/
/** /**
* Represents any kind of x-based or non variable based expression. * Represents any kind of x-based or non variable based expression.
*/ */
export class Expression { export class Expression {
/**
*
* @param {string|ExprEvalExpression} expr
*/
constructor(expr) { constructor(expr) {
if(typeof expr === "string") { if(typeof expr === "string") {
this.expr = Utils.exponentsToExpression(expr) this.expr = Utils.exponentsToExpression(expr)
this.calc = ExprParser.parse(this.expr).simplify() this.calc = ExprParser.parse(this.expr).simplify()
} else if(expr instanceof ExprEvalExpression) { } else {
// Passed an expression here directly. // Passed an expression here directly.
this.calc = expr.simplify() this.calc = expr.simplify()
this.expr = expr.toString() this.expr = expr.toString()
} else {
const type = expr != null ? "a " + expr.constructor.name : expr
throw new Error(`Cannot create an expression with ${type}.`)
} }
this.canBeCached = this.isConstant() this.cached = this.isConstant()
this.cachedValue = null this.cachedValue = null
if(this.canBeCached && this.allRequirementsFulfilled()) if(this.cached && this.allRequirementsFulfilled())
this.recache() this.cachedValue = this.calc.evaluate(Objects.currentObjectsByName)
this.latexMarkup = Latex.expression(this.calc.tokens) this.latexMarkup = Latex.expression(this.calc.tokens)
} }
@ -87,20 +77,21 @@ export class Expression {
/** /**
* Returns a list of names whose corresponding objects this expression is dependant on and are missing. * Returns a list of names whose corresponding objects this expression is dependant on and are missing.
* @return {string[]} * @return {boolean}
*/ */
undefinedVariables() { undefinedVariables() {
return this.requiredObjects().filter(objName => !(objName in Objects.currentObjectsByName)) return this.requiredObjects().filter(objName => !(objName in Objects.currentObjectsByName))
} }
recache() { recache() {
this.cachedValue = this.calc.evaluate(Objects.currentObjectsByName) if(this.cached)
this.cachedValue = this.calc.evaluate(Objects.currentObjectsByName)
} }
execute(x = 1) { execute(x = 1) {
if(this.canBeCached) { if(this.cached) {
if(this.cachedValue == null) if(this.cachedValue == null)
this.recache() this.cachedValue = this.calc.evaluate(Objects.currentObjectsByName)
return this.cachedValue return this.cachedValue
} }
ExprParser.currentVars = Object.assign({ "x": x }, Objects.currentObjectsByName) ExprParser.currentVars = Object.assign({ "x": x }, Objects.currentObjectsByName)
@ -108,10 +99,9 @@ export class Expression {
} }
simplify(x) { simplify(x) {
let expr = new Expression(this.calc.substitute("x", x).simplify()) let expr = this.calc.substitute("x", x).simplify()
if(expr.allRequirementsFulfilled() && expr.execute() === 0) if(expr.evaluate() === 0) expr = "0"
expr = new Expression("0") return new Expression(expr)
return expr
} }
toEditableString() { toEditableString() {
@ -120,28 +110,17 @@ export class Expression {
toString(forceSign = false) { toString(forceSign = false) {
let str = Utils.makeExpressionReadable(this.calc.toString()) let str = Utils.makeExpressionReadable(this.calc.toString())
if(str !== undefined && str.match(NUMBER_MATCHER)) { if(str !== undefined && str.match(/^\d*\.\d+$/)) {
const decimals = str.split(".")[1].split("e")[0] if(str.split(".")[1].split("0").length > 7) {
const zeros = decimals.split("0").length
const nines = decimals.split("9").length
if(zeros > 7 || nines > 7) {
// Likely rounding error // Likely rounding error
str = parseFloat(str).toDecimalPrecision(8).toString() str = parseFloat(str.substring(0, str.length - 1)).toString()
} }
} }
if(str[0] === "(" && str.at(-1) === ")") if(str[0] !== "-" && forceSign) str = "+" + str
str = str.substring(1, str.length - 1)
if(str[0] !== "-" && forceSign)
str = "+" + str
return str return str
} }
} }
/**
* Parses and executes the given expression
* @param {string} expr
* @return {number}
*/
export function executeExpression(expr) { export function executeExpression(expr) {
return (new Expression(expr.toString())).execute() return (new Expression(expr.toString())).execute()
} }

View file

@ -17,7 +17,7 @@
*/ */
import * as Expr from "./expression.mjs" import * as Expr from "./expression.mjs"
import * as Utils from "../utils/index.mjs" import * as Utils from "../utils.mjs"
import Latex from "../module/latex.mjs" import Latex from "../module/latex.mjs"
import Objects from "../module/objects.mjs" import Objects from "../module/objects.mjs"
import ExprParser from "../module/expreval.mjs" import ExprParser from "../module/expreval.mjs"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,186 +0,0 @@
/**
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 Ad5001
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Module } from "./common.mjs"
import { BaseEvent } from "../events.mjs"
import { HelperInterface } from "./interface.mjs"
/**
* Base event for when a setting was changed.
*/
class ChangedEvent extends BaseEvent {
/**
*
* @param {string} property - Name of the property that was chagned
* @param {string|number|boolean} oldValue - Old value of the property
* @param {string|number|boolean} newValue - Current (new) value of the property
* @param {boolean} byUser - True if the user is at the source of the change in the setting.
*/
constructor(property, oldValue, newValue, byUser) {
super("changed")
this.property = property
this.oldValue = oldValue
this.newValue = newValue
this.byUser = byUser
}
}
/**
* Module for graph settings.
*/
class SettingsAPI extends Module {
static emits = ["changed"]
#nonConfigurable = ["saveFilename"]
/** @type {Map<string, string|number|boolean>} */
#properties = new Map([
["saveFilename", ""],
["xzoom", 100],
["yzoom", 10],
["xmin", .5],
["ymax", 25],
["xaxisstep", "4"],
["yaxisstep", "4"],
["xlabel", ""],
["ylabel", ""],
["linewidth", 1],
["textsize", 18],
["logscalex", true],
["showxgrad", true],
["showygrad", true]
])
constructor() {
super("Settings", {
helper: HelperInterface
})
}
/**
*
* @param {HelperInterface} helper
*/
initialize({ helper }) {
super.initialize({ helper })
// Initialize default values.
for(const key of this.#properties.keys())
if(!this.#nonConfigurable.includes(key))
this.set(key, helper.getSetting("default_graph."+key), false)
}
/**
* Sets a setting to a given value
*
* @param {string} property
* @param {string|number|boolean} value
* @param {boolean} byUser - Set to true if the user is at the origin of this change.
*/
set(property, value, byUser) {
if(!this.#properties.has(property))
throw new Error(`Property ${property} is not a setting.`)
const oldValue = this.#properties.get(property)
const propType = typeof oldValue
if(byUser)
console.debug("Setting", property, "from", oldValue, "to", value, `(${typeof value}, ${byUser})`)
if(propType !== typeof value)
throw new Error(`Value of ${property} must be a ${propType} (${typeof value} provided).`)
this.#properties.set(property, value)
const evt = new ChangedEvent(property, oldValue, value, byUser === true)
this.emit(evt)
}
/**
* Name of the currently opened file.
* @returns {string}
*/
get saveFilename() { return this.#properties.get("saveFilename") }
/**
* Zoom on the x axis of the diagram.
* @returns {number}
*/
get xzoom() { return this.#properties.get("xzoom") }
/**
* Zoom on the y axis of the diagram.
* @returns {number}
*/
get yzoom() { return this.#properties.get("yzoom") }
/**
* Minimum x of the diagram.
* @returns {number}
*/
get xmin() { return this.#properties.get("xmin") }
/**
* Maximum y of the diagram.
* @returns {number}
*/
get ymax() { return this.#properties.get("ymax") }
/**
* Step of the x axis graduation (expression).
* @note Only available in non-logarithmic mode.
* @returns {string}
*/
get xaxisstep() { return this.#properties.get("xaxisstep") }
/**
* Step of the y axis graduation (expression).
* @returns {string}
*/
get yaxisstep() { return this.#properties.get("yaxisstep") }
/**
* Label used on the x axis.
* @returns {string}
*/
get xlabel() { return this.#properties.get("xlabel") }
/**
* Label used on the y axis.
* @returns {string}
*/
get ylabel() { return this.#properties.get("ylabel") }
/**
* Width of lines that will be drawn into the canvas.
* @returns {number}
*/
get linewidth() { return this.#properties.get("linewidth") }
/**
* Font size of the text that will be drawn into the canvas.
* @returns {number}
*/
get textsize() { return this.#properties.get("textsize") }
/**
* true if the canvas should be in logarithmic mode, false otherwise.
* @returns {boolean}
*/
get logscalex() { return this.#properties.get("logscalex") }
/**
* true if the x graduation should be shown, false otherwise.
* @returns {boolean}
*/
get showxgrad() { return this.#properties.get("showxgrad") }
/**
* true if the y graduation should be shown, false otherwise.
* @returns {boolean}
*/
get showygrad() { return this.#properties.get("showygrad") }
}
Modules.Settings = Modules.Settings || new SettingsAPI()
export default Modules.Settings

View file

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

View file

@ -16,9 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { getRandomColor } from "../utils.mjs"
import Objects from "../module/objects.mjs" import Objects from "../module/objects.mjs"
import Latex from "../module/latex.mjs" import Latex from "../module/latex.mjs"
import { getRandomColor } from "../utils/index.mjs"
import { ensureTypeSafety, serializesByPropertyType } from "../parameters.mjs" import { ensureTypeSafety, serializesByPropertyType } from "../parameters.mjs"
// This file contains the default data to be imported from all other objects // This file contains the default data to be imported from all other objects
@ -224,7 +224,7 @@ export class DrawableObject {
currentObjectsByName[objName].requiredBy.push(this) currentObjectsByName[objName].requiredBy.push(this)
} }
} }
if(this[property].canBeCached && this[property].requiredObjects().length > 0) if(this[property].cached && this[property].requiredObjects().length > 0)
// Recalculate // Recalculate
this[property].recache() this[property].recache()

View file

@ -16,7 +16,7 @@
* 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 { textsub } from "../utils/index.mjs" import { textsub } from "../utils.mjs"
import Objects from "../module/objects.mjs" import Objects from "../module/objects.mjs"
import { ExecutableObject } from "./common.mjs" import { ExecutableObject } from "./common.mjs"
import { parseDomain, Expression, SpecialDomain } from "../math/index.mjs" import { parseDomain, Expression, SpecialDomain } from "../math/index.mjs"
@ -122,9 +122,7 @@ export default class Function extends ExecutableObject {
*/ */
static drawFunction(canvas, expr, definitionDomain, destinationDomain, drawPoints = true, drawDash = true) { static drawFunction(canvas, expr, definitionDomain, destinationDomain, drawPoints = true, drawDash = true) {
let pxprecision = 10 let pxprecision = 10
const startDrawFrom = canvas.x2px(1)%pxprecision-pxprecision let previousX = canvas.px2x(0)
let previousX = canvas.px2x(startDrawFrom)
// console.log("Starting draw from", previousX, startDrawFrom, canvas.x2px(1))
let previousY = null let previousY = null
if(definitionDomain instanceof SpecialDomain && definitionDomain.moveSupported) { if(definitionDomain instanceof SpecialDomain && definitionDomain.moveSupported) {
// Point based functions. // Point based functions.
@ -162,7 +160,7 @@ export default class Function extends ExecutableObject {
// Calculate the previousY at the start of the canvas // Calculate the previousY at the start of the canvas
if(definitionDomain.includes(previousX)) if(definitionDomain.includes(previousX))
previousY = expr.execute(previousX) previousY = expr.execute(previousX)
for(let px = pxprecision; px-pxprecision < canvas.width; px += pxprecision) { for(let px = pxprecision; px < canvas.width; px += pxprecision) {
let currentX = canvas.px2x(px) let currentX = canvas.px2x(px)
if(!definitionDomain.includes(previousX) && definitionDomain.includes(currentX)) { if(!definitionDomain.includes(previousX) && definitionDomain.includes(currentX)) {
// Should draw up to currentX, but NOT at previousX. // Should draw up to currentX, but NOT at previousX.
@ -171,7 +169,7 @@ export default class Function extends ExecutableObject {
do { do {
tmpPx++ tmpPx++
previousX = canvas.px2x(tmpPx) previousX = canvas.px2x(tmpPx)
} while(!definitionDomain.includes(previousX) && currentX > previousX) } while(!definitionDomain.includes(previousX))
// Recaclulate previousY // Recaclulate previousY
previousY = expr.execute(previousX) previousY = expr.execute(previousX)
} else if(!definitionDomain.includes(currentX)) { } else if(!definitionDomain.includes(currentX)) {
@ -181,7 +179,7 @@ export default class Function extends ExecutableObject {
do { do {
tmpPx-- tmpPx--
currentX = canvas.px2x(tmpPx) currentX = canvas.px2x(tmpPx)
} while(!definitionDomain.includes(currentX) && currentX > previousX) } while(!definitionDomain.includes(currentX) && currentX !== previousX)
} }
// This max variation is needed for functions with asymptotical vertical lines (e.g. 1/x, tan x...) // This max variation is needed for functions with asymptotical vertical lines (e.g. 1/x, tan x...)
let maxvariation = (canvas.px2y(0) - canvas.px2y(canvas.height)) let maxvariation = (canvas.px2y(0) - canvas.px2y(canvas.height))

View file

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

View file

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

View file

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

414
common/src/utils.mjs Normal file
View file

@ -0,0 +1,414 @@
/**
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 Ad5001
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// Add string methods
/**
* Replaces latin characters with their uppercase versions.
* @return {string}
*/
String.prototype.toLatinUppercase = String.prototype.toLatinUppercase || function() {
return this.replace(/[a-z]/g, function(match) {
return match.toUpperCase()
})
}
/**
* Removes the 'enclosers' of a string (e.g. quotes, parentheses, brackets...)
* @return {string}
*/
String.prototype.removeEnclosure = function() {
return this.substring(1, this.length - 1)
}
const powerpos = {
"-": "⁻",
"+": "⁺",
"=": "⁼",
" ": "",
"(": "⁽",
")": "⁾",
"0": "⁰",
"1": "¹",
"2": "²",
"3": "³",
"4": "⁴",
"5": "⁵",
"6": "⁶",
"7": "⁷",
"8": "⁸",
"9": "⁹",
"a": "ᵃ",
"b": "ᵇ",
"c": "ᶜ",
"d": "ᵈ",
"e": "ᵉ",
"f": "ᶠ",
"g": "ᵍ",
"h": "ʰ",
"i": "ⁱ",
"j": "ʲ",
"k": "ᵏ",
"l": "ˡ",
"m": "ᵐ",
"n": "ⁿ",
"o": "ᵒ",
"p": "ᵖ",
"r": "ʳ",
"s": "ˢ",
"t": "ᵗ",
"u": "ᵘ",
"v": "ᵛ",
"w": "ʷ",
"x": "ˣ",
"y": "ʸ",
"z": "ᶻ"
}
const exponents = [
"⁰","¹","²","³","⁴","⁵","⁶","⁷","⁸","⁹"
]
const exponentReg = new RegExp('(['+exponents.join('')+']+)', 'g')
const indicepos = {
"-": "₋",
"+": "₊",
"=": "₌",
"(": "₍",
")": "₎",
" ": "",
"0": "₀",
"1": "₁",
"2": "₂",
"3": "₃",
"4": "₄",
"5": "₅",
"6": "₆",
"7": "₇",
"8": "₈",
"9": "₉",
"a": "ₐ",
"e": "ₑ",
"h": "ₕ",
"i": "ᵢ",
"j": "ⱼ",
"k": "ₖ",
"l": "ₗ",
"m": "ₘ",
"n": "ₙ",
"o": "ₒ",
"p": "ₚ",
"r": "ᵣ",
"s": "ₛ",
"t": "ₜ",
"u": "ᵤ",
"v": "ᵥ",
"x": "ₓ",
}
// Put a text in sup position
export function textsup(text) {
let ret = ""
text = text.toString()
for (let i = 0; i < text.length; i++) {
if(Object.keys(powerpos).indexOf(text[i]) >= 0) {
ret += powerpos[text[i]]
} else {
ret += text[i]
}
}
return ret
}
// Put a text in sub position
export function textsub(text) {
let ret = ""
text = text.toString()
for (let i = 0; i < text.length; i++) {
if(Object.keys(indicepos).indexOf(text[i]) >= 0) {
ret += indicepos[text[i]]
} else {
ret += text[i]
}
}
return ret
}
/**
* Simplifies (mathematically) a mathematical expression.
* @param {string} str - Expression to parse
* @returns {string}
*/
export function simplifyExpression(str) {
let replacements = [
// Operations not done by parser.
// [// Decomposition way 2
// /(^|[+-] |\()([-.\d\w]+) ([*/]) \((([-.\d\w] [*/] )?[-\d\w.]+) ([+\-]) (([-.\d\w] [*/] )?[\d\w.+]+)\)($| [+-]|\))/g,
// "$1$2 $3 $4 $6 $2 $3 $7$9"
// ],
// [ // Decomposition way 2
// /(^|[+-] |\()\((([-.\d\w] [*/] )?[-\d\w.]+) ([+\-]) (([-.\d\w] [*/] )?[\d\w.+]+)\) ([*/]) ([-.\d\w]+)($| [+-]|\))/g,
// "$1$2 $7 $8 $4 $5 $7 $8$9"
// ],
[ // Factorisation of π elements.
/(([-\d\w.]+ [*/] )*)(pi|π)(( [/*] [-\d\w.]+)*) ([+-]) (([-\d\w.]+ [*/] )*)(pi|π)(( [/*] [-\d\w.]+)*)?/g,
function(match, m1, n1, pi1, m2, ope2, n2, opeM, m3, n3, pi2, m4, ope4, n4) {
// g1, g2, g3 , g4, g5, g6, g7, g8, g9, g10, g11,g12 , g13
// We don't care about mx & pix, ope2 & ope4 are either / or * for n2 & n4.
// n1 & n3 are multiplied, opeM is the main operation (- or +).
// Putting all n in form of number
//n2 = n2 == undefined ? 1 : parseFloat(n)
n1 = m1 === undefined ? 1 : eval(m1 + '1')
n2 = m2 === undefined ? 1 : eval('1' + m2)
n3 = m3 === undefined ? 1 : eval(m3 + '1')
n4 = m4 === undefined ? 1 : eval('1' + m4)
//let [n1, n2, n3, n4] = [n1, n2, n3, n4].map(n => n == undefined ? 1 : parseFloat(n))
// Falling back to * in case it does not exist (the corresponding n would be 1)
[ope2, ope4] = [ope2, ope4].map(ope => ope === '/' ? '/' : '*')
let coeff1 = n1*n2
let coeff2 = n3*n4
let coefficient = coeff1+coeff2-(opeM === '-' ? 2*coeff2 : 0)
return `${coefficient} * π`
}
],
[ // Removing parenthesis when content is only added from both sides.
/(^|[+-] |\()\(([^)(]+)\)($| [+-]|\))/g,
function(match, b4, middle, after) {return `${b4}${middle}${after}`}
],
[ // Removing parenthesis when content is only multiplied.
/(^|[*\/] |\()\(([^)(+-]+)\)($| [*\/+-]|\))/g,
function(match, b4, middle, after) {return `${b4}${middle}${after}`}
],
[ // Removing parenthesis when content is only multiplied.
/(^|[*\/+-] |\()\(([^)(+-]+)\)($| [*\/]|\))/g,
function(match, b4, middle, after) {return `${b4}${middle}${after}`}
],
[// Simplification additions/subtractions.
/(^|[^*\/] |\()([-.\d]+) [+-] (\([^)(]+\)|[^)(]+) [+-] ([-.\d]+)($| [^*\/]|\))/g,
function(match, b4, n1, op1, middle, op2, n2, after) {
let total
if(op2 === '+') {
total = parseFloat(n1) + parseFloat(n2)
} else {
total = parseFloat(n1) - parseFloat(n2)
}
return `${b4}${total} ${op1} ${middle}${after}`
}
],
[// Simplification multiplications/divisions.
/([-.\d]+) [*\/] (\([^)(]+\)|[^)(+-]+) [*\/] ([-.\d]+)/g,
function(match, n1, op1, middle, op2, n2) {
if(parseInt(n1) === n1 && parseInt(n2) === n2 && op2 === '/' &&
(parseInt(n1) / parseInt(n2)) % 1 !== 0) {
// Non int result for int division.
return `(${n1} / ${n2}) ${op1} ${middle}`
} else {
if(op2 === '*') {
return `${parseFloat(n1) * parseFloat(n2)} ${op1} ${middle}`
} else {
return `${parseFloat(n1) / parseFloat(n2)} ${op1} ${middle}`
}
}
}
],
[// Starting & ending parenthesis if not needed.
/^\s*\((.*)\)\s*$/g,
function(match, middle) {
let str = middle
// Replace all groups
while(/\([^)(]+\)/g.test(str))
str = str.replace(/\([^)(]+\)/g, '')
// There shouldn't be any more parenthesis
// If there is, that means the 2 parenthesis are needed.
if(!str.includes(')') && !str.includes('(')) {
return middle
} else {
return `(${middle})`
}
}
],
// Simple simplifications
// [/(\s|^|\()0(\.0+)? \* (\([^)(]+\))/g, '$10'],
// [/(\s|^|\()0(\.0+)? \* ([^)(+-]+)/g, '$10'],
// [/(\([^)(]\)) \* 0(\.0+)?(\s|$|\))/g, '0$3'],
// [/([^)(+-]) \* 0(\.0+)?(\s|$|\))/g, '0$3'],
// [/(\s|^|\()1(\.0+)? [\*\/] /g, '$1'],
// [/(\s|^|\()0(\.0+)? (\+|\-) /g, '$1'],
// [/ [\*\/] 1(\.0+)?(\s|$|\))/g, '$3'],
// [/ (\+|\-) 0(\.0+)?(\s|$|\))/g, '$3'],
// [/(^| |\() /g, '$1'],
// [/ ($|\))/g, '$1'],
]
// Replacements
let found
do {
found = false
for(let replacement of replacements)
while(replacement[0].test(str)) {
found = true
str = str.replace(replacement[0], replacement[1])
}
} while(found)
return str
}
/**
* Transforms a mathematical expression to make it readable by humans.
* NOTE: Will break parsing of expression.
* @param {string} str - Expression to parse.
* @returns {string}
*/
export function makeExpressionReadable(str) {
let replacements = [
// letiables
[/pi/g, 'π'],
[/Infinity/g, '∞'],
[/inf/g, '∞'],
// Other
[/ \* /g, '×'],
[/ \^ /g, '^'],
[/\^\(([\d\w+-]+)\)/g, function(match, p1) { return textsup(p1) }],
[/\^([\d\w+-]+)/g, function(match, p1) { return textsup(p1) }],
[/_\(([\d\w+-]+)\)/g, function(match, p1) { return textsub(p1) }],
[/_([\d\w+-]+)/g, function(match, p1) { return textsub(p1) }],
[/\[([^\[\]]+)\]/g, function(match, p1) { return textsub(p1) }],
[/(\d|\))×/g, '$1'],
[/integral\((.+),\s?(.+),\s?["'](.+)["'],\s?["'](.+)["']\)/g, function(match, a, b, p1, body, p2, p3, by, p4) {
if(a.length < b.length) {
return `${textsub(a)}${textsup(b)} ${body} d${by}`
} else {
return `${textsup(b)}${textsub(a)} ${body} d${by}`
}
}],
[/derivative\(?["'](.+)["'], ?["'](.+)["'], ?(.+)\)?/g, function(match, p1, body, p2, p3, by, p4, x) {
return `d(${body.replace(new RegExp(by, 'g'), 'x')})/dx`
}]
]
// str = simplifyExpression(str)
// Replacements
for(let replacement of replacements)
while(replacement[0].test(str))
str = str.replace(replacement[0], replacement[1])
return str
}
/**
* Parses a variable name to make it readable by humans.
*
* @param {string} str - Variable name to parse
* @param {boolean} removeUnallowed - Remove domain symbols disallowed in name.
* @returns {string} - The parsed name
*/
export function parseName(str, removeUnallowed = true) {
let replacements = [
// Greek letters
[/([^a-z]|^)al(pha)?([^a-z]|$)/g, '$1α$3'],
[/([^a-z]|^)be(ta)?([^a-z]|$)/g, '$1β$3'],
[/([^a-z]|^)ga(mma)?([^a-z]|$)/g, '$1γ$3'],
[/([^a-z]|^)de(lta)?([^a-z]|$)/g, '$1δ$3'],
[/([^a-z]|^)ep(silon)?([^a-z]|$)/g, '$1ε$3'],
[/([^a-z]|^)ze(ta)?([^a-z]|$)/g, '$1ζ$3'],
[/([^a-z]|^)et(a)?([^a-z]|$)/g, '$1η$3'],
[/([^a-z]|^)th(eta)?([^a-z]|$)/g, '$1θ$3'],
[/([^a-z]|^)io(ta)?([^a-z]|$)/g, '$1ι$3'],
[/([^a-z]|^)ka(ppa)([^a-z]|$)?/g, '$1κ$3'],
[/([^a-z]|^)la(mbda)?([^a-z]|$)/g, '$1λ$3'],
[/([^a-z]|^)mu([^a-z]|$)/g, '$1μ$2'],
[/([^a-z]|^)nu([^a-z]|$)/g, '$1ν$2'],
[/([^a-z]|^)xi([^a-z]|$)/g, '$1ξ$2'],
[/([^a-z]|^)rh(o)?([^a-z]|$)/g, '$1ρ$3'],
[/([^a-z]|^)si(gma)?([^a-z]|$)/g, '$1σ$3'],
[/([^a-z]|^)ta(u)?([^a-z]|$)/g, '$1τ$3'],
[/([^a-z]|^)up(silon)?([^a-z]|$)/g, '$1υ$3'],
[/([^a-z]|^)ph(i)?([^a-z]|$)/g, '$1φ$3'],
[/([^a-z]|^)ch(i)?([^a-z]|$)/g, '$1χ$3'],
[/([^a-z]|^)ps(i)?([^a-z]|$)/g, '$1ψ$3'],
[/([^a-z]|^)om(ega)?([^a-z]|$)/g, '$1ω$3'],
// Capital greek letters
[/([^a-z]|^)gga(mma)?([^a-z]|$)/g, '$1Γ$3'],
[/([^a-z]|^)gde(lta)?([^a-z]|$)/g, '$1Δ$3'],
[/([^a-z]|^)gth(eta)?([^a-z]|$)/g, '$1Θ$3'],
[/([^a-z]|^)gla(mbda)?([^a-z]|$)/g, '$1Λ$3'],
[/([^a-z]|^)gxi([^a-z]|$)/g, '$1Ξ$2'],
[/([^a-z]|^)gpi([^a-z]|$)/g, '$1Π$2'],
[/([^a-z]|^)gsi(gma)([^a-z]|$)?/g, '$1Σ$3'],
[/([^a-z]|^)gph(i)?([^a-z]|$)/g, '$1Φ$3'],
[/([^a-z]|^)gps(i)?([^a-z]|$)/g, '$1Ψ$3'],
[/([^a-z]|^)gom(ega)?([^a-z]|$)/g, '$1Ω$3'],
// Underscores
// [/_\(([^_]+)\)/g, function(match, p1) { return textsub(p1) }],
// [/_([^" ]+)/g, function(match, p1) { return textsub(p1) }],
// Array elements
[/\[([^\]\[]+)\]/g, function(match, p1) { return textsub(p1) }],
// Removing
[/[xπ\\∪∩\]\[ ()^/÷*×+=\d-]/g , ''],
]
if(!removeUnallowed) replacements.pop()
// Replacements
for(let replacement of replacements)
str = str.replace(replacement[0], replacement[1])
return str
}
/**
* Transforms camel case strings to a space separated one.
*
* @param {string} label - Camel case to parse
* @returns {string} Parsed label.
*/
export function camelCase2readable(label) {
let parsed = parseName(label, false)
return parsed.charAt(0).toLatinUppercase() + parsed.slice(1).replace(/([A-Z])/g," $1")
}
/**
* Creates a randomized color string.
* @returns {string}
*/
export function getRandomColor() {
let clrs = '0123456789ABCDEF';
let color = '#';
for(let i = 0; i < 6; i++) {
color += clrs[Math.floor(Math.random() * (16-5*(i%2===0)))];
}
return color;
}
/**
* Escapes text to html entities.
* @param {string} str
* @returns {string}
*/
export function escapeHTML(str) {
return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;') ;
}
/**
* Parses exponents and replaces them with expression values
* @param {string} expression - The expression to replace in.
* @return {string} The parsed expression
*/
export function exponentsToExpression(expression) {
return expression.replace(exponentReg, (m, exp) => '^' + exp.split('').map((x) => exponents.indexOf(x)).join(''))
}

View file

@ -1,258 +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 { textsub, textsup } from "./subsup.mjs"
/**
* Simplifies (mathematically) a mathematical expression.
* @deprecated
* @param {string} str - Expression to parse
* @returns {string}
*/
export function simplifyExpression(str) {
let replacements = [
// Operations not done by parser.
// [// Decomposition way 2
// /(^|[+-] |\()([-.\d\w]+) ([*/]) \((([-.\d\w] [*/] )?[-\d\w.]+) ([+\-]) (([-.\d\w] [*/] )?[\d\w.+]+)\)($| [+-]|\))/g,
// "$1$2 $3 $4 $6 $2 $3 $7$9"
// ],
// [ // Decomposition way 2
// /(^|[+-] |\()\((([-.\d\w] [*/] )?[-\d\w.]+) ([+\-]) (([-.\d\w] [*/] )?[\d\w.+]+)\) ([*/]) ([-.\d\w]+)($| [+-]|\))/g,
// "$1$2 $7 $8 $4 $5 $7 $8$9"
// ],
[ // Factorisation of π elements.
/(([-\d\w.]+ [*/] )*)(pi|π)(( [/*] [-\d\w.]+)*) ([+-]) (([-\d\w.]+ [*/] )*)(pi|π)(( [/*] [-\d\w.]+)*)?/g,
function(match, m1, n1, pi1, m2, ope2, n2, opeM, m3, n3, pi2, m4, ope4, n4) {
// g1, g2, g3 , g4, g5, g6, g7, g8, g9, g10, g11,g12 , g13
// We don't care about mx & pix, ope2 & ope4 are either / or * for n2 & n4.
// n1 & n3 are multiplied, opeM is the main operation (- or +).
// Putting all n in form of number
//n2 = n2 == undefined ? 1 : parseFloat(n)
n1 = m1 === undefined ? 1 : eval(m1 + "1")
n2 = m2 === undefined ? 1 : eval("1" + m2)
n3 = m3 === undefined ? 1 : eval(m3 + "1")
n4 = m4 === undefined ? 1 : eval("1" + m4)
//let [n1, n2, n3, n4] = [n1, n2, n3, n4].map(n => n == undefined ? 1 : parseFloat(n))
// Falling back to * in case it does not exist (the corresponding n would be 1)
[ope2, ope4] = [ope2, ope4].map(ope => ope === "/" ? "/" : "*")
let coeff1 = n1 * n2
let coeff2 = n3 * n4
let coefficient = coeff1 + coeff2 - (opeM === "-" ? 2 * coeff2 : 0)
return `${coefficient} * π`
}
],
[ // Removing parenthesis when content is only added from both sides.
/(^|[+-] |\()\(([^)(]+)\)($| [+-]|\))/g,
function(match, b4, middle, after) {
return `${b4}${middle}${after}`
}
],
[ // Removing parenthesis when content is only multiplied.
/(^|[*\/] |\()\(([^)(+-]+)\)($| [*\/+-]|\))/g,
function(match, b4, middle, after) {
return `${b4}${middle}${after}`
}
],
[ // Removing parenthesis when content is only multiplied.
/(^|[*\/+-] |\()\(([^)(+-]+)\)($| [*\/]|\))/g,
function(match, b4, middle, after) {
return `${b4}${middle}${after}`
}
],
[// Simplification additions/subtractions.
/(^|[^*\/] |\()([-.\d]+) [+-] (\([^)(]+\)|[^)(]+) [+-] ([-.\d]+)($| [^*\/]|\))/g,
function(match, b4, n1, op1, middle, op2, n2, after) {
let total
if(op2 === "+") {
total = parseFloat(n1) + parseFloat(n2)
} else {
total = parseFloat(n1) - parseFloat(n2)
}
return `${b4}${total} ${op1} ${middle}${after}`
}
],
[// Simplification multiplications/divisions.
/([-.\d]+) [*\/] (\([^)(]+\)|[^)(+-]+) [*\/] ([-.\d]+)/g,
function(match, n1, op1, middle, op2, n2) {
if(parseInt(n1) === n1 && parseInt(n2) === n2 && op2 === "/" &&
(parseInt(n1) / parseInt(n2)) % 1 !== 0) {
// Non int result for int division.
return `(${n1} / ${n2}) ${op1} ${middle}`
} else {
if(op2 === "*") {
return `${parseFloat(n1) * parseFloat(n2)} ${op1} ${middle}`
} else {
return `${parseFloat(n1) / parseFloat(n2)} ${op1} ${middle}`
}
}
}
],
[// Starting & ending parenthesis if not needed.
/^\s*\((.*)\)\s*$/g,
function(match, middle) {
let str = middle
// Replace all groups
while(/\([^)(]+\)/g.test(str))
str = str.replace(/\([^)(]+\)/g, "")
// There shouldn't be any more parenthesis
// If there is, that means the 2 parenthesis are needed.
if(!str.includes(")") && !str.includes("(")) {
return middle
} else {
return `(${middle})`
}
}
]
// Simple simplifications
// [/(\s|^|\()0(\.0+)? \* (\([^)(]+\))/g, '$10'],
// [/(\s|^|\()0(\.0+)? \* ([^)(+-]+)/g, '$10'],
// [/(\([^)(]\)) \* 0(\.0+)?(\s|$|\))/g, '0$3'],
// [/([^)(+-]) \* 0(\.0+)?(\s|$|\))/g, '0$3'],
// [/(\s|^|\()1(\.0+)? [\*\/] /g, '$1'],
// [/(\s|^|\()0(\.0+)? (\+|\-) /g, '$1'],
// [/ [\*\/] 1(\.0+)?(\s|$|\))/g, '$3'],
// [/ (\+|\-) 0(\.0+)?(\s|$|\))/g, '$3'],
// [/(^| |\() /g, '$1'],
// [/ ($|\))/g, '$1'],
]
// Replacements
let found
do {
found = false
for(let replacement of replacements)
while(replacement[0].test(str)) {
found = true
str = str.replace(replacement[0], replacement[1])
}
} while(found)
return str
}
/**
* Transforms a mathematical expression to make it readable by humans.
* NOTE: Will break parsing of expression.
* @deprecated
* @param {string} str - Expression to parse.
* @returns {string}
*/
export function makeExpressionReadable(str) {
let replacements = [
// letiables
[/pi/g, "π"],
[/Infinity/g, "∞"],
[/inf/g, "∞"],
// Other
[/ \* /g, "×"],
[/ \^ /g, "^"],
[/\^\(([\d\w+-]+)\)/g, function(match, p1) {
return textsup(p1)
}],
[/\^([\d\w+-]+)/g, function(match, p1) {
return textsup(p1)
}],
[/_\(([\d\w+-]+)\)/g, function(match, p1) {
return textsub(p1)
}],
[/_([\d\w+-]+)/g, function(match, p1) {
return textsub(p1)
}],
[/\[([^\[\]]+)\]/g, function(match, p1) {
return textsub(p1)
}],
[/(\d|\))×/g, "$1"],
[/integral\((.+),\s?(.+),\s?["'](.+)["'],\s?["'](.+)["']\)/g, function(match, a, b, p1, body, p2, p3, by, p4) {
if(a.length < b.length) {
return `${textsub(a)}${textsup(b)} ${body} d${by}`
} else {
return `${textsup(b)}${textsub(a)} ${body} d${by}`
}
}],
[/derivative\(?["'](.+)["'], ?["'](.+)["'], ?(.+)\)?/g, function(match, p1, body, p2, p3, by, p4, x) {
return `d(${body.replace(new RegExp(by, "g"), "x")})/dx`
}]
]
// str = simplifyExpression(str)
// Replacements
for(let replacement of replacements)
while(replacement[0].test(str))
str = str.replace(replacement[0], replacement[1])
return str
}
/** @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.
*
* @param {string} str - Variable name to parse
* @param {boolean} removeUnallowed - Remove domain symbols disallowed in name.
* @returns {string} - The parsed name
*/
export function parseName(str, removeUnallowed = true) {
for(const replacement of replacements)
str = str.replace(replacement[0], replacement[1])
if(removeUnallowed)
str = str.replace(/[xnπ\\∪∩\]\[ ()^/÷*×+=\d¹²³⁴⁵⁶⁷⁸⁹⁰-]/g, "")
return str
}

View file

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

View file

@ -1,41 +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/>.
*/
/**
* Creates a randomized color string.
* @returns {string}
*/
export function getRandomColor() {
let clrs = "0123456789ABCDEF"
let color = "#"
for(let i = 0; i < 6; i++) {
color += clrs[Math.floor(Math.random() * (16 - 5 * (i % 2 === 0)))]
}
return color
}
/**
* Escapes text to html entities.
* @param {string} str
* @returns {string}
*/
export function escapeHTML(str) {
return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
}

View file

@ -1,51 +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/>.
*/
// Add string methods
/**
* Replaces latin characters with their uppercase versions.
* @return {string}
*/
String.prototype.toLatinUppercase = function() {
return this.replace(/[a-z]/g, function(match) {
return match.toUpperCase()
})
}
/**
* Removes the first and last character of a string
* Used to remove enclosing characters like quotes, parentheses, brackets...
* @note Does NOT check for their existence ahead of time.
* @return {string}
*/
String.prototype.removeEnclosure = function() {
return this.substring(1, this.length - 1)
}
/**
* Rounds to a certain number of decimal places.
* From https://stackoverflow.com/a/48764436
*
* @param {number} decimalPlaces
* @return {number}
*/
Number.prototype.toDecimalPrecision = function(decimalPlaces = 0) {
const p = Math.pow(10, decimalPlaces)
const n = (this * p) * (1 + Number.EPSILON)
return Math.round(n) / p
}

View file

@ -1,140 +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/>.
*/
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 EXPONENTS_REG = new RegExp("([" + EXPONENTS.join("") + "]+)", "g")
/**
* Put a text in sup position
* @param {string} text
* @return {string}
*/
export function textsup(text) {
let ret = ""
text = text.toString()
for(let letter of text)
ret += CHARACTER_TO_POWER.has(letter) ? CHARACTER_TO_POWER.get(letter) : letter
return ret
}
/**
* Put a text in sub position
* @param {string} text
* @return {string}
*/
export function textsub(text) {
let ret = ""
text = text.toString()
for(let letter of text)
ret += CHARACTER_TO_INDICE.has(letter) ? CHARACTER_TO_INDICE.get(letter) : letter
return ret
}
/**
* Parses exponents and replaces them with expression values
* @param {string} expression - The expression to replace in.
* @return {string} The parsed expression
*/
export function exponentsToExpression(expression) {
return expression.replace(EXPONENTS_REG, (m, exp) => "^" + exp.split("").map((x) => EXPONENTS.indexOf(x)).join(""))
}

View file

@ -1,118 +0,0 @@
/**
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 Ad5001
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { describe, it } from "mocha"
import { expect } from "chai"
const { spy } = chaiPlugins
import { BaseEventEmitter, BaseEvent } from "../../src/events.mjs"
class MockEmitter extends BaseEventEmitter {
static emits = ["example1", "example2"]
}
class MockEvent1 extends BaseEvent {
constructor() {
super("example1")
}
}
class MockEvent2 extends BaseEvent {
constructor(parameter) {
super("example2")
this.parameter = parameter
}
}
describe("Lib/EventsEmitters", function() {
it("sends events with unique and readonly names", function() {
const event = new MockEvent1()
expect(event.name).to.equal("example1")
expect(() => event.name = "not").to.throw()
})
it("forwards events to all of its listeners", function() {
const emitter = new MockEmitter()
const listener1 = spy()
const listener2 = spy()
emitter.on("example1", listener1)
emitter.on("example1", listener2)
emitter.emit(new MockEvent1())
expect(listener1).to.have.been.called.once
expect(listener2).to.have.been.called.once
})
it("forwards multiple events to a singular listener", function() {
const emitter = new MockEmitter()
const listener = spy()
const mockEvent1 = new MockEvent1()
const mockEvent2 = new MockEvent2(3)
emitter.on("example1 example2", listener)
emitter.emit(mockEvent1)
emitter.emit(mockEvent2)
expect(listener).to.have.been.called.twice
expect(listener).to.have.been.first.called.with.exactly(mockEvent1)
expect(listener).to.have.been.second.called.with.exactly(mockEvent2)
})
it("is able to have listeners removed", function() {
const emitter = new MockEmitter()
const listener = spy()
emitter.on("example1", listener)
const removedFromEventItDoesntListenTo = emitter.off("example2", listener)
const removedFromEventItListensTo = emitter.off("example1", listener)
const removedFromEventASecondTime = emitter.off("example1", listener)
expect(removedFromEventItDoesntListenTo).to.be.false
expect(removedFromEventItListensTo).to.be.true
expect(removedFromEventASecondTime).to.be.false
emitter.on("example1 example2", listener)
const removedFromBothEvents = emitter.off("example1 example2", listener)
expect(removedFromBothEvents).to.be.true
emitter.on("example1", listener)
const removedFromOneOfTheEvents = emitter.off("example1 example2", listener)
expect(removedFromOneOfTheEvents).to.be.true
})
it("is able to have one listener's listening to a single event removed when said listener listens to multiple", function() {
const emitter = new MockEmitter()
const listener = spy()
const mockEvent1 = new MockEvent1()
const mockEvent2 = new MockEvent2(3)
emitter.on("example1 example2", listener)
// Disable listener for example1
emitter.off("example1", listener)
emitter.emit(mockEvent1)
emitter.emit(mockEvent2)
expect(listener).to.have.been.called.once
expect(listener).to.also.have.been.called.with.exactly(mockEvent2)
})
it("is not able to emit or add/remove listeners for inexistant events", function() {
const emitter = new MockEmitter()
const listener = spy()
expect(() => emitter.on("inexistant", listener)).to.throw(Error)
expect(() => emitter.off("inexistant", listener)).to.throw(Error)
expect(() => emitter.emit(new BaseEvent("inexistant"))).to.throw(Error)
})
it("isn't able to emit non-events", function() {
const emitter = new MockEmitter()
expect(() => emitter.emit("not-an-event")).to.throw(Error)
})
})

View file

@ -1,58 +0,0 @@
/**
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 Ad5001
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { describe, it } from "mocha"
import { expect } from "chai"
import { MockLatex } from "../mock/latex.mjs"
import { MockHelper } from "../mock/helper.mjs"
import {
CanvasInterface,
DialogInterface,
HelperInterface,
Interface,
LatexInterface,
RootInterface
} from "../../src/module/interface.mjs"
import { MockDialog } from "../mock/dialog.mjs"
import { MockRootElement } from "../mock/root.mjs"
import { MockCanvas } from "../mock/canvas.mjs"
describe("Interfaces", function() {
describe("#interface methods", function() {
it("throws an error when called directly", function() {
const obj = new CanvasInterface()
expect(() => obj.markDirty()).to.throw(Error)
})
})
describe("#checkImplementation", function() {
it("validates the implementation of mocks", function() {
const checkMockLatex = () => Interface.checkImplementation(LatexInterface, new MockLatex())
const checkMockHelper = () => Interface.checkImplementation(HelperInterface, new MockHelper())
const checkMockDialog = () => Interface.checkImplementation(DialogInterface, new MockDialog())
const checkMockRoot = () => Interface.checkImplementation(RootInterface, new MockRootElement())
const checkMockCanvas = () => Interface.checkImplementation(CanvasInterface, new MockCanvas())
expect(checkMockLatex).to.not.throw()
expect(checkMockHelper).to.not.throw()
expect(checkMockDialog).to.not.throw()
expect(checkMockRoot).to.not.throw()
expect(checkMockCanvas).to.not.throw()
})
})
})

View file

@ -1,232 +0,0 @@
/**
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 Ad5001
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// Load prior tests
import { describe, it } from "mocha"
import { expect } from "chai"
import * as Polyfill from "../../src/lib/expr-eval/polyfill.mjs"
import {
andOperator,
cbrt,
equal,
expm1,
hypot,
lessThan,
log1p,
log2,
notEqual
} from "../../src/lib/expr-eval/polyfill.mjs"
describe("Math/Polyfill", () => {
describe("#add", function() {
it("adds two numbers", function() {
expect(Polyfill.add(2, 3)).to.equal(5)
expect(Polyfill.add("2", "3")).to.equal(5)
})
})
describe("#sub", function() {
it("subtracts two numbers", function() {
expect(Polyfill.sub(2, 1)).to.equal(1)
expect(Polyfill.sub("2", "1")).to.equal(1)
})
})
describe("#mul", function() {
it("multiplies two numbers", function() {
expect(Polyfill.mul(2, 3)).to.equal(6)
expect(Polyfill.mul("2", "3")).to.equal(6)
})
})
describe("#div", function() {
it("divides two numbers", function() {
expect(Polyfill.div(10, 2)).to.equal(5)
expect(Polyfill.div("10", "2")).to.equal(5)
})
})
describe("#mod", function() {
it("returns 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("returns 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("returns 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("returns 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("returns 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("returns 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("returns 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("returns 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("returns 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("returns 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("checks 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("returns 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("returns 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("returns the hypothenus length of a triangle whose length are provided in arguments", function() {
for(let x = 0; x < 10; x += 0.3) {
expect(Polyfill.hypot(x)).to.be.approximately(Math.hypot(x), Number.EPSILON)
for(let y = 0; y < 10; y += 0.3) {
expect(Polyfill.hypot(x, y)).to.be.approximately(Math.hypot(x, y), Number.EPSILON)
}
}
})
})
describe("#sign, #cbrt, #exmp1", function() {
for(let x = -10; x < 10; x += 0.3) {
expect(Polyfill.sign(x)).to.approximately(Math.sign(x), 1e-12)
expect(Polyfill.cbrt(x)).to.approximately(Math.cbrt(x), 1e-12)
expect(Polyfill.expm1(x)).to.approximately(Math.expm1(x), 1e-12)
}
})
describe("#log1p, #log2", function() {
for(let x = 1; x < 10; x += 0.3) {
expect(Polyfill.log1p(x)).to
.be.approximately(Math.log1p(x), 1e-12)
expect(Polyfill.log2(x)).to.be.approximately(Math.log2(x), 1e-12)
}
})
})

View file

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

View file

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

View file

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

View file

@ -1,181 +0,0 @@
/**
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 Ad5001
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// Load prior tests
import "../basics/utils.mjs"
import "../module/latex.mjs"
import "../module/expreval.mjs"
import "../module/objects.mjs"
import { describe, it } from "mocha"
import { expect } from "chai"
import { executeExpression, Expression } from "../../src/math/expression.mjs"
import ExprEval from "../../src/module/expreval.mjs"
describe("Math/Expression", function() {
describe("#constructor", function() {
it("accepts strings", function() {
expect(() => new Expression("2+3")).to.not.throw
expect(() => new Expression("x+2")).to.not.throw
})
it("accepts already parsed expressions", function() {
expect(() => new Expression(ExprEval.parse("2+3"))).to.not.throw
expect(() => new Expression(ExprEval.parse("x+2"))).to.not.throw
})
it("doesn't accept anything else", function() {
expect(() => new Expression()).to.throw("Cannot create an expression with undefined.")
expect(() => new Expression(12)).to.throw("Cannot create an expression with a Number.")
expect(() => new Expression({})).to.throw("Cannot create an expression with a Object.")
expect(() => new Expression(true)).to.throw("Cannot create an expression with a Boolean.")
})
})
describe("#variables", function() {
it("returns a list of variables for non-constant expressions", function() {
expect(new Expression("x+1").variables()).to.deep.equal(["x"])
expect(new Expression("x+n").variables()).to.deep.equal(["x", "n"])
expect(new Expression("u[n] + A.x").variables()).to.deep.equal(["u", "n", "A"])
})
it("returns an empty array if the expression is constant", function() {
expect(new Expression("2+1").variables()).to.deep.equal([])
expect(new Expression("sin π").variables()).to.deep.equal([])
expect(new Expression("e^3").variables()).to.deep.equal([])
})
})
describe("#isConstant", function() {
it("returns true if neither x nor n are included into the expression", function() {
expect(new Expression("2+1").isConstant()).to.be.true
expect(new Expression("e^3").isConstant()).to.be.true
expect(new Expression("2+f(3)").isConstant()).to.be.true
expect(new Expression("sin A.x").isConstant()).to.be.true
})
it("returns false if either x or n are included into the expression", function() {
expect(new Expression("2+x").isConstant()).to.be.false
expect(new Expression("e^n").isConstant()).to.be.false
expect(new Expression("2+f(x)").isConstant()).to.be.false
expect(new Expression("n + sin x").isConstant()).to.be.false
})
})
describe("#requiredObjects", function() {
it("returns the list of objects that need to be registered for this expression", function() {
expect(new Expression("x^n").requiredObjects()).to.deep.equal([])
expect(new Expression("2+f(3)").requiredObjects()).to.deep.equal(["f"])
expect(new Expression("A.x+x").requiredObjects()).to.deep.equal(["A"])
expect(new Expression("2+f(sin A.x)+n").requiredObjects()).to.deep.equal(["f", "A"])
})
})
describe.skip("#allRequirementsFulfilled", function() {
// TODO: Make tests for objects
})
describe.skip("#undefinedVariables", function() {
// TODO: Make tests for objects
})
describe("#toEditableString", function() {
it("should return a readable expression", function() {
expect(new Expression("2+1").toEditableString()).to.equal("3")
expect(new Expression("2+x").toEditableString()).to.equal("(2 + x)")
expect(new Expression("x*2+x/3").toEditableString()).to.equal("((x * 2) + (x / 3))")
})
it("should be able to be reparsed and equal the same expression", function() {
const exprs = ["5", "x/2", "4/2", "sin x"]
for(const expr of exprs) {
const exprObj = new Expression(expr)
expect(new Expression(exprObj.toEditableString()).calc).to.deep.equal(exprObj.calc)
}
})
})
describe("#execute", function() {
it("returns the result of the computation of the expression", function() {
expect(new Expression("2+3").execute()).to.equal(5)
expect(new Expression("2+3").execute(10)).to.equal(5)
expect(new Expression("2+x").execute(10)).to.equal(12)
expect(new Expression("sin x").execute(Math.PI)).to.be.approximately(0, Number.EPSILON)
})
it("returns the cached value if the expression can be cached", function() {
const exprs = ["2+3", "x/2", "4/2", "sin x"]
for(const expr of exprs) {
const exprObj = new Expression(expr)
if(exprObj.canBeCached)
expect(exprObj.execute()).to.equal(exprObj.cachedValue)
else
expect(exprObj.execute()).to.not.equal(exprObj.cachedValue)
}
})
it("throws an error if some variables are undefined.", function() {
expect(() => new Expression("x+n").execute()).to.throw("Undefined variable n.")
expect(() => new Expression("sin A.t").execute()).to.throw("Undefined variable A.")
expect(() => new Expression("f(3)").execute()).to.throw("Undefined variable f.")
})
})
describe("#simplify", function() {
it("returns an expression with just the result when no constant or object are used", function() {
expect(new Expression("2+2").simplify(Math.PI/2)).to.deep.equal(new Expression("4"))
expect(new Expression("x+3").simplify(5)).to.deep.equal(new Expression("8"))
expect(new Expression("sin x").simplify(Math.PI/2)).to.deep.equal(new Expression("1"))
expect(new Expression("0*e^x").simplify(Math.PI/2)).to.deep.equal(new Expression("0"))
})
it("returns a simplified version of the expression if constants are used", function() {
const original = new Expression("e^x").simplify(2)
const to = new Expression("e^2")
expect(original.toEditableString()).to.deep.equal(to.toEditableString())
})
})
describe("#toString", function() {
it("returns a human readable string of the expression", function() {
expect(new Expression("-2-3").toString()).to.equal("-5")
expect(new Expression("0.2+0.1").toString()).to.equal("0.3")
expect(new Expression("sin x").toString()).to.equal("sin x")
expect(new Expression("sin π").toString()).to.equal("sin π")
})
it("should add a sign if the option is passed", function() {
expect(new Expression("-2-3").toString(true)).to.equal("-5")
expect(new Expression("2+3").toString(true)).to.equal("+5")
})
})
describe("#executeExpression", function() {
it("directly computes the result of the expression with no variable", function() {
expect(executeExpression("2+3")).to.equal(5)
expect(executeExpression("sin (π/2)")).to.equal(1)
expect(executeExpression("e^3")).to.be.approximately(Math.pow(Math.E, 3), Number.EPSILON)
})
it("throws an error if variables are employed", function() {
expect(() => executeExpression("x+n")).to.throw("Undefined variable n.")
})
})
})

View file

@ -1,59 +0,0 @@
/**
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 Ad5001
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export class MockCanvas {
constructor(mockLoading = false) {
this.mockLoading = mockLoading
}
getContext(context) {
throw new Error("MockCanvas.getContext not implemented")
}
markDirty(rect) {
this.requestPaint()
}
loadImageAsync(image) {
return new Promise((resolve, reject) => {
resolve()
})
}
/**
* Image loading is instantaneous.
* @param {string} image
* @return {boolean}
*/
isImageLoading(image) {
return this.mockLoading
}
/**
* Image loading is instantaneous.
* @param {string} image
* @return {boolean}
*/
isImageLoaded(image) {
return !this.mockLoading
}
requestPaint() {
}
}

View file

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

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