Skip to content
This repository has been archived by the owner on Aug 30, 2024. It is now read-only.

Commit

Permalink
feat: design and implement input/output interfaces
Browse files Browse the repository at this point in the history
- Designed user-friendly input and output interfaces to facilitate interaction with the system.
- Implemented input handling mechanisms to ensure accurate and efficient data entry.
- Ensured compatibility of the interfaces with various data structures and algorithms.
- Validated the functionality of the interfaces through comprehensive unit tests.
- Documented the user interaction flows and interface specifications.
  • Loading branch information
KokomiSensei committed Mar 29, 2024
1 parent 4dc4eff commit 39a9da0
Show file tree
Hide file tree
Showing 4 changed files with 670 additions and 1 deletion.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,21 @@
├── test_epsilon_nfa.py # 测试 epsilon_nfa 数据结构的单元测试
└── test_nfa.py # 测试 nfa 数据结构的单元测试
```

## 使用说明

使用时请确保切换为英文输入模式。拖拽代表状态的圆圈,将它们放在屏幕上的任意位置。使用时按下空格键来输入 ε。

在图形化界面中单击按钮按下即生效,随时点击下方切换按钮,即可切换进对应功能;再次点击可以退出状态:

| 按钮 | 按钮类型 | 使用说明 |
| --- | --- | --- |
| New Delta | 切换 | 点击两个对应状态,再按下字符,添加转换关系 |
| Remove Delta | 切换 | 点击两个对应状态,再按下字符,移除指定转换关系 |
| Reassign Char | 切换 | 点击两个对应状态,按下字符 `a`,再按下字符 `b` 即可完成修改 |
| Rename State | 切换 | 点击状态,输入新名字(数字),再按回车,将对应状态改名 |
| Set as Start | 切换 | 点击状态,将其设为起始状态 |
| Set as Final | 切换 | 点击状态,将其设为终止状态 |
| Remove State | 切换 | 删除点击的状态 |
| New State | 单击 | 添加一个新状态 |
| Conver to DFA | 单击 | 将输入的自动机转化为最简 DFA 并输出。并顺便输出中途产生的 ε-NFA、NFA 和非最简 DFA |
261 changes: 261 additions & 0 deletions src/appModel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
import pygame
from pygame.locals import *
import math
from enum import Enum
from pygame import gfxdraw


class AppState(Enum):
idle = 0
pairing = 1
depairing = 2
renamingLabels = 3
removing = 4
setAsStart = 5
setAsEnd = 6
renamingStates = 7


class AppModel:
def __init__(self) -> None:
self.state = AppState.idle
self.selected_tiles: list[Tile] = []
self.pressedKeys = []

def setState(self, state: int):
self.state = state
self.selected_tiles.clear()
self.pressedKeys.clear()


class TileType(Enum):
start = 0
final = 1
intermediate = 2
start_final = 3


class Tile(pygame.sprite.Sprite):
def __init__(self, position, id):
super().__init__()
self.name = "q" + str(id)
self.rect = pygame.Rect(position[0], position[1], 100, 100)
self.rect.topleft = position
self.clicked = False
self.id = id
self.type = TileType.intermediate

def rename(self, id):
self.id = id
self.name = "q" + str(id)

def isClicked(self, event):
if event.type == MOUSEBUTTONDOWN:
if self.rect.collidepoint(event.pos):
distance = math.sqrt(
(event.pos[0] - self.rect.centerx) ** 2
+ (event.pos[1] - self.rect.centery) ** 2
)
if distance <= 50:
return True

def update(self, event, appModel: AppModel):
if self.isClicked(event):
self.clicked = True
if appModel.state != AppState.idle:
appModel.selected_tiles.append(self)
return True
elif event.type == MOUSEBUTTONUP:
self.clicked = False
elif event.type == MOUSEMOTION:
if self.clicked:
self.rect.move_ip(event.rel)
return False

def draw(self, screen):
backgroundColor = (125, 125, 125)

gfxdraw.aacircle(
screen,
self.rect.centerx,
self.rect.centery,
50,
(0, 0, 0),
)

if self.clicked:
gfxdraw.filled_circle(
screen,
self.rect.centerx,
self.rect.centery,
50,
(0, 0, 255),
)
gfxdraw.aacircle(
screen,
self.rect.centerx,
self.rect.centery,
50,
(0, 0, 255),
)
gfxdraw.filled_circle(
screen,
self.rect.centerx,
self.rect.centery,
45,
(255, 255, 255),
)
gfxdraw.aacircle(
screen,
self.rect.centerx,
self.rect.centery,
45,
(255, 255, 255),
)

font = pygame.font.Font(None, 36)
text = font.render(self.name, True, (0, 0, 0))
text_rect = text.get_rect(center=self.rect.center)
screen.blit(text, text_rect)
if self.type == TileType.final or self.type == TileType.start_final:
pygame.draw.circle(screen, "black", self.rect.center, 40, 5)
if self.type == TileType.start or self.type == TileType.start_final:
arrow_x = self.rect.left
arrow_y = self.rect.centery
arrow_x1 = arrow_x + 30 * math.cos(0.9 * math.pi)
arrow_y1 = arrow_y + 30 * math.sin(0.9 * math.pi)
arrow_x2 = arrow_x + 30 * math.cos(1.1 * math.pi)
arrow_y2 = arrow_y + 30 * math.sin(1.1 * math.pi)
gfxdraw.filled_polygon(
screen,
[(arrow_x, arrow_y), (arrow_x1, arrow_y1), (arrow_x2, arrow_y2)],
(0, 0, 0),
)
gfxdraw.aapolygon(
screen,
[(arrow_x, arrow_y), (arrow_x1, arrow_y1), (arrow_x2, arrow_y2)],
(0, 0, 0),
)
pygame.draw.aaline(
screen, "black", (arrow_x, arrow_y), (arrow_x - 100, arrow_y), 5
)

def __eq__(self, other):
if isinstance(other, Tile):
return self.id == other.id
return False


class Edge:
def __init__(self, fromTile: Tile, toTile: Tile, label=""):
self.fromTile = fromTile
self.toTile = toTile
self.labels: set[str] = {label}

def addLabel(self, label):
self.labels.add(label)

def renameLabel(self, old, new):
if old in self.labels:
self.labels.remove(old)
self.labels.add(new)

def isEmpty(self):
return len(self.labels) == 0

def removeLabel(self, label):
if label in self.labels:
self.labels.remove(label)

def __eq__(self, other):
if isinstance(other, Edge):
return self.fromTile == other.fromTile and self.toTile == other.toTile
return False

def draw(self, screen):
arrow_length = 30
radius = self.fromTile.rect.width // 2
arrow_angle = math.pi / 6

displayedStr = ""
for label in self.labels:
displayedStr += (label if label != "" else "ε") + ", "
displayedStr = displayedStr[:-2]

if self.fromTile == self.toTile:
offset = self.fromTile.rect.width
rect = pygame.Rect(
self.fromTile.rect.centerx,
self.fromTile.rect.centery - offset,
offset,
offset,
)
pygame.draw.arc(screen, (255, 0, 0), rect, -0.5 * math.pi, math.pi, 2)
angle = 0.9 * math.pi
arrow_x = self.fromTile.rect.centerx + radius
arrow_y = self.fromTile.rect.centery
arrow_x1 = arrow_x - arrow_length * math.cos(angle + arrow_angle / 2)
arrow_y1 = arrow_y - arrow_length * math.sin(angle + arrow_angle / 2)
arrow_x2 = arrow_x - arrow_length * math.cos(angle - arrow_angle / 2)
arrow_y2 = arrow_y - arrow_length * math.sin(angle - arrow_angle / 2)
font = pygame.font.Font(None, 50)
text = font.render(displayedStr, True, (0, 0, 0))
text_rect = text.get_rect(center=rect.topright)
screen.blit(text, text_rect)
gfxdraw.filled_polygon(
screen,
[(arrow_x, arrow_y), (arrow_x1, arrow_y1), (arrow_x2, arrow_y2)],
(255, 0, 0),
)
gfxdraw.aapolygon(
screen,
[(arrow_x, arrow_y), (arrow_x1, arrow_y1), (arrow_x2, arrow_y2)],
(255, 0, 0),
)
else:
start_x, start_y = self.fromTile.rect.centerx, self.fromTile.rect.centery
end_x, end_y = self.toTile.rect.centerx, self.toTile.rect.centery
angle = math.atan2(end_y - start_y, end_x - start_x)
tailDeviateAngle = math.pi * 0.1
tailArcHeight = 20
tail_x, tail_y = start_x + radius * math.cos(
angle + tailDeviateAngle
), start_y + radius * math.sin(angle + tailDeviateAngle)
head_x, head_y = end_x + radius * math.cos(
angle - tailDeviateAngle + math.pi
), end_y + radius * math.sin(angle - tailDeviateAngle + math.pi)
distance = math.sqrt((tail_x - head_x) ** 2 + (tail_y - head_y) ** 2)
mid_x, mid_y = (tail_x + head_x) / 2, (tail_y + head_y) / 2
angle_offset = math.atan(tailArcHeight * 2 / distance)
r = distance / (2 * math.sin(2 * angle_offset))
center_x, center_y = mid_x + (r - tailArcHeight) * math.sin(
angle
), mid_y - (r - tailArcHeight) * math.cos(angle)
from_angle, to_angle = (
math.pi * 0.5 + angle - 2 * angle_offset,
math.pi * 0.5 + angle + 2 * angle_offset,
)
arcRect = pygame.Rect(center_x - r, center_y - r, 2 * r, 2 * r)
pygame.draw.arc(screen, (255, 0, 0), arcRect, -to_angle, -from_angle, 2)
finalAngle = angle - tailDeviateAngle
arrow_x1 = head_x - arrow_length * math.cos(finalAngle + arrow_angle / 2)
arrow_y1 = head_y - arrow_length * math.sin(finalAngle + arrow_angle / 2)
arrow_x2 = head_x - arrow_length * math.cos(finalAngle - arrow_angle / 2)
arrow_y2 = head_y - arrow_length * math.sin(finalAngle - arrow_angle / 2)
gfxdraw.filled_polygon(
screen,
[(head_x, head_y), (arrow_x1, arrow_y1), (arrow_x2, arrow_y2)],
(255, 0, 0),
)
gfxdraw.aapolygon(
screen,
[(head_x, head_y), (arrow_x1, arrow_y1), (arrow_x2, arrow_y2)],
(255, 0, 0),
)
font = pygame.font.Font(None, 50)
text = font.render(displayedStr, True, (0, 0, 0))
yOffset = 20 if self.fromTile.id < self.toTile.id else -20
text_rect = text.get_rect(
center=((tail_x + head_x) // 2, (tail_y + head_y) // 2 + yOffset)
)
screen.blit(text, text_rect)
66 changes: 66 additions & 0 deletions src/buttons.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import pygame
from pygame.locals import *
from appModel import AppState, AppModel


class Button(pygame.Rect):
def __init__(self, left, top, width, height, text, color):
super().__init__(left, top, width, height)
self.color = color
self.text = text
self.corner_radius = 5

def isClicked(self, event):
if event.type == pygame.MOUSEBUTTONDOWN:
if self.collidepoint(event.pos):
return True

def draw(self, screen):
pygame.draw.rect(screen, self.color, self, border_radius=self.corner_radius)
font = pygame.font.Font(None, 36)
text = font.render(self.text, True, (255, 255, 255))
text_rect = text.get_rect(center=self.center)
screen.blit(text, text_rect)


class ToggleButton(Button):
def __init__(self, left, top, width, height, text, color, toState):
super().__init__(left, top, width, height, text, color)
self.selfState = toState

def isClicked(self, event):
if event.type == MOUSEBUTTONDOWN:
if self.collidepoint(event.pos):
return True
return False

def update(self, event, appModel: AppModel):
if self.isClicked(event):
if appModel.state != self.selfState:
appModel.setState(self.selfState)
else:
appModel.setState(AppState.idle)

def setState(self, pressed: bool):
self.pressedDown = pressed

def draw(self, screen, appModel: AppModel):
r = self.corner_radius
brightnessIncrement = 100
if appModel.state == self.selfState:
pygame.draw.rect(
screen,
(
self.color[0] + brightnessIncrement,
self.color[1] + brightnessIncrement,
self.color[2] + brightnessIncrement,
),
self,
border_radius=r,
)
else:
pygame.draw.rect(screen, self.color, self, border_radius=r)
font = pygame.font.Font(None, 36)
text = font.render(self.text, True, (255, 255, 255))
text_rect = text.get_rect(center=self.center)
screen.blit(text, text_rect)
Loading

0 comments on commit 39a9da0

Please sign in to comment.