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
*.lgg
# Tests
common/coverage/
**/.coverage
# npm
common/node_modules
common/coverage/
common/.coverage
runtime-pyside6/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/index.mjs*

View file

@ -1,4 +1,4 @@
# ![icon](https://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)
[![Translation status](https://hosted.weblate.org/widgets/logarithmplotter/-/logarithmplotter/svg-badge.svg)](https://hosted.weblate.org/engage/logarithmplotter/)
@ -24,7 +24,7 @@ First, you'll need to install all the required dependencies:
- [npm](https://npmjs.com) (or [yarn](https://yarnpkg.com/)), go to the `common` directory, and run `npm install` (or `yarn install`).
You can simply run LogarithmPlotter using `python3 run.py`. It automatically compiles the language files (requires
`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
`python3 build/runtime-pyside6/LogarithmPlotter/logarithmplotter.py`.
@ -68,13 +68,7 @@ To run LogarithmPlotter's tests, follow these steps:
- Python
- Install python3 and [poetry](https://python-poetry.org/)
- Create and activate virtual env (recommended)
- Go into `runtime-pyside6` and run `poetry install --with test`
- ECMAScript
- Install node with npm
- Go into `common` and run `npm install -D`
Finally, to actually run the tests:
- Run `poetry install --with test`
- Run `scripts/run-tests.sh`
## Legal notice
@ -95,8 +89,8 @@ Finally, to actually run the tests:
You should have received a copy of the GNU General Public License
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
are copyrighted by their original authors:
Language files translations located at LogarithmPlotter/i18n are licensed under GNU GPL3.0+ and are copyrighted by their
original authors. See LICENSE.md for more details:
- 🇭🇺 Hungarian translation by [Óvári](https://github.com/ovari)
- 🇳🇴 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;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).

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
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
files=$(find ../../common/src -name '*.mjs')
files=$(find .. -name *.mjs)
for file in $files; do
echo "Moving '$file' to '${file%.*}.js'..."
mv "$file" "${file%.*}.js"
@ -33,14 +33,12 @@ for file in $files; do
replace "${file%.*}.js" "^export" "/*export*/"
replace "${file%.*}.js" "async " "/*async */"
replace "${file%.*}.js" "await" "/*await */"
replace "${file%.*}.js" " #" "// #"
replace "${file%.*}.js" "this.#" "/*this.#*/"
done
echo "----------------------------"
echo "| Updating translations... |"
echo "----------------------------"
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
for lp in *.ts; do
echo "Replacing locations in $lp..."
@ -57,9 +55,7 @@ for file in $files; do
replace "$file" "/*async */" "async "
replace "$file" "^/*export*/" "export"
replace "$file" "^/*export default*/" "export default"
replace "$file" '.mjs"*/' '.mjs"'
replace "$file" "^/*import" "import"
replace "$file" "^/*export" "export"
replace "$file" "// #" " #"
replace "$file" "/*this.#*/" "this.#"
replace "$file" '.mjs"*/$' '.mjs"'
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"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="48.0px"
height="48.0px"
viewBox="0 0 48.0 48.0"
width="24.0px"
height="24.0px"
viewBox="0 0 24.0 24.0"
version="1.1"
id="SVGRoot"
xmlns:xlink="http://www.w3.org/1999/xlink"
xml:space="preserve"
xmlns="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:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<title
id="title38896">LogarithmPlotter Icon</title>
<defs
id="defs2254">
<linearGradient
id="linearGradient27593">
<stop
style="stop-color:#000000;stop-opacity:0.15000001;"
offset="0"
id="stop27589" />
<stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop27591" />
</linearGradient>
<linearGradient
id="linearGradient13467">
<stop
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"
xmlns:dc="http://purl.org/dc/elements/1.1/"><title
id="title836">LogarithmPlotter Icon v1.0</title><defs
id="defs833" /><metadata
id="metadata836"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title>LogarithmPlotter Icon v1.0</dc:title><cc:license
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
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><g
id="layer2"
transform="matrix(1,0,0,0.94444444,0,1.1666667)"
style="fill:#666666"><rect
style="fill:#666666;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect1546"
width="18"
height="18.105883"
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"
height="18"
x="3"
y="3"
ry="2.3212669"
rx="2.2499998"
transform="matrix(2.2222222,0,0,2.1539961,-2.6666667,-2.4619883)" />
</g>
<g
id="layer3"
style="fill:#0000ff">
<path
id="path27475"
style="fill:url(#linearGradient27595);fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
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" />
</g>
<g
id="layer1-6"
style="stroke-width:2;stroke-dasharray:none"
transform="matrix(2,0,0,2,0,1)">
<rect
ry="2.25" /></g><g
id="layer2-6"
transform="matrix(1,0,0,0.94444444,0,0.16666668)"
style="fill:#f9f9f9"><rect
style="fill:#f9f9f9;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"
x="3"
y="3"
ry="2.25" /></g><g
id="layer1"
style="stroke-width:2;stroke-dasharray:none"><rect
style="fill:#000000;fill-rule:evenodd;stroke-width:1.86898;stroke-dasharray:none;stroke-opacity:0"
id="rect1410"
width="14"
height="2"
x="5"
y="15.5" />
<rect
style="fill:#000000;fill-rule:evenodd;stroke-width:2.06559;stroke-dasharray:none;stroke-opacity:0"
y="15.5" /><rect
style="fill:#000000;fill-rule:evenodd;stroke-width:2;stroke-dasharray:none;stroke-opacity:0"
id="rect1412"
width="2"
height="16"
x="8"
y="3.5" />
<path
height="15"
x="9"
y="3.9768662" /><path
style="fill:none;fill-rule:evenodd;stroke:#ff0000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path1529"
d="m 18,3.5 c 0,7 -4,12 -13,12" />
</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>
d="M 18,4 C 18,10.017307 13.40948,15.5 5,15.5" /></g></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
Architecture: all
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
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>
<category>Science</category>
<category>Education</category>
<category>Qt</category>
</categories>
<url type="homepage">https://apps.ad5001.eu/logarithmplotter/</url>
<url type="bugtracker">https://git.ad5001.eu/Ad5001/LogarithmPlotter/issues/</url>
<url type="help">https://git.ad5001.eu/Ad5001/LogarithmPlotter/wiki/</url>
<url type="translate">https://hosted.weblate.org/engage/logarithmplotter/</url>
<screenshots>
<screenshot type="default">
<image>https://apps.ad5001.eu/img/en/logarithmplotter/gain.png?v=0.6</image>
<image xml:lang="de">https://apps.ad5001.eu/img/de/logarithmplotter/gain.png?v=0.6</image>
<image xml:lang="fr">https://apps.ad5001.eu/img/fr/logarithmplotter/gain.png?v=0.6</image>
<image xml:lang="hu">https://apps.ad5001.eu/img/hu/logarithmplotter/gain.png?v=0.6</image>
<image xml:lang="no">https://apps.ad5001.eu/img/no/logarithmplotter/gain.png?v=0.6</image>
<image xml:lang="es">https://apps.ad5001.eu/img/es/logarithmplotter/gain.png?v=0.6</image>
<image>https://apps.ad5001.eu/img/en/logarithmplotter/gain.png?v=0.5</image>
<image xml:lang="de">https://apps.ad5001.eu/img/de/logarithmplotter/gain.png?v=0.5</image>
<image xml:lang="fr">https://apps.ad5001.eu/img/fr/logarithmplotter/gain.png?v=0.5</image>
<image xml:lang="hu">https://apps.ad5001.eu/img/hu/logarithmplotter/gain.png?v=0.5</image>
<image xml:lang="no">https://apps.ad5001.eu/img/no/logarithmplotter/gain.png?v=0.5</image>
<caption>Main view of LogarithmPlotter showing an asymptotic Bode magnitude plot.</caption>
<caption xml:lang="de">Die Hauptansicht des LogarithmPlotters zeigt eine asymptotische Bode-Magnitude-Darstellung.</caption>
<caption xml:lang="fr">Vue principale de LogarithmPlotter montrant un tracé asymptotique d'une magnitude de Bode.</caption>
<caption xml:lang="hu">A LogarithmPlotter fő nézete, amely egy aszimptotikus Bode-magnitúdó ábrát mutat.</caption>
<caption xml:lang="no">Hovedvisning av LogarithmPlotter som viser et asymptotisk Bode-størrelsesplott.</caption>
<caption xml:lang="es">Vista principal de LogarithmPlotter mostrando un gráfico asintótico de una magnitud de Bode.</caption>
</screenshot>
<screenshot>
<image>https://apps.ad5001.eu/img/en/logarithmplotter/phase.png?v=0.6</image>
<image xml:lang="de">https://apps.ad5001.eu/img/de/logarithmplotter/phase.png?v=0.6</image>
<image xml:lang="fr">https://apps.ad5001.eu/img/fr/logarithmplotter/phase.png?v=0.6</image>
<image xml:lang="hu">https://apps.ad5001.eu/img/hu/logarithmplotter/phase.png?v=0.6</image>
<image xml:lang="no">https://apps.ad5001.eu/img/no/logarithmplotter/phase.png?v=0.6</image>
<image xml:lang="es">https://apps.ad5001.eu/img/es/logarithmplotter/phase.png?v=0.6</image>
<image>https://apps.ad5001.eu/img/en/logarithmplotter/phase.png?v=0.5</image>
<image xml:lang="de">https://apps.ad5001.eu/img/de/logarithmplotter/phase.png?v=0.5</image>
<image xml:lang="fr">https://apps.ad5001.eu/img/fr/logarithmplotter/phase.png?v=0.5</image>
<image xml:lang="hu">https://apps.ad5001.eu/img/hu/logarithmplotter/phase.png?v=0.5</image>
<image xml:lang="no">https://apps.ad5001.eu/img/no/logarithmplotter/phase.png?v=0.5</image>
<caption>Main view of LogarithmPlotter showing an asymptotic Bode phase plot.</caption>
<caption xml:lang="de">Hauptansicht des LogarithmPlotters mit einer asymptotischen Bode-Phasendarstellung.</caption>
<caption xml:lang="fr">Vue principale de LogarithmPlotter montrant un tracé asymptotique d'une phase de Bode.</caption>
<caption xml:lang="hu">A LogarithmPlotter fő nézete, amely egy aszimptotikus Bode-fázis ábrát mutat.</caption>
<caption xml:lang="no">Hovedvisning av LogarithmPlotter som viser et asymptotisk Bode-fasediagram.</caption>
<caption xml:lang="es">Vista principal de LogarithmPlotter mostrando un gráfico asintótico de una fase de Bode.</caption>
</screenshot>
<screenshot>
<image>https://apps.ad5001.eu/img/en/logarithmplotter/welcome.png?v=0.6</image>
<image xml:lang="de">https://apps.ad5001.eu/img/de/logarithmplotter/welcome.png?v=0.6</image>
<image xml:lang="fr">https://apps.ad5001.eu/img/fr/logarithmplotter/welcome.png?v=0.6</image>
<image xml:lang="hu">https://apps.ad5001.eu/img/hu/logarithmplotter/welcome.png?v=0.6</image>
<image xml:lang="no">https://apps.ad5001.eu/img/no/logarithmplotter/welcome.png?v=0.6</image>
<image xml:lang="es">https://apps.ad5001.eu/img/es/logarithmplotter/welcome.png?v=0.6</image>
<image>https://apps.ad5001.eu/img/en/logarithmplotter/welcome.png?v=0.5</image>
<image xml:lang="de">https://apps.ad5001.eu/img/de/logarithmplotter/welcome.png?v=0.5</image>
<image xml:lang="fr">https://apps.ad5001.eu/img/fr/logarithmplotter/welcome.png?v=0.5</image>
<image xml:lang="hu">https://apps.ad5001.eu/img/hu/logarithmplotter/welcome.png?v=0.5</image>
<image xml:lang="no">https://apps.ad5001.eu/img/no/logarithmplotter/welcome.png?v=0.5</image>
<caption>LogarithmPlotter's welcome page.</caption>
<caption xml:lang="de">LogarithmPlotter's Willkommensseite.</caption>
<caption xml:lang="fr">Page d'accueil de LogarithmPlotter.</caption>
<caption xml:lang="hu">LogarithmPlotter üdvözlő oldala.</caption>
<caption xml:lang="no">LogarithmPlotters velkomstside.</caption>
<caption xml:lang="es">Página de bienvenida de LogarithmPlotter.</caption>
</screenshot>
</screenshots>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type xmlns="http://www.freedesktop.org/standards/shared-mime-info" type="application/x-logarithm-plot">
<comment>Logarithmic Plot File</comment>
<comment>Logarithm Plot File</comment>
<comment xml:lang="fr">Fichier Graphe Logarithmique</comment>
<icon name="application-x-logarithm-plot"/>
<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
- name: Build
image: ad5001/ubuntu-pyside-xvfb:linux-6-latest-latex-node
image: node:18-bookworm
commands:
- cd common && npm install && cd ..
- apt update
- apt install -y qtchooser qttools5-dev-tools
# Start building
- bash scripts/build.sh
when:
event: [ push, tag ]
- name: Unit Tests
image: ad5001/ubuntu-pyside-xvfb:linux-6-latest-latex-node
image: ad5001/ubuntu-pyside-xvfb:linux-6-latest-latex
commands:
- apt update
- apt install -y npm
- cd common && npm install -D && cd ..
- xvfb-run bash scripts/run-tests.sh --no-rebuild
when:
event: [ push, tag ]
- name: File Tests
image: ad5001/ubuntu-pyside-xvfb:linux-6-latest-latex-node
image: ad5001/ubuntu-pyside-xvfb:linux-6-latest-latex
commands:
- xvfb-run python3 run.py --test-build --no-check-for-updates
- xvfb-run python3 run.py --test-build --no-check-for-updates ./ci/test1.lpf
- 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:
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",
"version": "0.6.0",
"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": {
"build": "rollup --config rollup.config.mjs",
"test": "c8 mocha test/**/*.mjs"
@ -24,12 +24,9 @@
},
"devDependencies": {
"@types/chai": "^5.0.0",
"@types/chai-spies": "^1.0.6",
"@types/chai-as-promised": "^8.0.1",
"@types/mocha": "^10.0.8",
"chai": "^5.1.1",
"chai-as-promised": "^8.0.0",
"chai-spies": "^1.1.0",
"esm": "^3.2.25",
"mocha": "^10.7.3"
}

View file

@ -22,7 +22,7 @@ import { babel } from "@rollup/plugin-babel"
import cleanup from "rollup-plugin-cleanup"
const src = "./src/index.mjs"
const dest = "../build/runtime-pyside6/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Common/index.mjs"
const dest = "../build/runtime-pyside6/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/index.mjs"
export default {
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)
throw new Error("Cannot render an item as LaTeX when LaTeX is disabled.")
const imgDepth = History.imageDepth
const renderArguments = [
const { source, width, height } = await Latex.requestAsyncRender(
latexString,
imgDepth * (History.fontSize + 2),
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"/>`
}

View file

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

View file

@ -18,11 +18,10 @@
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"
export * as Modules from "./module/index.mjs"
export * as MathLib from "./math/index.mjs"
export * as HistoryLib from "./history/index.mjs"
export * as Parsing from "./parsing/index.mjs"
export * as Utils from "./utils.mjs"

View file

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

View file

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

View file

@ -210,8 +210,9 @@ export function gamma(n) {
}
export function stringOrArrayLength(s) {
if(Array.isArray(s))
if(Array.isArray(s)) {
return 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))
}
export function setVar(name, value, variables) {
if(variables) variables[name] = value
return value
}
export function arrayIndex(array, index) {
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) {
return ((x > 0) - (x < 0)) || +x
}

View file

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

View file

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

View file

@ -17,38 +17,28 @@
*/
import * as Utils from "../utils/index.mjs"
import { ExprEvalExpression } from "../lib/expr-eval/expression.mjs"
import * as Utils from "../utils.mjs"
import Latex from "../module/latex.mjs"
import ExprParser from "../module/expreval.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.
*/
export class Expression {
/**
*
* @param {string|ExprEvalExpression} expr
*/
constructor(expr) {
if(typeof expr === "string") {
this.expr = Utils.exponentsToExpression(expr)
this.calc = ExprParser.parse(this.expr).simplify()
} else if(expr instanceof ExprEvalExpression) {
} else {
// Passed an expression here directly.
this.calc = expr.simplify()
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
if(this.canBeCached && this.allRequirementsFulfilled())
this.recache()
if(this.cached && this.allRequirementsFulfilled())
this.cachedValue = this.calc.evaluate(Objects.currentObjectsByName)
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.
* @return {string[]}
* @return {boolean}
*/
undefinedVariables() {
return this.requiredObjects().filter(objName => !(objName in Objects.currentObjectsByName))
}
recache() {
this.cachedValue = this.calc.evaluate(Objects.currentObjectsByName)
if(this.cached)
this.cachedValue = this.calc.evaluate(Objects.currentObjectsByName)
}
execute(x = 1) {
if(this.canBeCached) {
if(this.cached) {
if(this.cachedValue == null)
this.recache()
this.cachedValue = this.calc.evaluate(Objects.currentObjectsByName)
return this.cachedValue
}
ExprParser.currentVars = Object.assign({ "x": x }, Objects.currentObjectsByName)
@ -108,10 +99,9 @@ export class Expression {
}
simplify(x) {
let expr = new Expression(this.calc.substitute("x", x).simplify())
if(expr.allRequirementsFulfilled() && expr.execute() === 0)
expr = new Expression("0")
return expr
let expr = this.calc.substitute("x", x).simplify()
if(expr.evaluate() === 0) expr = "0"
return new Expression(expr)
}
toEditableString() {
@ -120,28 +110,17 @@ export class Expression {
toString(forceSign = false) {
let str = Utils.makeExpressionReadable(this.calc.toString())
if(str !== undefined && str.match(NUMBER_MATCHER)) {
const decimals = str.split(".")[1].split("e")[0]
const zeros = decimals.split("0").length
const nines = decimals.split("9").length
if(zeros > 7 || nines > 7) {
if(str !== undefined && str.match(/^\d*\.\d+$/)) {
if(str.split(".")[1].split("0").length > 7) {
// Likely rounding error
str = parseFloat(str).toDecimalPrecision(8).toString()
str = parseFloat(str.substring(0, str.length - 1)).toString()
}
}
if(str[0] === "(" && str.at(-1) === ")")
str = str.substring(1, str.length - 1)
if(str[0] !== "-" && forceSign)
str = "+" + str
if(str[0] !== "-" && forceSign) str = "+" + str
return str
}
}
/**
* Parses and executes the given expression
* @param {string} expr
* @return {number}
*/
export function executeExpression(expr) {
return (new Expression(expr.toString())).execute()
}

View file

@ -17,7 +17,7 @@
*/
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 Objects from "../module/objects.mjs"
import ExprParser from "../module/expreval.mjs"

View file

@ -18,33 +18,30 @@
import { Module } from "./common.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 Latex from "./latex.mjs"
import Objects from "./objects.mjs"
import History from "./history.mjs"
import Settings from "./settings.mjs"
class CanvasAPI extends Module {
/** @type {CanvasInterface} */
#canvas = null
/** @type {CanvasRenderingContext2D} */
#ctx = null
/** Lock to prevent asynchronous stuff from printing stuff that is outdated. */
#redrawCount = 0
/** @type {{show(string, string, string)}} */
#drawingErrorDialog = null
constructor() {
super("Canvas", {
canvas: CanvasInterface,
drawingErrorDialog: DialogInterface
})
/** @type {CanvasInterface} */
this._canvas = null
/** @type {CanvasRenderingContext2D} */
this._ctx = null
/**
* @type {{show(string, string, string)}}
* @private
*/
this._drawingErrorDialog = null
/**
*
* @type {Object.<string, {expression: Expression, value: number, maxDraw: number}>}
@ -70,18 +67,18 @@ class CanvasAPI extends Module {
*/
initialize({ canvas, drawingErrorDialog }) {
super.initialize({ canvas, drawingErrorDialog })
this.#canvas = canvas
this.#drawingErrorDialog = drawingErrorDialog
this._canvas = canvas
this._drawingErrorDialog = drawingErrorDialog
}
get width() {
if(!this.initialized) throw new Error("Attempting width before initialize!")
return this.#canvas.width
return this._canvas.width
}
get height() {
if(!this.initialized) throw new Error("Attempting height before initialize!")
return this.#canvas.height
return this._canvas.height
}
/**
@ -90,7 +87,7 @@ class CanvasAPI extends Module {
*/
get xmin() {
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() {
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() {
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() {
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() {
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() {
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() {
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() {
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() {
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() {
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() {
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() {
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() {
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._reset()
this._drawGrid()
this._drawAxes()
this._drawLabels()
this.#ctx.lineWidth = this.linewidth
this._ctx.lineWidth = this.linewidth
for(let objType in Objects.currentObjects) {
for(let obj of Objects.currentObjects[objType]) {
this.#ctx.strokeStyle = obj.color
this.#ctx.fillStyle = obj.color
this._ctx.strokeStyle = obj.color
this._ctx.fillStyle = obj.color
if(obj.visible)
try {
obj.draw(this)
@ -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)
console.error(e)
console.log(e.stack)
this.#drawingErrorDialog.show(objType, obj.name, e.message)
this._drawingErrorDialog.show(objType, obj.name, e.message)
History.undo()
}
}
}
this.#ctx.lineWidth = 1
this._ctx.lineWidth = 1
}
/**
@ -244,9 +240,9 @@ class CanvasAPI extends Module {
* @private
*/
_computeAxes() {
let exprY = new Expression(`x*(${Settings.yaxisstep})`)
let exprY = new Expression(`x*(${this._canvas.yaxisstep})`)
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)
this.axesSteps = {
x: {
@ -268,10 +264,10 @@ class CanvasAPI extends Module {
*/
_reset() {
// Reset
this.#ctx.fillStyle = "#FFFFFF"
this.#ctx.strokeStyle = "#000000"
this.#ctx.font = `${this.textsize}px sans-serif`
this.#ctx.fillRect(0, 0, this.width, this.height)
this._ctx.fillStyle = "#FFFFFF"
this._ctx.strokeStyle = "#000000"
this._ctx.font = `${this.textsize}px sans-serif`
this._ctx.fillRect(0, 0, this.width, this.height)
}
/**
@ -279,7 +275,7 @@ class CanvasAPI extends Module {
* @private
*/
_drawGrid() {
this.#ctx.strokeStyle = "#C0C0C0"
this._ctx.strokeStyle = "#C0C0C0"
if(this.logscalex) {
for(let xpow = -this.maxgradx; xpow <= this.maxgradx; xpow++) {
for(let xmulti = 1; xmulti < 10; xmulti++) {
@ -303,7 +299,7 @@ class CanvasAPI extends Module {
* @private
*/
_drawAxes() {
this.#ctx.strokeStyle = "#000000"
this._ctx.strokeStyle = "#000000"
let axisypos = this.logscalex ? 1 : 0
this.drawXLine(axisypos)
this.drawYLine(0)
@ -324,19 +320,19 @@ class CanvasAPI extends Module {
let axisypx = this.x2px(this.logscalex ? 1 : 0) // X coordinate of Y axis
let axisxpx = this.y2px(0) // Y coordinate of X axis
// Labels
this.#ctx.fillStyle = "#000000"
this.#ctx.font = `${this.textsize}px sans-serif`
this.#ctx.fillText(this.ylabel, axisypx + 10, 24)
let textWidth = this.#ctx.measureText(this.xlabel).width
this.#ctx.fillText(this.xlabel, this.width - 14 - textWidth, axisxpx - 5)
this._ctx.fillStyle = "#000000"
this._ctx.font = `${this.textsize}px sans-serif`
this._ctx.fillText(this.ylabel, axisypx + 10, 24)
let textWidth = this._ctx.measureText(this.xlabel).width
this._ctx.fillText(this.xlabel, this.width - 14 - textWidth, axisxpx - 5)
// Axis graduation labels
this.#ctx.font = `${this.textsize - 4}px sans-serif`
this._ctx.font = `${this.textsize - 4}px sans-serif`
let txtMinus = this.#ctx.measureText("-").width
let txtMinus = this._ctx.measureText("-").width
if(this.showxgrad) {
if(this.logscalex) {
for(let xpow = -this.maxgradx; xpow <= this.maxgradx; xpow += 1) {
textWidth = this.#ctx.measureText("10" + textsup(xpow)).width
textWidth = this._ctx.measureText("10" + textsup(xpow)).width
if(xpow !== 0)
this.drawVisibleText("10" + textsup(xpow), this.x2px(Math.pow(10, xpow)) - textWidth / 2, axisxpx + 16 + (6 * (xpow === 1)))
}
@ -354,13 +350,13 @@ class CanvasAPI extends Module {
for(let y = 0; y < this.axesSteps.y.maxDraw; y += 1) {
let drawY = y * this.axesSteps.y.value
let txtY = this.axesSteps.y.expression.simplify(y).toString().replace(/^\((.+)\)$/, "$1")
textWidth = this.#ctx.measureText(txtY).width
textWidth = this._ctx.measureText(txtY).width
this.drawVisibleText(txtY, axisypx - 6 - textWidth, this.y2px(drawY) + 4 + (10 * (y === 0)))
if(y !== 0)
this.drawVisibleText("-" + txtY, axisypx - 6 - textWidth - txtMinus, this.y2px(-drawY) + 4)
}
}
this.#ctx.fillStyle = "#FFFFFF"
this._ctx.fillStyle = "#FFFFFF"
}
//
@ -398,7 +394,7 @@ class CanvasAPI extends Module {
drawVisibleText(text, x, y) {
if(x > 0 && x < this.width && y > 0 && y < this.height) {
text.toString().split("\n").forEach((txt, i) => {
this.#ctx.fillText(txt, x, y + (this.textsize * i))
this._ctx.fillText(txt, x, y + (this.textsize * i))
})
}
}
@ -413,8 +409,8 @@ class CanvasAPI extends Module {
* @param {number} height
*/
drawVisibleImage(image, x, y, width, height) {
this.#canvas.markDirty(Qt.rect(x, y, width, height))
this.#ctx.drawImage(image, x, y, width, height)
this._canvas.markDirty(Qt.rect(x, y, width, height))
this._ctx.drawImage(image, x, y, width, height)
}
/**
@ -428,7 +424,7 @@ class CanvasAPI extends Module {
let defaultHeight = this.textsize * 1.2 // Approximate but good enough!
for(let txt of text.split("\n")) {
theight += defaultHeight
if(this.#ctx.measureText(txt).width > twidth) twidth = this.#ctx.measureText(txt).width
if(this._ctx.measureText(txt).width > twidth) twidth = this._ctx.measureText(txt).width
}
return { "width": twidth, "height": theight }
}
@ -498,10 +494,10 @@ class CanvasAPI extends Module {
* @param {number} y2
*/
drawLine(x1, y1, x2, y2) {
this.#ctx.beginPath()
this.#ctx.moveTo(x1, y1)
this.#ctx.lineTo(x2, y2)
this.#ctx.stroke()
this._ctx.beginPath()
this._ctx.moveTo(x1, y1)
this._ctx.lineTo(x2, y2)
this._ctx.stroke()
}
/**
@ -513,9 +509,9 @@ class CanvasAPI extends Module {
* @param {number} dashPxSize
*/
drawDashedLine(x1, y1, x2, y2, dashPxSize = 6) {
this.#ctx.setLineDash([dashPxSize / 2, dashPxSize])
this._ctx.setLineDash([dashPxSize / 2, dashPxSize])
this.drawLine(x1, y1, x2, y2)
this.#ctx.setLineDash([])
this._ctx.setLineDash([])
}
/**
@ -525,22 +521,14 @@ class CanvasAPI extends Module {
* @param {function(LatexRenderResult|{width: number, height: number, source: string})} callback
*/
renderLatexImage(ltxText, color, callback) {
const currentRedrawCount = this.#redrawCount
const onRendered = (imgData) => {
if(!this.#canvas.isImageLoaded(imgData.source) && !this.#canvas.isImageLoading(imgData.source)) {
if(!this._canvas.isImageLoaded(imgData.source) && !this._canvas.isImageLoading(imgData.source)) {
// Wait until the image is loaded to callback.
this.#canvas.loadImageAsync(imgData.source).then(() => {
if(this.#redrawCount === currentRedrawCount)
callback(imgData)
else
console.log("1. Discard render of", imgData.source, this.#redrawCount, currentRedrawCount)
})
this._canvas.loadImage(imgData.source)
this._canvas.imageLoaders[imgData.source] = [callback, imgData]
} else {
// Callback directly
if(this.#redrawCount === currentRedrawCount)
callback(imgData)
else
console.log("2. Discard render of", imgData.source, this.#redrawCount, currentRedrawCount)
callback(imgData)
}
}
const prerendered = Latex.findPrerendered(ltxText, this.textsize, color)
@ -555,11 +543,11 @@ class CanvasAPI extends Module {
//
get font() {
return this.#ctx.font
return this._ctx.font
}
set font(value) {
return this.#ctx.font = value
return this._ctx.font = value
}
/**
@ -572,9 +560,9 @@ class CanvasAPI extends Module {
* @param {boolean} counterclockwise
*/
arc(x, y, radius, startAngle, endAngle, counterclockwise = false) {
this.#ctx.beginPath()
this.#ctx.arc(x, y, radius, startAngle, endAngle, counterclockwise)
this.#ctx.stroke()
this._ctx.beginPath()
this._ctx.arc(x, y, radius, startAngle, endAngle, counterclockwise)
this._ctx.stroke()
}
/**
@ -584,9 +572,9 @@ class CanvasAPI extends Module {
* @param {number} radius
*/
disc(x, y, radius) {
this.#ctx.beginPath()
this.#ctx.arc(x, y, radius, 0, 2 * Math.PI)
this.#ctx.fill()
this._ctx.beginPath()
this._ctx.arc(x, y, radius, 0, 2 * Math.PI)
this._ctx.fill()
}
/**
@ -597,7 +585,7 @@ class CanvasAPI extends Module {
* @param {number} h
*/
fillRect(x, y, w, h) {
this.#ctx.fillRect(x, y, w, h)
this._ctx.fillRect(x, y, w, h)
}
}

View file

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

View file

@ -19,7 +19,7 @@
import { Module } from "./common.mjs"
import { Parser } from "../lib/expr-eval/parser.mjs"
const EVAL_VARIABLES = {
const evalVariables = {
// Variables not provided by expr-eval.js, needs to be provided manually
"pi": Math.PI,
"PI": Math.PI,
@ -35,17 +35,15 @@ const EVAL_VARIABLES = {
}
class ExprParserAPI extends Module {
#parser = new Parser()
constructor() {
super("ExprParser")
this.currentVars = {}
this.#parser = new Parser()
this._parser = new Parser()
this.#parser.consts = Object.assign({}, this.#parser.consts, EVAL_VARIABLES)
this._parser.consts = Object.assign({}, this._parser.consts, evalVariables)
this.#parser.functions.integral = this.integral.bind(this)
this.#parser.functions.derivative = this.derivative.bind(this)
this._parser.functions.integral = this.integral.bind(this)
this._parser.functions.derivative = this.derivative.bind(this)
}
/**
@ -70,7 +68,7 @@ class ExprParserAPI extends Module {
[f, variable] = args
if(typeof f !== "string" || typeof variable !== "string")
throw EvalError(qsTranslate("usage", "Usage:\n%1").arg(usage2))
f = this.#parser.parse(f).toJSFunction(variable, this.currentVars)
f = this._parser.parse(f).toJSFunction(variable, this.currentVars)
} else
throw EvalError(qsTranslate("usage", "Usage:\n%1\n%2").arg(usage1).arg(usage2))
return f
@ -81,14 +79,14 @@ class ExprParserAPI extends Module {
* @returns {ExprEvalExpression}
*/
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 usage2 = qsTranslate("usage", "integral(<from: number>, <to: number>, <f: string>, <variable: string>)")
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))
// 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 x = args.pop()
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))
let derivative_precision = 1e-8
let derivative_precision = x / 10
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 { HelperInterface, NUMBER, STRING } from "./interface.mjs"
import { BaseEvent } from "../events.mjs"
import { Action, Actions } from "../history/index.mjs"
import { HistoryInterface, NUMBER, STRING } from "./interface.mjs"
class ClearedEvent extends BaseEvent {
constructor() {
super("cleared")
}
}
class LoadedEvent extends BaseEvent {
constructor() {
super("loaded")
}
}
class AddedEvent extends BaseEvent {
constructor(action) {
super("added")
this.action = action
}
}
class UndoneEvent extends BaseEvent {
constructor(action) {
super("undone")
this.undid = action
}
}
class RedoneEvent extends BaseEvent {
constructor(action) {
super("redone")
this.redid = action
}
}
class HistoryAPI extends Module {
static emits = ["cleared", "loaded", "added", "undone", "redone"]
#helper
constructor() {
super("History", {
helper: HelperInterface,
historyObj: HistoryInterface,
themeTextColor: STRING,
imageDepth: NUMBER,
fontSize: NUMBER
})
// History QML object
/** @type {Action[]} */
this.undoStack = []
/** @type {Action[]} */
this.redoStack = []
this.history = null
this.themeTextColor = "#FF0000"
this.imageDepth = 2
this.fontSize = 28
}
/**
* @param {HelperInterface} historyObj
* @param {string} themeTextColor
* @param {number} imageDepth
* @param {number} fontSize
*/
initialize({ helper, themeTextColor, imageDepth, fontSize }) {
super.initialize({ helper, themeTextColor, imageDepth, fontSize })
this.#helper = helper
initialize({ historyObj, themeTextColor, imageDepth, fontSize }) {
super.initialize({ historyObj, themeTextColor, imageDepth, fontSize })
this.history = historyObj
this.themeTextColor = themeTextColor
this.imageDepth = imageDepth
this.fontSize = fontSize
}
/**
* Undoes the Action at the top of the undo stack and pushes it to the top of the redo stack.
*/
undo() {
if(!this.initialized) throw new Error("Attempting undo before initialize!")
if(this.undoStack.length > 0) {
const action = this.undoStack.pop()
action.undo()
this.redoStack.push(action)
this.emit(new UndoneEvent(action))
}
this.history.undo()
}
/**
* Redoes the Action at the top of the redo stack and pushes it to the top of the undo stack.
*/
redo() {
if(!this.initialized) throw new Error("Attempting redo before initialize!")
if(this.redoStack.length > 0) {
const action = this.redoStack.pop()
action.redo()
this.undoStack.push(action)
this.emit(new RedoneEvent(action))
}
this.history.redo()
}
/**
* Clears both undo and redo stacks completely.
*/
clear() {
if(!this.initialized) throw new Error("Attempting clear before initialize!")
this.undoStack = []
this.redoStack = []
this.emit(new ClearedEvent())
this.history.clear()
}
/**
* Adds an instance of HistoryLib.Action to history.
* @param action
*/
addToHistory(action) {
if(!this.initialized) throw new Error("Attempting addToHistory before initialize!")
if(action instanceof Action) {
console.log("Added new entry to history: " + action.getReadableString())
this.undoStack.push(action)
if(this.#helper.getSetting("reset_redo_stack"))
this.redoStack = []
this.emit(new AddedEvent(action))
}
this.history.addToHistory(action)
}
/**
* Unserializes both the undo stack and redo stack from serialized content.
* @param {[string, any[]][]} undoSt
* @param {[string, any[]][]} redoSt
*/
unserialize(undoSt, redoSt) {
unserialize(...data) {
if(!this.initialized) throw new Error("Attempting unserialize before initialize!")
this.clear()
for(const [name, args] of undoSt)
this.undoStack.push(
new Actions[name](...args)
)
for(const [name, args] of redoSt)
this.redoStack.push(
new Actions[name](...args)
)
this.emit(new LoadedEvent())
this.history.unserialize(...data)
}
/**
* Serializes history into JSON-able content.
* @return {[[string, any[]], [string, any[]]]}
*/
serialize() {
if(!this.initialized) throw new Error("Attempting serialize before initialize!")
let undoSt = [], redoSt = [];
for(const action of this.undoStack)
undoSt.push([ action.type(), action.export() ])
for(const action of this.redoStack)
redoSt.push([ action.type(), action.export() ])
return [undoSt, redoSt]
return this.history.serialize()
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
om_0 = Objects.createNewRegisteredObject("Point", [Objects.getNewName("ω"), this.color, "name"])
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"
}
om_0.requiredBy.push(this)

View file

@ -16,9 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { getRandomColor } from "../utils.mjs"
import Objects from "../module/objects.mjs"
import Latex from "../module/latex.mjs"
import { getRandomColor } from "../utils/index.mjs"
import { ensureTypeSafety, serializesByPropertyType } from "../parameters.mjs"
// 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)
}
}
if(this[property].canBeCached && this[property].requiredObjects().length > 0)
if(this[property].cached && this[property].requiredObjects().length > 0)
// Recalculate
this[property].recache()

View file

@ -16,7 +16,7 @@
* 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 { ExecutableObject } from "./common.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) {
let pxprecision = 10
const startDrawFrom = canvas.x2px(1)%pxprecision-pxprecision
let previousX = canvas.px2x(startDrawFrom)
// console.log("Starting draw from", previousX, startDrawFrom, canvas.x2px(1))
let previousX = canvas.px2x(0)
let previousY = null
if(definitionDomain instanceof SpecialDomain && definitionDomain.moveSupported) {
// Point based functions.
@ -162,7 +160,7 @@ export default class Function extends ExecutableObject {
// Calculate the previousY at the start of the canvas
if(definitionDomain.includes(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)
if(!definitionDomain.includes(previousX) && definitionDomain.includes(currentX)) {
// Should draw up to currentX, but NOT at previousX.
@ -171,7 +169,7 @@ export default class Function extends ExecutableObject {
do {
tmpPx++
previousX = canvas.px2x(tmpPx)
} while(!definitionDomain.includes(previousX) && currentX > previousX)
} while(!definitionDomain.includes(previousX))
// Recaclulate previousY
previousY = expr.execute(previousX)
} else if(!definitionDomain.includes(currentX)) {
@ -181,7 +179,7 @@ export default class Function extends ExecutableObject {
do {
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...)
let maxvariation = (canvas.px2y(0) - canvas.px2y(canvas.height))

View file

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

View file

@ -28,7 +28,7 @@ const XZOOM = new NumberSetting(
const YZOOM = new NumberSetting(
qsTranslate("Settings", "Y Zoom"),
"default_graph.yzoom",
"default_graph.xzoom",
"yzoom",
0.1
)
@ -37,7 +37,7 @@ const XMIN = new NumberSetting(
qsTranslate("Settings", "Min X"),
"default_graph.xmin",
"xmin",
() => Helper.getSetting("default_graph.logscalex") ? 1e-100 : -Infinity
() => Helper.getSettingBool("default_graph.logscalex") ? 1e-100 : -Infinity
)
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 [
CHECK_FOR_UPDATES,
RESET_REDO_STACK,
new EnableLatex(),
ENABLE_LATEX_ASYNC
new EnableLatex()
]

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

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

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
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as fs from "./mock/fs.mjs"
import Qt from "./mock/qt.mjs"
import { MockHelper } from "./mock/helper.mjs"
import { MockLatex } from "./mock/latex.mjs"
import { use } from "chai"
import spies from "chai-spies"
import promised from "chai-as-promised"
import * as fs from "./mock/fs.mjs";
import Qt from "./mock/qt.mjs";
import { MockHelper } from "./mock/helper.mjs";
import { MockLatex } from "./mock/latex.mjs";
import Modules from "../src/module/index.mjs";
function setup() {
use(promised)
const { spy } = use(spies)
globalThis.Helper = new MockHelper()
globalThis.Latex = new MockLatex()
globalThis.chaiPlugins = { spy }
Modules.Latex.initialize({ latex: Latex, helper: Helper })
}
setup()

View file

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

View file

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