added zoom levels, reload, home, cache TTL

This commit is contained in:
Louis Heredero 2024-07-05 22:23:03 +02:00
parent a4249899e9
commit 9a3bbc95d9
Signed by: HEL
GPG Key ID: 8D83DE470F8544E7
3 changed files with 55 additions and 22 deletions

View File

@ -5,6 +5,7 @@ import os.path
class Config: class Config:
LAST_OPENED_FILE = "" LAST_OPENED_FILE = ""
AUTOSAVE_INTERVAL = 5 * 60 * 1000 AUTOSAVE_INTERVAL = 5 * 60 * 1000
CACHE_TTL = 10
def __init__(self, path: str): def __init__(self, path: str):
self._path: str = path self._path: str = path

View File

@ -3,12 +3,12 @@ from enum import Enum, auto
from math import floor from math import floor
from typing import Optional from typing import Optional
import platformdirs
import pygame import pygame
from src.config import Config from src.config import Config
from src.image_handler import ImageHandler
from src.graph.graph import Graph from src.graph.graph import Graph
from src.image_handler import ImageHandler
from src.utils.paths import CONFIG_DIR, CACHE_DIR
class Editor: class Editor:
@ -17,13 +17,11 @@ class Editor:
WIDTH: int = 800 WIDTH: int = 800
HEIGHT: int = 600 HEIGHT: int = 600
MAP_SIZE: int = 1024 MAP_SIZE: int = 1024
CACHE_DIR: str = platformdirs.user_cache_dir(appname=APP_NAME, appauthor=APP_AUTHOR, ensure_exists=True)
CONFIG_DIR: str = platformdirs.user_config_dir(appname=APP_NAME, appauthor=APP_AUTHOR, ensure_exists=True)
CONFIG_PATH: str = os.path.join(CONFIG_DIR, "config.json") CONFIG_PATH: str = os.path.join(CONFIG_DIR, "config.json")
MAPS_DIR: str = os.path.join(CACHE_DIR, "maps") MAPS_DIR: str = os.path.join(CACHE_DIR, "maps")
AUTOSAVE_PATH: str = os.path.join(CACHE_DIR, "AUTOSAVE.txt") AUTOSAVE_PATH: str = os.path.join(CACHE_DIR, "AUTOSAVE.txt")
AUTOSAVE_EVENT: int = pygame.event.custom_type() AUTOSAVE_EVENT: int = pygame.event.custom_type()
ZOOMS: tuple[float] = (0.25, 0.5, 1, 2, 4) ZOOMS: tuple[float] = tuple(2**p for p in range(-6, 7))
CROSSHAIR_SIZE: int = 10 CROSSHAIR_SIZE: int = 10
def __init__(self): def __init__(self):
@ -34,19 +32,21 @@ class Editor:
self.win: pygame.Surface = pygame.display.set_mode([self.width, self.height], pygame.RESIZABLE) self.win: pygame.Surface = pygame.display.set_mode([self.width, self.height], pygame.RESIZABLE)
pygame.display.set_caption("Lycacraft Map Editor") pygame.display.set_caption("Lycacraft Map Editor")
self.center: list[int] = [0, 0] self.center: list[int] = [0, 0]
self.zoom_i: int = 2 self.zoom_i: int = self.ZOOMS.index(1)
self.zoom: float = self.ZOOMS[self.zoom_i] self.zoom: float = self.ZOOMS[self.zoom_i]
self.running: bool = False self.running: bool = False
self.image_handler: ImageHandler = ImageHandler(self.MAPS_DIR, self.MAP_SIZE) self.image_handler: ImageHandler = ImageHandler(self.MAPS_DIR, self.MAP_SIZE, self.config.CACHE_TTL)
self.clock: pygame.time.Clock = pygame.time.Clock() self.clock: pygame.time.Clock = pygame.time.Clock()
self.left_drag_pos: Optional[tuple[int, int]] = None self.left_drag_pos: Optional[tuple[int, int]] = None
self.mid_drag_pos: Optional[tuple[int, int]] = None self.mid_drag_pos: Optional[tuple[int, int]] = None
self.font: pygame.font.Font = pygame.font.SysFont("Ubuntu", 20) self.font: pygame.font.Font = pygame.font.SysFont("Ubuntu", 20)
self.loading_font: pygame.font.Font = pygame.font.SysFont("Ubuntu", 30) self.loading_font: pygame.font.Font = pygame.font.SysFont("Ubuntu", 30)
self.zooms_texts: list[pygame.Surface] = list(map( self.zooms_texts: list[pygame.Surface] = []
lambda z: self.font.render(str(z), True, (255, 255, 255)), for zoom in self.ZOOMS:
self.ZOOMS txt = str(zoom)
)) if zoom < 1:
txt = f"1/{int(1/zoom):d}"
self.zooms_texts.append(self.font.render(txt, True, (255, 255, 255)))
self.is_renaming_node: bool = False self.is_renaming_node: bool = False
self.state: State = State.STOPPING self.state: State = State.STOPPING
self.graph = Graph() self.graph = Graph()
@ -61,6 +61,7 @@ class Editor:
self.original_move_pos: Optional[tuple[int, int]] = None self.original_move_pos: Optional[tuple[int, int]] = None
self.move_old_poses: Optional[dict[int, tuple[int, int]]] = None self.move_old_poses: Optional[dict[int, tuple[int, int]]] = None
self.dirty: bool = False self.dirty: bool = False
self.loading_bg: pygame.Surface = pygame.Surface([self.width, self.height])
pygame.time.set_timer(self.AUTOSAVE_EVENT, self.config.AUTOSAVE_INTERVAL) pygame.time.set_timer(self.AUTOSAVE_EVENT, self.config.AUTOSAVE_INTERVAL)
if os.path.exists(self.AUTOSAVE_PATH): if os.path.exists(self.AUTOSAVE_PATH):
@ -72,7 +73,7 @@ class Editor:
def mainloop(self) -> None: def mainloop(self) -> None:
self.state = State.LOADING self.state = State.LOADING
while self.state != State.STOPPING: while self.state != State.STOPPING:
caption = f"Lycacraft Map Editor - {self.clock.get_fps():.2f}fps" caption = f"Lycacraft Map Editor - {self.clock.get_fps():.2f}fps - {self.image_handler.size} images"
if self.dirty: if self.dirty:
caption += " (unsaved)" caption += " (unsaved)"
pygame.display.set_caption(caption) pygame.display.set_caption(caption)
@ -87,6 +88,7 @@ class Editor:
if self.original_move_pos is not None: if self.original_move_pos is not None:
self.move_poses() self.move_poses()
self.render() self.render()
self.image_handler.clean()
self.clock.tick(30) self.clock.tick(30)
def quit(self) -> None: def quit(self) -> None:
@ -138,6 +140,10 @@ class Editor:
if len(self.selected_nodes) == 1: if len(self.selected_nodes) == 1:
self.typing_text = self.graph.nodes[self.selected_nodes[0]].name self.typing_text = self.graph.nodes[self.selected_nodes[0]].name
self.is_renaming_node = True self.is_renaming_node = True
elif event.key == pygame.K_HOME:
self.center = [0, 0]
elif event.key == pygame.K_F5:
self.reload()
elif event.type == pygame.KEYUP: elif event.type == pygame.KEYUP:
if event.key == pygame.K_m: if event.key == pygame.K_m:
if self.original_move_pos is not None: if self.original_move_pos is not None:
@ -198,7 +204,7 @@ class Editor:
self.mid_drag_pos = mpos self.mid_drag_pos = mpos
def render(self) -> None: def render(self) -> None:
self.win.fill((0, 0, 0)) self.win.fill((50, 50, 50))
off_x = (self.center[0] * self.zoom) % self.MAP_SIZE off_x = (self.center[0] * self.zoom) % self.MAP_SIZE
off_y = (self.center[1] * self.zoom) % self.MAP_SIZE off_y = (self.center[1] * self.zoom) % self.MAP_SIZE
@ -254,13 +260,13 @@ class Editor:
pygame.display.flip() pygame.display.flip()
def render_zoom_slider(self) -> None: def render_zoom_slider(self) -> None:
zoom_height = self.height * 0.2 zoom_r = self.height / 80
zoom_space = zoom_r * 4
zoom_height = zoom_space * (len(self.ZOOMS) - 1)
zoom_h_margin = self.width * 0.02 zoom_h_margin = self.width * 0.02
zoom_v_margin = self.height * 0.05 zoom_v_margin = self.height * 0.05
zoom_x = self.width - zoom_h_margin zoom_x = self.width - zoom_h_margin
zoom_y = self.height - zoom_v_margin - zoom_height zoom_y = self.height - zoom_v_margin - zoom_height
zoom_space = zoom_height / 4
zoom_r = zoom_space / 4
zoom_width = max(s.get_width() for s in self.zooms_texts) + 2 * zoom_r + 5 zoom_width = max(s.get_width() for s in self.zooms_texts) + 2 * zoom_r + 5
pygame.draw.rect(self.win, (80, 80, 80), [ pygame.draw.rect(self.win, (80, 80, 80), [
zoom_x + zoom_r - zoom_width - 5, zoom_x + zoom_r - zoom_width - 5,
@ -277,6 +283,7 @@ class Editor:
def render_loading(self) -> None: def render_loading(self) -> None:
self.win.fill((0, 0, 0)) self.win.fill((0, 0, 0))
self.win.blit(self.loading_bg, [0, 0])
count = self.image_handler.count count = self.image_handler.count
total = self.image_handler.total total = self.image_handler.total
txt = self.loading_font.render(f"Loading maps - {count}/{total}", True, (255, 255, 255)) txt = self.loading_font.render(f"Loading maps - {count}/{total}", True, (255, 255, 255))
@ -642,11 +649,18 @@ class Editor:
if len(path.strip()) == 0: if len(path.strip()) == 0:
path = last_path path = last_path
self.graph = Graph.load(path) if os.path.exists(path):
if save_config: self.graph = Graph.load(path)
self.config.LAST_OPENED_FILE = path if save_config:
self.config.save() self.config.LAST_OPENED_FILE = path
self.dirty = False self.config.save()
self.dirty = False
def reload(self) -> None:
self.state = State.LOADING
self.loading_bg = self.win.copy()
del self.image_handler
self.image_handler = ImageHandler(self.MAPS_DIR, self.MAP_SIZE, self.config.CACHE_TTL)
class State(Enum): class State(Enum):

View File

@ -1,5 +1,6 @@
import os import os
import threading import threading
import time
from math import floor from math import floor
from typing import Optional from typing import Optional
@ -7,10 +8,13 @@ import pygame
class ImageHandler: class ImageHandler:
def __init__(self, maps_dir: str, base_size: int): def __init__(self, maps_dir: str, base_size: int, ttl: int):
self.maps_dir: str = maps_dir self.maps_dir: str = maps_dir
self.base_size: int = base_size self.base_size: int = base_size
self.ttl: int = ttl
self.cache: dict = {} self.cache: dict = {}
self.history: dict[tuple[float, tuple[int, int]], float] = {}
self.size: int = 0
self.count: int = 0 self.count: int = 0
self.total: int = 0 self.total: int = 0
self.loading: bool = False self.loading: bool = False
@ -29,6 +33,7 @@ class ImageHandler:
name, x, y = path.split(".")[0].split("_") name, x, y = path.split(".")[0].split("_")
cache[(int(x), int(y))] = pygame.image.load(fullpath).convert_alpha() cache[(int(x), int(y))] = pygame.image.load(fullpath).convert_alpha()
self.count += 1 self.count += 1
self.size += 1
self.cache = { self.cache = {
1: cache 1: cache
@ -65,6 +70,19 @@ class ImageHandler:
img = pygame.transform.scale_by(img, zoom) img = pygame.transform.scale_by(img, zoom)
cache[pos] = img cache[pos] = img
self.size += 1
self.cache[zoom] = cache self.cache[zoom] = cache
self.history[(zoom, pos)] = time.time()
return cache[pos] return cache[pos]
def clean(self) -> None:
t = time.time()
new_history = {}
for (zoom, pos), t0 in self.history.items():
if zoom != 1 and t0 + self.ttl < t:
del self.cache[zoom][pos]
self.size -= 1
else:
new_history[(zoom, pos)] = t0
self.history = new_history