-
Notifications
You must be signed in to change notification settings - Fork 3
Dokumentacja i opis projektu
Spis treści:
Ogólne założenia
uwagi
Biblioteki
Hierarchia
Wygląd GUI
Mechanika
Ogólne
App
Engine
FileController (wcześniej CircuitParser)
Data
Circuit
Grafika
Canvas
GCircuit
GBlock
GPort
Bramki
Gate
And, Or, Xor, Not ...
Clock
Delay
Input
Switch
Własne bloki
Ogólne założenia: TOP
Program reprezentuje graficznie układy logiczne złożone z prostych bramek logicznych, guzików, włączników i zegarów. Program umożliwia tworzenie zapisywanie i wczytywanie układów z plików w formacie CSV i JSON, dodawanie własnych bloków, interakcję z układem, symulowanie ciągłe z ustawionym przez użytkownika taktowaniem oraz symulowanie krok po kroku. Dodatkowo program może monitorować stan elementów i wypisywać je do plików typu '.log'. Wszystkie pliki należą do namespace Logicon
.
uwagi: TOP
Program musi korzystać z pliku config, który bedzie miał zapisany wygląd podstawowych bramek w formacie, który da się przetłumaczyć na klasę typu Data.
Domyslny układ współrzędnych jest taki, że x
zwiększa się w prawo a y
w dół. od lewego górnego rogu elementu chyba że napisane inaczej.
port
: Jeśli będę mówił gdzieś o portach, to chodzi o inputy i outputy, z albo bez rozróżnienia, to już wynika z kontekstu. Najczęściej chodzi o to, że trzeba wygenerować metody dla obu i sprawdzać czy chodzi nam o ten port etc.
Czasami wartości zwracane przez funkcje zamiast obiektami będą wskaźnikami na obiekty. W takich wypadkach należy używać smart pointerów (weak_ptr, shared_ptr, uniqe_ptr) - operatory działają jak na zwykłych wskaźnikach ale są ciut "lepsze"
ID
: jeszcze nie sprecyzowaliśmy jak reprezentujemy ID, póki co stosuję liczby > 0 jako ID
Point
: dla uproszczenia będę pisał point, ale może być jako para x,y lub nawet jako osobne argumenty czyli f(Point) = f(x,y)
Data
: prawdopodobnie będzie można zaimplementować później, ale nie jest wykluczone, że będzie potrzebne do działania zegara - wszędzie gdzie jest pole typu Data musi być funkcja getData() żeby można było wykonywać operacje na danych
[]
: nie oznacza konkretnie tablicy a jakikolwiek kontener - zalezy od miejsca - czasem może to być lista, czasem vector etc.
tuple
: to inaczej krotki - żeby nie używać par<par> etc. - krotki są fajne ;)
Biblioteki: TOP
- OpenGL - renderowanie okna i baza dla ImGUI
- glew, gl3w - dodatki do OpenGL ułatwiające pracę z grafiką
- ImGui - główne GUI aplikacji
- json - obsługa formatu JSON
Hierarchia: TOP
ogólne
- App - główna klasa aplikacji;
- Engine - silnik prowadzący symulację
- FileController (wcześniej CircuitParser) - klasa zajmująca się interakcją aplikacja <-> pliki
- Data - klasa zajmująca się wewnętrznymi danymi bramek, które można zmieniać
- Circuit - klasa reprezentująca układ logiczny - reprezentacja grafu
- types.h - header zawierający wszystkie aliasy typów w projektu
grafika
- Canvas - plansza do graficznej edycji układu
- MenuWidget - widget ImGui do zarządzania plikami i symulacją
- BlocksWidget - widget ImGui do dodawania bloków z palety
- FooterWidget - widget ImGui do wyświetlania informacji
- ContextMenuWidget - widget ImGui odpalający menu kontekstowe dla elementu
- GCircuit - graficzna reprezentacja układu
- GBlock - graficzna reprezentacja bloków
- GPoert
bramki
- Gate - klasa abstrakcyjna reprezentująca bramkę, po której dziedziczą wszystkie pozostałe
- ^ And, Or, Not, Xor etc. - proste bramki z prostą logiką liczenia kolejnego stanu
- ^ Clock - klasa zegara który włącza się i wyłącza cyklicznie w zależności od czasu
- ^ Delay - puszcza sygnał z zadanym opóźnineniem; konfigurowalny jak Clock
- ^ Input - reaguje na kliknięcie; zmienia stan wyjścia przy kliknięciu (taki toggle button)
- ^ Switch - reaguje na kliknięcie; jeśli jest jest w stanie ON przekazuje wejście na wyjście (przełącznik)
Wygląd GUI TOP
MenuWidget: [nowy, otwórz, zapisz] [start, pauza, krok, restart] [pole liczbowe: tickrate] BlocksWidget: przewijana lista klocków reprezentujących bramki logiczne FooterWidget: stópka z tekstem informacyjnym w prawym dolnym rogu - nazwa układu, ilość jakich komponentów etc. Canvas: przewijana (może nieskończona) plansza 2D z siatką o odstępach 32x32 px na której wyświetlany jest model układu
Mechanika TOP
Ogólne TOP
App TOP
App
tworzy okienko z OpenGLa, ma singletony(chyba) widgetów, planszy i silnika. Słóży jako kontener na właściwą aplikację i GUI. Do tego musi generować kolejne ID bloków za pomocą metody nextID()
.
// psudokod klasy App
class App{
Clock clock;
Engine engine;
MenuWidget menu;
BlocksWidget blockMenu;
FooterWidget footer;
Canvas canvas;
GCircuit gCircuit;
Time tickrate // w milisekundach czas co ile ma odpalić się engine.calcLogic()
STATE state = {UNINITIALIZED, RUNNING, PAUSED, STEP_BY_STEP} // w zależności od stanu
bool init();
void render_ui();
bool close();
void run(){
init();
while(){
render_ui();
// UI events
if(tickrate /**/ && STATE){
engine.calcLogic(gCircuit.circuit);
// redraw GCircuit
}
}
close();
}
ID nextID(); // zwraca następne dostępne ID
}
Engine TOP
Engine
liczy stan układu (może przekazać mu Circuit
do policzenia w argumencie calculateLogic(c)?
). Silnik musi wspierać funkcję do zainicjalizowania układu od nowa oraz funkcję do policzenia następnego stanu. To w enginie siedzi algorytm do liczenia następnych stanów grafu reprezentowanego przez circuit. Trzeba też zadbać o poprawne liczenie i inicjalizowanie układu z cyklami (np. NOT łączący się z samym sobą). Bramki, których wejść nie da się bezpośrednio zainicjalizować mają mieć domyślnie na tych wejściach podane 0 (iinymi słowy, jeśli jest ciąg bramek, które w jakiś sposób się zapętlają i bramka, która jako jedno z wejść przyjmuje wartość zależną od obliczeń tej samej bramki, to to wejście ma być interpretowane jako 0 np. bramka AND o wejściach A i B która wypluwa wynik na C, ale z C sygnał przechodzi przez inne bramki i wraca do wejścia A dodatkowo będąc niejednoznaczny(nie jesteśmy w stanie stwierdzić czy zawsze jest prawdziwy czy fałszywy), to to wejście traktujemy jako 0)
class Engine{
void restart(Circuit c); // inicjalizuje układ na stan początkowy (faktyczny stan układu w zerowym ticku)
void calcLogic(Circuit c); // liczy następny stan układu - tutaj siedzi algorytm
}
FileController (wcześniej CircuitParser) TOP
FileController
musi być w stanie zwrócić nowy obiekt typu GCircuit
oraz Circuit
z pliku w formacie CSV lub JSON i vice versa. Parser do CSV trzeba napisać samemu, natomiast do JSON-a można skorzystać z gotowej biblioteki. Trzeba też sprawdzać poprawność danych, żeby przypadkiem czegoś nie spieprzyć i pamiętać żeby przy odpalonych kilku instancjach programu nie zepsuć zapisywania do pliku.
Data TOP
Data
ma być klasą reprezentującą dane - inaczej jest to zbiór zmiennych o różnych typach które mają swoje odpowiednie wartości. Dane mogłyby być np. w formacie JSON i odczytywanie i zapisywanie wartości odbywało by się przez znajdowanie danych pól i zmienianiu im wartości. Możemy całe Data zrobić jako opakowanie dla klasy od JSONa i dopisać jedynie klasy, które produkują odpowiednie klasy na podstawie plików JSON. Powinna mieć możliwość ustawiania i pobierania danych - coś typu get/setData("klucz", "opcja@wartość")
Na pewno potrzebne są w Data
- pola do pamiętania: obrazków, labeli, opisu, tablic napisów, wartości liczbowych i nazw zmiennych.
{
// ...
"gates": [
{
"name": "and",
"description": "and gate",
"icon": "graphics/icons/and.png",
"inputs": [
{
"label": "A",
"side":"EAST",
"coord": [0, 0]
}
],
"extras": {
"Hz": 0.5
}
},
// pozostałe bramki
]
}
Bardzo ułatwiłaby pracę z opcjami dla układów, bo sprawdzalibyśmy jedyni czy bramka.data
jest puste i jeśli nie jest, to odpalali okienko z możliwością zmiany tych danych
Circuit TOP
Circuit
reprezentuje układ bramek - jest to nasza reprezentacja "grafu", którego wierzchołkami są poszczególne bramki. Musi zawierać swój unikalny identyfikator ID
albo CircuitID
listę wszystkich bramek, które wchodzą w jego skład, pole data
przechowujące wszystkie potrzebne dane, metody connect(ID1, output, ID2, input)
. disconnect(ID1, input, ID2, output)
, add(Gate)
, remove(ID)
do zmieniania struktury grafu, find(ID)
(wcześniejsze getBlockByID(ID)
), getGates()
do zwracania bramek które wchodzą w jego skład.
class Circuit{
int ID;
Gate gates[];
Data data;
void connect(ID1, output, ID2, input);
void disconnect(ID1, output, ID2, input);
void add(Gate g); // dodaje bramkę do grafu, łączyć trzeba samemu
void remove(ID); // usuwa z grafu bramkę i usuwa wszystkie połączenia
Gate find(ID); // zwraca bramkę o danym ID
Gate[] getGates(); // zwraca całą listę bramek
}
Grafika TOP
Canvas TOP
Widget z Imgui w którym wyświetlamy nasz układ. Musi renderować pomocniczą siatkę, mieć scrolbary i ewentualnie jakieś inne bajery do zmieniania widoku. Domyślny układ współrzędnych, tyle że koordynaty (0,0)
powinny być wyświetlane na środku. Canvas
wyświetla tylko jeden GCircuit
(w pszyszłości można zmienić).
GCircuit TOP
GCircuit (singleton?) jest graficzną reprezentacją układu na planszy - ma określone wymiary, referencję do Circuit
oraz listę GBlocków
, do tego zajmuje się wyłapywaniem eventów związanych z wstawianiem bloczka i usuwa blok o podanym ID zarówno z GCircuita jak i z Circuita. Jest podzielony na macierz z kwadracików o wymiarach siatki czyli 32x32 px (czy jakoś tak żeby pokrywało się z siatką). Dodatkowo musi umieć zwracać ID GBlocka w danym punkcie. Chyba może mieć w sobie funkcje do łączenia i rozłączania dwóch portów (zamiast umieszczać je w GInput i GOutput).
GCircuit{
int width, height; // ile kwadratów 32x32 zajmuje
Circuit circuit; // referencja do circuita który reprezentuje
tuple<GBlock, Point> blocks[]; // lista tupli graficznych bloków z ich pozycjami
void insert(x, y, Block); // wkleja blok w (x,y)
void remove(ID); // usuwa blok o danym ID
void move(ID, Point destination) // zmienia pozycję bloku o ID na `destination`
void connenct(...) // łączy port `a` z `b` - wywołuje circuit.connect()
void disconnect(...) // rozłącza wszystkie połączenia z portem - wywołuje curcuit.disconnect()
bool isOccupied(ID, Point a, Point d); // sprawdza czy w prostokącie wyznaczonym przez rogi `a` i `d`
// istnieje element inny niż ten o danym ID
ID getIdAt(x, y); // zwraca ID bloku po kliknięciu w (x,y)
GBlock getGBlockByID(ID); // zwraca GBlock o ID - potrzebne przy szukaniu klikniętych portów
<ID, port> getElementAt(Point); // zwraca parę ID, port - jeśli któryś element nie został najechany,
// wartość ma być NULL
private? void update(); // ściąga stan z c i aktualizuje każdy element (prywatne i wywołane lokalnie w render()?)
void render(); // rysuje układ - rysuje bloki w ich współrzędnych a następnie kable jako krzywe Beziera
}
GBlock TOP
GBlock
powinien być zaimplementowany jako ImGui::ImageButton
. Służy reprezentacji graficznej bloku w GCircuit
. Ma pola określające ID
(albo referencję do Block
?), wymiary bloku, listę gInputs[]
, gOutputs[]
, img
oraz data
(Potem będzie można usunąć wszystkie informacje poza ID
i indeksem
z GInputów i GOutputów i pamiętać wszystkie te informacje w polu data).
- Jesli usuwamy blok za pomocą menu kontekstowego, to musi wspierać akcje PPM do wyświetlania menu z opcjami (Edit|Delete) (oczywiście kiedy edit jest nie dozwolone to tego nie wyswietla)
- kliknięcie LPM do zmiany stanu guzika (triggeruje
block.clickAction()
) - przeciągnięcia do poruszenia guzika(można też hamsko usuwać element za pomocą skrótów klawiszowych.
Musi mieć referencję do GCircuita, żeby móc korzystać z metod move(destination)
przy przeniesieniu, i remove(this.ID)
przy usuwaniu. Dodatkowo musi mieć funkcje getPortAt(Point p)
zwracającą indeks portu w danym punkcie (zwraca np. -1 jak to nie jest dobry port albo w tym miejscu nie istnieje, albo jest poza blokiem itd.). Można potem renderować na czerwono podczas przesuwania jeśli nie da się przenieść w dane miejsce albo na zielono jeśli można.
class GBlock{
GCircuit parent; // do funkcji move, getIdAd(), isOccupied() i dla inputów
int ID;
int width, height;
Point position; // position in GCircuit
GInput gInputs[]; // one same w swoim obrębie wykrywają łączenie kablami
GOutput gOutputs[]; // może się okazać, że wystarczy jedna klasa GPort zamiast GIn i GOut
Image img;
Data data; // dane typu label etc. (może potem?)
void clickAction(){
if(LPM) parent.circuit.getBlockById(ID).clickAction(); // triggeruje clickAction bramki
if(PPM); // usuwanie albo menu kontekstowe albo opcje
if(DRAGGED)
if(parent.isOccupied(this)) render(RED); // koloruje na czerwono jeśli nie można przesunąć
else parent.move(this)
}
int getPortAt(Point p); // zwraca indeks portu w tej współrzędnej, relatywnie do środka układu współrzędnych canvas albo jakoś (po metodzie do input i output)
private? void update(); // ściąga info z bloku (prywatne i wywołane lokalnie w render()?)
void render(); // renderuje blok i wywołuje dla każdego portu render()
}
GPort TOP
GPort
reprezentuje wejście i wyjście do/z bloku. W związku z tym ma referencję na blok do którego należy, pamięta swoją pozycję w tablicy gInputs[]
lu b gOutputs[]
w zależności od typu portu, swoje współrzędne względem lewego górnego rogu, stronę, od której powinien wchodzić kabel SIDE = {EAST, WEST, SOUTH, NORTH}
oraz dodatkowe pole Data zawierające dodatkowe informacje. Powinien być zaimplementowany jako ImGui::ImageButton
i wykrywać w swoim zakresie akcje takie jak kliknięcia i przeciągnięcia.
- Przy naciśnięciu PPM rozłącza wszystkie połączenia portu. Dla inputa taki kabel jest jeden więc wystarczy kliknąć. Dla outputów może istnieć wiele kabli wychodzących, wtedy należy usunąć wszytskie kable. W przyszłości można otwierać menu kontekstowe z listą podpiętych bloków i portów, żeby wybrać które połączenie usunąć.
- Przeciąganie output->input dodaje nowy kabel do outputa i usuwa każdy obecnie podpięty do inputa kabel.
- Przeciąganie input->output usuwa każdy kabel podpięty do inputa i dodaje nowy kabel do outputa.
Ilość GInputów
i Inputów
dla bloku o danym ID
jest taka sama, więc mapowanie który input jest który pomiędzy GBlock
i Block
odbywa się jedynie na zasadzie porównania indeksów w odpowiednich tablicach (czyli gInputs[7]
w Gblock
odpowiada inputs[7]
w Block
). Powinny wywoływać na GCircuit getGBlockAt(x,y) żeby dostać ID
bloku z którym mają się połączyć
class GPort{ // wspólna klasa dla GInput i GOutput
GBlock parent; // referencja na GBlock do którego należy
int index; // index w tablicy parent.inputs[]
enum SIDE {EAST, WEST, SOUTH, NORTH} side; // określa gdzie ma przychodzić kabel
Point position; // pozycja wejścia relatywnie do lewego górnego rogu bloku
Data data; // dodatkowe data jak label, description etc.
final bool isInput = true; // domyślnie jest wejściem
GPort(bool isInput, ...) // konstruktor ustawia typ wejścia, może mieć dodatkowe parametry
this.isInput = isInput;
void portButtonClicked(){ // po kliknięciu lub przytrzymaniu portu ma się dziać to
if(PPM) parent.parent.disconnect(this) // rozłącza wszystkie porty z this
if(LPM DRAGGED ...) { // musi sprawdzać czy łączone są IN-OUT a nie np. IN-IN
if(target.isInput()) GCircuit.disconnect(this, target) // rozłącza kable z inputa targetu
if(this.isInput()) GCircuit.disconncet(this, target) // rozłącza kable z tego input
parent.connect(this, second); // łączy z tym gdzie puszczono - `second`
}
}
bool isInput(); // jeśli port jest wejściem, zwraca true
}
Bramki TOP
Gate TOP
Gate
jest abstrakcyjną klasą bramki, po której dziedziczą specjalistyczne bramki. Musi mieć referencję na Circuit
do którego należy, pole ID
identyfikujące bramkę, kontenery zawierające wejścia i wyjścia, gdzie wejścia są jako tuple <bool, ID, port_index>
, a wyjścia jako tuple <bool, <ID,port_index>[] >
(tablica par booli i tablic połączonych portów) i abstrakcyjną funkcję update()
do liczenia outputów na podstawie swoich wejść, którą każda dziedzicząca klasa ma implementować.
- Do każdego wejścia może wchodzić tylko jeden kabel, dlatego każdy element tablicy inputs jest jednoznacznie wyznaczony przez swój stan i element z którym się łączy.
- Z jednego wyjścia może wychodzić wiele kabli, dlatego wyjścia reprezentowane są jako tablica par stanów i list wszystkich bloków i portów z którymi się łączy się dane wyjście
Trzeba umieć aktualizować dane na portach (stany i połączenia), dlatego potrzebne są funkcje get/setPortState(index, ...)
, get/setInputConnection(index, ...)
, add/removeOutputConnection(ID, port)
, getOutputConnections(ID, port)
. Dodatkowo powinna potrafić usunąć wszystkie aktualne połączenia metodą resetConnections()
.
Musi implementować funkcję clickAction()
- domyślnie pustą, ale nie abstrakcyjną (dopiero klasy pochodne jak chcą, to ją przeciążają i implementują jej ciało). Będzie ona wywoływana jeśli nastąpi kliknięcie na blok (o ile nie było to kliknięcie w port). Dodatkowo może mieć potem dodane pole data
które przechowuje pewne dane (dla zegara częstotliwość i fazę etc.).
class Gate{
Circuit parent; // wskaźnik na swój kontener
int ID; // indywidualne dla każdego bloku
Tuple<bool, int, int> inputs[]; // <stan, ID, port_index>
Tuple<bool, Tuple<int, int>[]> outputs[]; <stan, <ID,port_index>[] >
Data data; // dodatkowe info dla bramki
Gate(ID) // konstruktor
this.ID = ID;
bool getInput/OutputState(index) return inputs[index].tupleGet(0); // zwraca stan portu
void setInput/OutputState(index,bool state) inputs[index].tupleSet(0, index); // zmienia stan portu
Tuple<int, int> getInputConnection(index); // zwraca połączenie pod inputs[index]
void setInputConnection(index, int, int); // ... ustawia tuplowi połączenie
void addOutputConnection(index, ID, p_ix); // dodaje połączenie portu index z blokiem ID w porcie p_ix
void removeOutputConnection(ID, p_ix); // wyszukuje połączenie w outputs i usuwa je
Connection getOutputConnections(p_ix); // zwraca tablicę par <ID, port> z którymi jest połączony blok w p_ix
resetConnections(); // usuwa wszystkie połączenia wchodzące i wychodzące na wszystkich portach
toString(); // zwraca tekstową reprezentację bramki
}
And, Or, Xor, Not, Nand, Nor, Xnor TOP
Wszystkie bramki podstawowe mają praktycznie taką samą strukturę - przy konstruktorze tworzy im się tablicę wejść i wyjść o odpowiednich rozmiarach, ich metoda clickAction()
pozostaje nieprzeciążona, a update()
zwyczajnie aktualizuje stan wyjścia według swojej logiki. Wszystkie bramki są intuicyjne
class And{
And() // konstruktor
super(App.nextID());
update()
setOutputState(0, getInputState(0) && getInputState(1)); // ustawia output C na (A & B)
// clickAction() nie przeciążamy, więc wywołuje domyślnie puste clickAction z `Gate`
}
Clock TOP
Clock
(0 in, 1 out) jest rozszerzeniem zwykłej bramki o funkcję stanu w czasie. Ma mieć pola
onPeriod
: przez ile ticków zegar jest włączony
offPeriod
: przez ile ticków zegar jest wyłączony
phase
: przesunięcie fazowe - jakbyśmy narysowali wykres sygnału w czasie to dodatnie przesuwa go w prawo
Prosto można zaimplementować mając wewnątrz jakiś licznik, który zlicza "cyknięcia" zegara
Delay TOP
Delay
(1 in, 1 out) ma działać jako opóźniacz sygnału który wysyła sygnał z pewnym zdefiniowanym opóźnieniem. Powiniem mieć pole delay
≥ 0 oznaczające po ilu tickach dany sygnał zotanie przesłany - jeśli jest zerem, sygnał leci bez opóźnienia (jak zwykły kabel)
Trzeba zaimplementować tak, żeby dokładnie odtwarzał sygnał wejściowy na wyjściu po odpowiedniej ilości ticków (czyli jeśli mamy od chwili t0 kolejno sygnały 1,0,1 i opóźnienie 5 to w chwili t5=1 t6=0, t7=1).
Input TOP
Input
(0 in, 1 out) ma działać jak źródło prądu którego stan zmieniamy przy kliknięciu na bramkę. Może mieć flagę ON
oznaczającą stan i zmienianą w metodzi clickAction()
. Jeśli jest włączony metoda update()
ma ustawiać wyjście na 1, w przeciwnym wypadku 0 (no logiczne raczej).
Switch TOP
Switch
(1 input, 1 output) ma działać analogicznie do inputa, tylko że przekazuje stan wejścia na wyjście jeśli jest włączony i 0 jak jest wyłaczony. Ma działać jak kontakt :D
Własne bloki TOP
Jak już dojdziemy do tego etapu, że cała reszta działa (pozdro xD) można zaimplementować dodawania własnych bloków. Wtedy customowe bloki będą dodawane jako kafelek 6 x max(inputs,outputs) z nazwą na środku i wejściami jedno pod drugim z lewej i wyjściami jedno pod drugim z prawej. innymi słowy trzeba będzie zmienić trochę rzeczy, bo blok będzie musiał być wyświetlany jako pojedynczy, ale zachowywać się wewnątrz jako zwykła część układu - czyli pewnie trzeba będzie dla każdego GInput i GOutput przechowywać nie referencję na ojca ale ID ojca etc. etc.