DNA
This commit is contained in:
@@ -357,47 +357,42 @@ CMake je močno orodje, ki olajša in avtomatizira proces gradnje programske opr
|
|||||||
== SQLITE3
|
== SQLITE3
|
||||||
*SQLite* je lahka, samostoječa in vdelana relacijska baza podatkov, ki deluje neposredno v programu, brez potrebe po ločenem strežniku. Razvil jo je D. Richard Hipp leta 2000 z namenom, da ponudi preprosto, hitro in zanesljivo rešitev za upravljanje podatkov, ki ne zahteva nastavljanja ali upravljanja strežnika. SQLite je napisana v jeziku C in je na voljo kot knjižnica, kar pomeni, da se lahko integrira neposredno v aplikacije @web_sqlite @wiki_sqlite.
|
*SQLite* je lahka, samostoječa in vdelana relacijska baza podatkov, ki deluje neposredno v programu, brez potrebe po ločenem strežniku. Razvil jo je D. Richard Hipp leta 2000 z namenom, da ponudi preprosto, hitro in zanesljivo rešitev za upravljanje podatkov, ki ne zahteva nastavljanja ali upravljanja strežnika. SQLite je napisana v jeziku C in je na voljo kot knjižnica, kar pomeni, da se lahko integrira neposredno v aplikacije @web_sqlite @wiki_sqlite.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
=== Glavne značilnosti SQLite:
|
=== Glavne značilnosti SQLite:
|
||||||
|
|
||||||
1. *Samostoječa in vdelana* SQLite deluje kot del programa in ne zahteva ločenega procesa ali strežnika. To pomeni, da aplikacija komunicira neposredno s podatkovno bazo, kar zmanjša kompleksnost in poveča hitrost delovanja.
|
1. *Samostoječa in vdelana:* SQLite deluje kot del programa in ne zahteva ločenega procesa ali strežnika. To pomeni, da aplikacija komunicira neposredno s podatkovno bazo, kar zmanjša kompleksnost in poveča hitrost delovanja.
|
||||||
|
|
||||||
2. *Enostavnost in prenosljivost* Celotna baza podatkov je shranjena v eni datoteki, kar olajša prenos in upravljanje. Datoteka je preprosto kopirljiva med različnimi sistemi, ne da bi potrebovali dodatne konfiguracije.
|
2. *Enostavnost in prenosljivost:* Celotna baza podatkov je shranjena v eni datoteki, kar olajša prenos in upravljanje. Datoteko lahko preprosto kopiramo med različnimi sistemi, ne da bi potrebovali dodatne konfiguracije.
|
||||||
|
|
||||||
3. *Nizke zahteve* SQLite ne potrebuje nastavljanja ali upravljanja, kar jo naredi idealno za uporabo v mobilnih napravah, vgrajenih sistemih in manjših aplikacijah, kjer so viri omejeni.
|
3. *Nizke zahteve:* SQLite ne potrebuje nastavljanja ali upravljanja, kar jo naredi idealno za uporabo v mobilnih napravah, vgrajenih sistemih in manjših aplikacijah, kjer so viri omejeni.
|
||||||
|
|
||||||
4. *Podpora za SQL* SQLite podpira večino standardnih SQL ukazov (npr. `SELECT`, `INSERT`, `UPDATE`, `DELETE`), kar omogoča enostavno upravljanje podatkov z uporabo poznanega jezika.
|
|
||||||
|
|
||||||
5. *Zanesljivost in varnost* SQLite zagotavlja transakcijsko varnost (ACID), kar pomeni, da so podatki vedno dosledni, tudi v primeru napak ali prekinitve dela.
|
|
||||||
|
|
||||||
6. *Odprtokodna in brezplačna* SQLite je odprtokodna in brezplačna, kar pomeni, da jo lahko prostovoljno uporabljate v komercialnih in nekomercialnih projektih.
|
|
||||||
|
|
||||||
|
4. *Podpora za SQL:* SQLite podpira večino standardnih SQL ukazov (npr. `SELECT`, `INSERT`, `UPDATE`, `DELETE`), kar omogoča enostavno upravljanje podatkov z uporabo poznanega jezika.
|
||||||
|
|
||||||
|
5. *Zanesljivost in varnost:* SQLite zagotavlja transakcijsko varnost (ACID), kar pomeni, da so podatki vedno dosledni, tudi v primeru napak ali prekinitve dela.
|
||||||
|
|
||||||
|
6. *Odprtokodna in brezplačna:* SQLite je odprtokodna in brezplačna, kar pomeni, da jo lahko prostovoljno uporabljamo v komercialnih in nekomercialnih projektih.
|
||||||
|
|
||||||
=== Uporaba SQLite:
|
=== Uporaba SQLite:
|
||||||
|
|
||||||
- *Mobilne aplikacije* SQLite je priljubljena izbira za shranjevanje podatkov v mobilnih aplikacijah (npr. Android, iOS), saj ne potrebuje strežnika in deluje lokalno na napravi.
|
- *Mobilne aplikacije:* SQLite je priljubljena izbira za shranjevanje podatkov v mobilnih aplikacijah (npr. Android, iOS), saj ne potrebuje strežnika in deluje lokalno na napravi.
|
||||||
|
|
||||||
- *Vgrajeni sistemi* Zaradi svoje preprostosti in nizkih zahtev je SQLite pogosto uporabljena v vgrajenih sistemih, kot so pametni ure, senzorji in druga naprava z omejenimi viri.
|
- *Vgrajeni sistemi:* Zaradi svoje preprostosti in nizkih zahtev je SQLite pogosto uporabljena v vgrajenih sistemih, kot so pametne ure, senzorji in druge naprave z omejenimi viri.
|
||||||
|
|
||||||
- *Spletne aplikacije* Uporablja se za lokalno shranjevanje podatkov v brskalnikih (npr. Web SQL API) ali kot preprosta rešitev za manjšo spletno stran.
|
- *Spletne aplikacije:* Uporablja se za lokalno shranjevanje podatkov v brskalnikih (npr. Web SQL API) ali kot preprosta rešitev za manjšo spletno stran.
|
||||||
|
|
||||||
- *Testiranje in razvoj* SQLite je pogosto uporabljena med razvojem in testiranjem, saj omogoča hitro in enostavno ustvarjanje in upravljanje podatkovnih baz.
|
- *Testiranje in razvoj:* SQLite je pogosto uporabljena med razvojem in testiranjem, saj omogoča hitro in enostavno ustvarjanje in upravljanje podatkovnih baz.
|
||||||
|
|
||||||
- *Aplikacije za namizje* Številne namizne aplikacije uporabljajo SQLite za lokalno shranjevanje podatkov, saj ne zahteva dodatne infrastrukture.
|
- *Aplikacije za namizje:* Številne namizne aplikacije uporabljajo SQLite za lokalno shranjevanje podatkov, saj ne zahteva dodatne infrastrukture.
|
||||||
|
|
||||||
SQLite je torej idealna rešitev, če potrebujete preprosto, hitro in zanesljivo podatkovno bazo, ki deluje neposredno v vaši aplikaciji. Je ena najpogosteje uporabljanih podatkovnih baz na svetu, predvsem zaradi svoje preprostosti in učinkovitosti.
|
SQLite je torej idealna rešitev, če potrebujemo preprosto, hitro in zanesljivo podatkovno bazo, ki deluje neposredno v naši aplikaciji. Je ena najpogosteje uporabljanih podatkovnih baz na svetu predvsem zaradi svoje preprostosti in učinkovitosti.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#pagebreak()
|
#pagebreak()
|
||||||
|
|
||||||
= STRUKTURA APLIKACIJE
|
= STRUKTURA APLIKACIJE
|
||||||
Aplikacija ima tri glavne enote DNA menedžer, DNA vizualizator in Jedro. DNA manedžer skrbi za shranjevanje trenutnega dna in mutacijo, DNA vizualizator pa skrbi za izris dna, njegovo delo je, da dobi dna sekvenco in jo pretvori v sliko. V jedru se pa nahajajo ostale komponente, ki so pomembne za delovanje aplikacije, omrežni vmesnik in vmesnik za shranjevanje.
|
Aplikacija ima tri glavne enote: DNA menedžer, DNA vizualizator in jedro. DNA manedžer skrbi za shranjevanje trenutnega dna in mutacijo, DNA vizualizator pa skrbi za izris dna, njegovo delo je, da dobi dna sekvenco in jo pretvori v sliko. V jedru se nahajajo ostale komponente, ki so pomembne za delovanje aplikacije, omrežni vmesnik in vmesnik za shranjevanje.
|
||||||
|
|
||||||
Jedro je hrbtenica, ki povezuje vse komponente, ob zagonu preračuna razmerja za prikaz besedila in slik dreves, skrbi za premik in rotacijo slike ko s prstom povlečemo po zaslonu, skrbi da, ko pridemo s prstom do roba in hočemo označiti sliko da se izbira pošlje v dna manager ki nam nato vrne nasledni dna za prikaz jedro tukaj poskrbi da se prenese v dna vizualizator in ko dobi sliko, da jo prikaže uporabniku, po vsaki izbiri se izbira tudi shrani na disk, ko pridemo pa do konca generacije se še cela generacija shrani na disk in nato se tudi pošlje na strežnik.
|
Jedro je hrbtenica, ki povezuje vse komponente, ob zagonu preračuna razmerja za prikaz besedila in slik dreves, skrbi za premik in rotacijo slike, ko s prstom povlečemo po zaslonu, skrbi, da ko pridemo s prstom do roba in hočemo označiti sliko, se izbira pošlje v DNA manager, ki nam nato vrne naslednji DNA za prikaz. Jedro tukaj poskrbi da se prenese v DNA vizualizator in ko dobi sliko, da jo prikaže uporabniku, se po vsaki izbiri ta tudi shrani na disk, ko pridemo pa do konca generacije, se še cela generacija shrani na disk in nato se tudi pošlje na strežnik.
|
||||||
|
|
||||||
#figure(
|
#figure(
|
||||||
image("assets/potek_diagram/diagram.svg", height: 400pt),
|
image("assets/potek_diagram/diagram.svg", height: 400pt),
|
||||||
@@ -406,23 +401,23 @@ Jedro je hrbtenica, ki povezuje vse komponente, ob zagonu preračuna razmerja za
|
|||||||
|
|
||||||
#pagebreak()
|
#pagebreak()
|
||||||
|
|
||||||
== UPORABNIŠKI VMESNIK - ROTACIJA SLIKE
|
== UPORABNIŠKI VMESNIK ‒ ROTACIJA SLIKE
|
||||||
Proces rotiranja slike glede na premik miške, je sestavljen iz treh korakov
|
Proces rotiranja slike glede na premik miške je sestavljen iz treh korakov
|
||||||
in uporabo trigometrije
|
in uporabe trigometrije.
|
||||||
|
|
||||||
=== Prvi korak: Ob pritisku miške
|
=== Prvi korak: Ob pritisku miške
|
||||||
V prvem koraku moramo izračunati velikost(magnituda) in kot vektorja med miško in levim spodnim kotom slike (Cyan vektor @Prikaz_vektorjev) in shraniti si moramo trenutno pozicijo miške (Oranžni vektor/Zelena pika @Prikaz_vektorjev)
|
V prvem koraku moramo izračunati velikost (magnitudo) in kot vektorja med miško in levim spodnjim kotom slike (Cyan vektor @Prikaz_vektorjev) in shraniti si moramo trenutno pozicijo miške (Oranžni vektor/Zelena pika @Prikaz_vektorjev).
|
||||||
|
|
||||||
$
|
$
|
||||||
"mouseStartV" = "mouseV"\
|
"mouseStartV" = "mouseV"\
|
||||||
"sizeOfVectorMI" = sqrt(("mouseV"_x - "imageV"_x)^2 + ("mouseV"_y - "imageV"_y)^2)\
|
"sizeOfVectorMI" = sqrt(("mouseV"_x - "imageV"_x)^2 + ("mouseV"_y - "imageV"_y)^2)\
|
||||||
"oldAngleOfVectorMI" = "atan2"("imageV"_x - "mouseV"_x, "imageV"_y - "mouseV"_y)
|
"oldAngleOfVectorMI" = "atan2"("imageV"_x - "mouseV"_x, "imageV"_y - "mouseV"_y)
|
||||||
$
|
$
|
||||||
- $"mouseV"$ - trenutni vektor miške
|
- $"mouseV"$ ‒ trenutni vektor miške
|
||||||
- $"imageV"$ - vektor pozicije slike (Modri vektor @Prikaz_vektorjev)
|
- $"imageV"$ ‒ vektor pozicije slike (Modri vektor @Prikaz_vektorjev)
|
||||||
- $"vectorMI"$ - vectorMouseImage - vector med miško in sliko
|
- $"vectorMI"$ ‒ vectorMouseImage ‒ vector med miško in sliko
|
||||||
- $"sizeOfVectorMI"$ - velikost vektorjaMI
|
- $"sizeOfVectorMI"$ ‒ velikost vektorjaMI
|
||||||
- $"oldAngleOfVectorMI"$ - kot vektorjaMI v radianih
|
- $"oldAngleOfVectorMI"$ ‒ kot vektorjaMI v radianih
|
||||||
|
|
||||||
#grid(
|
#grid(
|
||||||
columns: 2,
|
columns: 2,
|
||||||
@@ -443,7 +438,7 @@ $
|
|||||||
|
|
||||||
#pagebreak()
|
#pagebreak()
|
||||||
=== Drugi korak: Medtem ko miško držimo stisnjeno
|
=== Drugi korak: Medtem ko miško držimo stisnjeno
|
||||||
Sedaj lahko izračunamo rotacijo slike glede na horizontalni premik miške,in posledično novo pozicijo slike.
|
Sedaj lahko izračunamo rotacijo slike glede na horizontalni premik miške in posledično novo pozicijo slike.
|
||||||
|
|
||||||
$
|
$
|
||||||
Delta"mouseV"_x & = "mouseV"_x - "mouseStart"_x \
|
Delta"mouseV"_x & = "mouseV"_x - "mouseStart"_x \
|
||||||
@@ -453,15 +448,15 @@ $
|
|||||||
"rotationRad" & = (("rotation"* pi) / 180.0)
|
"rotationRad" & = (("rotation"* pi) / 180.0)
|
||||||
$<mat_rotacija>
|
$<mat_rotacija>
|
||||||
|
|
||||||
- $"screenWidth"$ - širina zaslona
|
- $"screenWidth"$ ‒ širina zaslona
|
||||||
- $Delta"mouseV"_x$ - sprememba pozicije $[-"screenWidth", "screenWidth"]$
|
- $Delta"mouseV"_x$ ‒ sprememba pozicije $[-"screenWidth", "screenWidth"]$
|
||||||
- $a$ - normalizirano na razpon $[-1, 1]$
|
- $a$ ‒ normalizirano na razpon $[-1, 1]$
|
||||||
- $b$ - normalizirano na razpon $[0, 1]$
|
- $b$ ‒ normalizirano na razpon $[0, 1]$
|
||||||
- $"rotation"$ - kot za koliko se more slika obrniti v stopinjah
|
- $"rotation"$ ‒ kot za koliko se more slika obrniti v stopinjah
|
||||||
- $"rotationRad"$ - v radianih
|
- $"rotationRad"$ ‒ v radianih
|
||||||
|
|
||||||
#v(10pt)
|
#v(10pt)
|
||||||
V @mat_rotacija Za $Delta"mouseV"$ lahko vidimo v (@Prikaz_razdalje) da, ko je začetek v zeleni piki in se premikamo proti modri je delta pozitivna in ko se premikamo proti rdeči je negativna zato potrebujemo normalizacijo $a$ in $b$ ker lerp sprejme vrednosti med $[0, 1]$
|
V @mat_rotacija za $Delta"mouseV"$ lahko vidimo na (@Prikaz_razdalje), da ko je začetek v zeleni piki in se premikamo proti modri, je delta pozitivna in ko se premikamo proti rdeči, je negativna, zato potrebujemo normalizacijo $a$ in $b$ ker LERP sprejme vrednosti med $[0, 1]$.
|
||||||
#v(10pt)
|
#v(10pt)
|
||||||
|
|
||||||
$
|
$
|
||||||
@@ -473,27 +468,27 @@ $
|
|||||||
"imageRotation" & = 90 + "rotation"
|
"imageRotation" & = 90 + "rotation"
|
||||||
$
|
$
|
||||||
|
|
||||||
- $"imageRotation"$ - novi kot slike v stopinjah
|
- $"imageRotation"$ ‒ novi kot slike v stopinjah
|
||||||
- $"imageV"$ - pozicija slike
|
- $"imageV"$ ‒ pozicija slike
|
||||||
|
|
||||||
#v(10pt)
|
#v(10pt)
|
||||||
Sedaj lahko končno narišemo sliko na poziciji $"imageV"$ in pot kotom $"imageRotation"$
|
Sedaj lahko končno narišemo sliko na poziciji $"imageV"$ in pot kotom $"imageRotation"$.
|
||||||
|
|
||||||
#pagebreak()
|
#pagebreak()
|
||||||
=== Tretji korak: Ob spustitvi miške
|
=== Tretji korak: Ob spustitvi miške
|
||||||
Ko miško spustimo moramo samo ponastaviti vrednosti slike da je spet pokončno in jo postaviti nazaj na originalno pozicijo.
|
Ko miško spustimo, moramo samo ponastaviti vrednosti slike, da je spet pokončno, in jo postaviti nazaj na originalno pozicijo.
|
||||||
|
|
||||||
$
|
$
|
||||||
"imageRotation" & = 90 \
|
"imageRotation" & = 90 \
|
||||||
"imageV" & = "orgImageV"
|
"imageV" & = "orgImageV"
|
||||||
$
|
$
|
||||||
|
|
||||||
- $"orgImageV"$ - originalna pozicija slike
|
- $"orgImageV"$ ‒ originalna pozicija slike
|
||||||
|
|
||||||
=== Uporabljene funkcije
|
=== Uporabljene funkcije
|
||||||
|
|
||||||
==== atan2
|
==== atan2
|
||||||
Funkcija atan2 je matematična funkcija, ki se uporablja za izračun tangente loka količnika, vendar namesto enega samega razmerja ($y/x$) vzame dva ločena argumenta ($y$ in $x$). Zaradi tega je še posebej uporaben za določanje kota v pravilnem kvadrantu, ki temelji na znakih obeh koordinat. @wiki_atan2:
|
Funkcija atan2 je matematična funkcija, ki se uporablja za izračun tangente loka količnika, vendar namesto enega samega razmerja ($y/x$) vzame dva ločena argumenta ($y$ in $x$). Zaradi tega je še posebej uporabna za določanje kota v pravilnem kvadrantu, ki temelji na znakih obeh koordinat @wiki_atan2.
|
||||||
|
|
||||||
$
|
$
|
||||||
"atan2"(x, y) := cases(
|
"atan2"(x, y) := cases(
|
||||||
@@ -507,7 +502,7 @@ $
|
|||||||
$
|
$
|
||||||
|
|
||||||
==== LERP
|
==== LERP
|
||||||
Lerp (okrajšava za linearno interpolacijo) je matematična tehnika, ki se uporablja za iskanje vrednosti med dvema danima vrednostma na podlagi oločenega interpolacijskega parametra, ki je pogosto označen kot $t$. @wiki_lerp
|
Lerp (okrajšava za linearno interpolacijo) je matematična tehnika, ki se uporablja za iskanje vrednosti med dvema danima vrednostma na podlagi določenega interpolacijskega parametra, ki je pogosto označen kot $t$ @wiki_lerp.
|
||||||
|
|
||||||
$
|
$
|
||||||
"lerp"(v 0, v 1, t) = v 0 + t * (v 1 - v 0)
|
"lerp"(v 0, v 1, t) = v 0 + t * (v 1 - v 0)
|
||||||
@@ -520,15 +515,15 @@ $
|
|||||||
#let like = `like`
|
#let like = `like`
|
||||||
#let newGen = `newGen`
|
#let newGen = `newGen`
|
||||||
|
|
||||||
Namenjena uporaba je, najprej pokličemo #next, ki nam vrne objekt z DNA podatki in indeks DNA-a, ko se uporabnik odloči ali mu je všec, to sporočimo managerju preko funkcije #like, ko nam #like vrne `false` in #next vrne `nullptr` vemo, da smo prikazali in označili vse DNA-je v generaciji in moramo generirati novo generacijo s klicom #newGen. (@dna_manager)
|
Namenjena uporaba je, najprej pokličemo #next, ki nam vrne objekt z DNA podatki in indeks DNA-ja, ko se uporabnik odloči, ali mu je všeč, to sporočimo managerju preko funkcije #like, ko nam #like vrne `false` in #next vrne `nullptr`, vemo, da smo prikazali in označili vse DNA-je v generaciji in moramo generirati novo generacijo s klicem #newGen (@dna_manager).
|
||||||
|
|
||||||
Dna manager nam dovoli, da večkrat pridobimo naslednji DNA brez, da takoj označimo ali nam je všeč. V aplikaciji je to uporabljeno tako da pridobimo dva DNA za prikaz. Prvi je na vrhu in ga lahko premikamo in označimo, drugi je pa v ozadju. Po označitvi se zgornji in spodnji zamenjata in spodnji se zamenja z naslednjim.
|
Dna manager nam dovoli, da večkrat pridobimo naslednji DNA, ne da takoj označimo ali nam je všeč. V aplikaciji je to uporabljeno tako, da pridobimo dva DNA-ja za prikaz. Prvi je na vrhu in ga lahko premikamo in označimo, drugi je pa v ozadju. Po označitvi se zgornji in spodnji zamenjata in spodnji se zamenja z naslednjim.
|
||||||
|
|
||||||
Funkcija #next vrne DNA iz `voctor[queued]` in ga nato inkrementira `queued` če je `queued` večji od `vector` vrne `nullptr`.
|
Funkcija #next vrne DNA iz `vector[queued]` in ga nato inkrementira `queued`, če je `queued` večji od `vector`, vrne `nullptr`.
|
||||||
|
|
||||||
Funkcija #like sprejme indeks in liked in nato shrani index v vector liked ali disliked, nato inkrementira `seen` in če je večji od `vector` vrne `false`.
|
Funkcija #like sprejme indeks in liked in nato shrani index v vector liked ali disliked, nato inkrementira `seen` in če je večji od `vector`, vrne `false`.
|
||||||
|
|
||||||
Medtem funkcija #newGen vsebuje glavno logiko. Deluje na principu naravne evolucije naključno izbere dva DNA in ustvari otroka tako da naključno kopira DNA od obeh staršev, da ima v povprečju 50% DNA od enega starša in 50% od drugega starša. Ko ustvari novo generacijo celotno generacijo mutira tako da naključno spremeni specificirano število genov(byte). Tukaj lahko upazimo robni primer kaj če imamo samo en ali nič DNA-jev ki so uporabniku všeč, v primeru da kloniramo dokler ne ustvarimo novo generacijo in v primeru ko uporabniku ni noben všeč generiramo novo naključno generacijo.
|
Medtem funkcija #newGen vsebuje glavno logiko. Deluje na principu naravne evolucije. Naključno izbere dva DNA in ustvari otroka tako, da naključno kopira DNA od obeh staršev, da ima v povprečju 50 % DNA od enega starša in 50 % od drugega. Ko ustvari novo generacijo, celotno generacijo mutira tako, da naključno spremeni specificirano število genov (byte). Tukaj lahko opazimo robni primer, kaj, če imamo samo enega ali nič DNA-jev, ki so uporabniku všeč, v primeru, da kloniramo, dokler ne ustvarimo nove generacije in v primeru, ko uporabniku ni nobeden všeč, generiramo novo naključno generacijo.
|
||||||
|
|
||||||
#figure(caption: [DnaManager])[
|
#figure(caption: [DnaManager])[
|
||||||
#grid(
|
#grid(
|
||||||
@@ -579,7 +574,7 @@ Medtem funkcija #newGen vsebuje glavno logiko. Deluje na principu naravne evoluc
|
|||||||
#pagebreak()
|
#pagebreak()
|
||||||
|
|
||||||
== DNA
|
== DNA
|
||||||
Dna je sestavljen iz dveh struktur, (@dna_objekt). Zanimljiv del ki ga opazimo je da je vsak gen točno en bite dolg in celoten objekt je v pomnilniku samo en velik list bitov (array of bites). To značinost uporabljamo v complementarnih funkcijah `newDna`, `makeChiled`, `clone` in `mutate`.
|
Dna je sestavljen iz dveh struktur (@dna_objekt). Zanimliv del, ki ga opazimo, je, da je vsak gen točno en bite dolg in celoten objekt je v pomnilniku samo en velik list bitov (array of bites). To značinost uporabljamo v complementarnih funkcijah `newDna`, `makeChiled`, `clone` in `mutate`.
|
||||||
|
|
||||||
#figure(caption: [DNA objekt])[
|
#figure(caption: [DNA objekt])[
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user