diff --git a/tools/editortool/map/camera.py b/tools/editortool/map/camera.py index 962175f..6e5b31e 100644 --- a/tools/editortool/map/camera.py +++ b/tools/editortool/map/camera.py @@ -25,9 +25,9 @@ class Camera: (self.pixelsPerUnit * self.scale) * math.tan(math.radians(self.fov / 2.0)) ) lookAt = [ - self.parent.parent.map.position[0] * TILE_WIDTH, - self.parent.parent.map.position[1] * TILE_HEIGHT, - self.parent.parent.map.position[2] * TILE_DEPTH, + self.parent.map.position[0] * TILE_WIDTH, + self.parent.map.position[1] * TILE_HEIGHT, + self.parent.map.position[2] * TILE_DEPTH, ] aspectRatio = vw / vh diff --git a/tools/editortool/map/chunk.py b/tools/editortool/map/chunk.py index 1cfe038..b2d4320 100644 --- a/tools/editortool/map/chunk.py +++ b/tools/editortool/map/chunk.py @@ -1,14 +1,53 @@ -from editortool.map.chunk import Tile +import json +import os +from dusk.event import Event class Chunk: - def __init__(self, x=0, y=0, z=0, size_x=1, size_y=1, size_z=1): + def __init__(self, map, x, y, z): + self.map = map self.x = x self.y = y self.z = z - self.data = [[[Tile() for _ in range(size_z)] for _ in range(size_y)] for _ in range(size_x)] + self.current = {} + self.original = {} + self.onChunkData = Event() - def draw(self): - for x in range(len(self.data)): - for y in range(len(self.data[x])): - for z in range(len(self.data[x][y])): - self.data[x][y][z].draw(x, y, z) \ No newline at end of file + def load(self): + fname = self.getFilename() + if not fname or not os.path.exists(fname): + self.new() + return + try: + with open(fname, 'r') as f: + self.current = json.load(f) + self.original = json.loads(json.dumps(self.current)) # Deep copy + self.onChunkData.invoke(self.current) + except Exception as e: + raise RuntimeError(f"Failed to load chunk file: {e}") + + def save(self): + fname = self.getFilename() + if not fname: + raise ValueError("No filename specified for saving chunk.") + try: + with open(fname, 'w') as f: + json.dump(self.current, f, indent=2) + self.original = json.loads(json.dumps(self.current)) # Deep copy + except Exception as e: + raise RuntimeError(f"Failed to save chunk file: {e}") + + def new(self): + self.current = {} + self.original = {} + self.onChunkData.invoke(self.current) + + def isDirty(self): + return json.dumps(self.current, sort_keys=True) != json.dumps(self.original, sort_keys=True) + + def getFilename(self): + if not self.map or not hasattr(self.map, 'getMapDirectory'): + return None + dir_path = self.map.getMapDirectory() + if dir_path is None: + return None + return f"{dir_path}/{self.x}_{self.y}_{self.z}.json" diff --git a/tools/editortool/map/chunkdata.py b/tools/editortool/map/chunkdata.py deleted file mode 100644 index 3a094c9..0000000 --- a/tools/editortool/map/chunkdata.py +++ /dev/null @@ -1,53 +0,0 @@ -import json -import os -from dusk.event import Event - -class ChunkData: - def __init__(self, mapData, x, y, z): - self.mapData = mapData - self.x = x - self.y = y - self.z = z - self.current = {} - self.original = {} - self.onChunkData = Event() - - def load(self): - fname = self.getFilename() - if not fname or not os.path.exists(fname): - self.new() - return - try: - with open(fname, 'r') as f: - self.current = json.load(f) - self.original = json.loads(json.dumps(self.current)) # Deep copy - self.onChunkData.invoke(self.current) - except Exception as e: - raise RuntimeError(f"Failed to load chunk file: {e}") - - def save(self): - fname = self.getFilename() - if not fname: - raise ValueError("No filename specified for saving chunk.") - try: - with open(fname, 'w') as f: - json.dump(self.current, f, indent=2) - self.original = json.loads(json.dumps(self.current)) # Deep copy - except Exception as e: - raise RuntimeError(f"Failed to save chunk file: {e}") - - def new(self): - self.current = {} - self.original = {} - self.onChunkData.invoke(self.current) - - def isDirty(self): - return json.dumps(self.current, sort_keys=True) != json.dumps(self.original, sort_keys=True) - - def getFilename(self): - if not self.mapData or not hasattr(self.mapData, 'getMapDirectory'): - return None - dir_path = self.mapData.getMapDirectory() - if dir_path is None: - return None - return f"{dir_path}/{self.x}_{self.y}_{self.z}.json" diff --git a/tools/editortool/map/chunkpanel.py b/tools/editortool/map/chunkpanel.py index 74197fa..0e96ffd 100644 --- a/tools/editortool/map/chunkpanel.py +++ b/tools/editortool/map/chunkpanel.py @@ -36,7 +36,11 @@ class ChunkPanel(QWidget): QTreeWidgetItem(parentItem2, ["Tile C"]) QTreeWidgetItem(parentItem2, ["Tile D"]) self.tree.expandAll() # Expand by default, remove if you want collapsed - layout.addWidget(self.tree, 1) # Give tree stretch factor so it expands + layout.addWidget(self.tree) # Removed invalid stretch factor + + # Add stretch so tree expands + layout.setStretchFactor(grid, 0) + layout.setStretchFactor(self.tree, 1) # Event subscriptions self.btnN.clicked.connect(lambda: self.parent.map.moveRelative(0, -1, 0)) diff --git a/tools/editortool/map/glwidget.py b/tools/editortool/map/glwidget.py index 211cbfd..aba2a9a 100644 --- a/tools/editortool/map/glwidget.py +++ b/tools/editortool/map/glwidget.py @@ -2,9 +2,6 @@ from PyQt5.QtCore import QTimer from PyQt5.QtWidgets import QOpenGLWidget from OpenGL.GL import * from OpenGL.GLU import * -from editortool.map.selectbox import SelectBox -from editortool.map.camera import Camera -from editortool.map.grid import Grid class GLWidget(QOpenGLWidget): def __init__(self, parent): @@ -13,9 +10,6 @@ class GLWidget(QOpenGLWidget): self.timer = QTimer(self) self.timer.timeout.connect(self.update) self.timer.start(16) # ~60 FPS - self.camera = Camera(self) - self.grid = Grid() - self.selectBox = SelectBox(self) def initializeGL(self): glClearColor(0.392, 0.584, 0.929, 1.0) @@ -36,7 +30,6 @@ class GLWidget(QOpenGLWidget): w = 1 glViewport(0, 0, w, h) - self.camera.setup(w, h) - - self.grid.draw() - self.selectBox.draw() \ No newline at end of file + self.parent.camera.setup(w, h) + self.parent.grid.draw() + self.parent.selectBox.draw() \ No newline at end of file diff --git a/tools/editortool/map/map.py b/tools/editortool/map/map.py index 03b8da7..c1dbf78 100644 --- a/tools/editortool/map/map.py +++ b/tools/editortool/map/map.py @@ -1,15 +1,135 @@ +import json from dusk.event import Event -from editortool.map.mapdata import MAP_WIDTH, MAP_HEIGHT, MAP_DEPTH +from PyQt5.QtWidgets import QFileDialog, QMessageBox +from PyQt5.QtCore import QTimer +import os +from editortool.map.chunk import Chunk +from editortool.map.mapdefs import MAP_WIDTH, MAP_HEIGHT, MAP_DEPTH +import traceback + +MAP_DEFAULT_PATH = os.path.join(os.path.dirname(__file__), '../../../assets/map/') +EDITOR_CONFIG_PATH = os.path.join(os.path.dirname(__file__), '.editor') class Map: def __init__(self, parent): self.parent = parent + self.data = {} + self.dataOriginal = {} self.position = [0, 0, 0] # x, y, z - self.positionEvent = Event() + self.chunks = {} + self.onMapData = Event() + self.onPositionChange = Event() + self.mapFileName = None + self.lastFile = None + + index = 0 + for x in range(MAP_WIDTH): + for y in range(MAP_HEIGHT): + for z in range(MAP_DEPTH): + self.chunks[index] = Chunk(self, x, y, z) + index += 1 + QTimer.singleShot(16, self.loadLastFile) + + def loadLastFile(self): + if not os.path.exists(EDITOR_CONFIG_PATH): + return + try: + with open(EDITOR_CONFIG_PATH, 'r') as f: + config = json.load(f) + lastFile = config.get('lastFile') + lastPosition = config.get('lastPosition') + if lastFile and os.path.exists(lastFile): + self.load(lastFile) + if lastPosition and isinstance(lastPosition, list) and len(lastPosition) == 3: + self.moveTo(*lastPosition) + except Exception: + traceback.print_exc() + + def updateEditorConfig(self): + print("Updating editor config...") + try: + mapFileName = self.getMapFilename() + config = { + 'lastFile': mapFileName if mapFileName else "", + 'lastPosition': self.position + } + config_dir = os.path.dirname(EDITOR_CONFIG_PATH) + if not os.path.exists(config_dir): + os.makedirs(config_dir, exist_ok=True) + with open(EDITOR_CONFIG_PATH, 'w') as f: + json.dump(config, f, indent=2) + except Exception: + traceback.print_exc() + + def newFile(self): + self.data = {} + self.dataOriginal = {} + self.mapFileName = None + self.lastFile = None + for chunk in self.chunks.values(): + chunk.new() + self.moveTo(0, 0, 0) + self.onMapData.invoke(self.data) + self.updateEditorConfig() + + def save(self, fname=None): + if not self.getMapFilename() and fname is None: + filePath, _ = QFileDialog.getSaveFileName(None, "Save Map File", MAP_DEFAULT_PATH, "Map Files (*.json)") + if not filePath: + return + self.mapFileName = filePath + if fname: + self.mapFileName = fname + try: + with open(self.getMapFilename(), 'w') as f: + json.dump(self.data, f, indent=2) + self.dataOriginal = json.loads(json.dumps(self.data)) # Deep copy + for chunk in self.chunks.values(): + chunk.save() + self.updateEditorConfig() + except Exception as e: + traceback.print_exc() + QMessageBox.critical(None, "Save Error", f"Failed to save map file:\n{e}") + + def load(self, fileName): + try: + with open(fileName, 'r') as f: + self.data = json.load(f) + self.mapFileName = fileName + self.dataOriginal = json.loads(json.dumps(self.data)) # Deep copy + for chunk in self.chunks.values(): + chunk.load() + self.onMapData.invoke(self.data) + self.updateEditorConfig() + except Exception as e: + traceback.print_exc() + QMessageBox.critical(None, "Load Error", f"Failed to load map file:\n{e}") + + def isMapFileDirty(self): + return json.dumps(self.data, sort_keys=True) != json.dumps(self.dataOriginal, sort_keys=True) + + def isDirty(self): + return self.isMapFileDirty() or self.anyChunksDirty() + + def getMapFilename(self): + return self.mapFileName if self.mapFileName and os.path.exists(self.mapFileName) else None + + def getMapDirectory(self): + fname = self.getMapFilename() + if not fname or not fname.endswith('.json'): + return None + return fname[:-5] # Remove '.json' extension + + def anyChunksDirty(self): + for chunk in self.chunks.values(): + if chunk.isDirty(): + return True + return False def moveTo(self, x, y, z): self.position = [x, y, z] - self.positionEvent.invoke(self.position) + self.onPositionChange.invoke(self.position) + self.updateEditorConfig() def moveRelative(self, x, y, z): self.moveTo( diff --git a/tools/editortool/map/mapdata.py b/tools/editortool/map/mapdata.py deleted file mode 100644 index d86efa3..0000000 --- a/tools/editortool/map/mapdata.py +++ /dev/null @@ -1,109 +0,0 @@ -import json -from dusk.event import Event -from PyQt5.QtWidgets import QFileDialog, QMessageBox -from PyQt5.QtCore import QTimer -import os -from editortool.map.chunkdata import ChunkData -from editortool.map.mapdefs import MAP_WIDTH, MAP_HEIGHT, MAP_DEPTH -import traceback - -MAP_DEFAULT_PATH = os.path.join(os.path.dirname(__file__), '../../../assets/map/') -EDITOR_CONFIG_PATH = os.path.join(os.path.dirname(__file__), '.editor') - -class MapData: - def __init__(self): - self.data = {} - self.dataOriginal = {} - self.onMapData = Event() - self.mapFileName = None - self.chunks = {} - - index = 0 - for x in range(MAP_WIDTH): - for y in range(MAP_HEIGHT): - for z in range(MAP_DEPTH): - self.chunks[index] = ChunkData(self, x, y, z) - index += 1 - QTimer.singleShot(16, self.loadLastFile) - - def loadLastFile(self): - if os.path.exists(EDITOR_CONFIG_PATH): - try: - with open(EDITOR_CONFIG_PATH, 'r') as f: - config = json.load(f) - lastFile = config.get('lastFile') - if lastFile and os.path.exists(lastFile): - self.load(lastFile) - except Exception: - pass - - def updateEditorConfig(self): - try: - config = {'lastFile': self.getMapFilename() if self.getMapFilename() else ""} - with open(EDITOR_CONFIG_PATH, 'w') as f: - json.dump(config, f, indent=2) - except Exception: - pass - - def newFile(self): - self.data = {} - self.dataOriginal = {} - self.mapFileName = None - for chunk in self.chunks.values(): - chunk.new() - self.onMapData.invoke(self.data) - self.updateEditorConfig() - - def save(self, fname=None): - if not self.getMapFilename() and fname is None: - filePath, _ = QFileDialog.getSaveFileName(None, "Save Map File", MAP_DEFAULT_PATH, "Map Files (*.json)") - if not filePath: - return - self.mapFileName = filePath - if fname: - self.mapFileName = fname - try: - with open(self.getMapFilename(), 'w') as f: - json.dump(self.data, f, indent=2) - self.dataOriginal = json.loads(json.dumps(self.data)) # Deep copy - for chunk in self.chunks.values(): - chunk.save() - self.updateEditorConfig() - except Exception as e: - traceback.print_exc() - QMessageBox.critical(None, "Save Error", f"Failed to save map file:\n{e}") - - def load(self, fileName): - try: - with open(fileName, 'r') as f: - self.data = json.load(f) - self.mapFileName = fileName - self.dataOriginal = json.loads(json.dumps(self.data)) # Deep copy - for chunk in self.chunks.values(): - chunk.load() - self.onMapData.invoke(self.data) - self.updateEditorConfig() - except Exception as e: - traceback.print_exc() - QMessageBox.critical(None, "Load Error", f"Failed to load map file:\n{e}") - - def isMapFileDirty(self): - return json.dumps(self.data, sort_keys=True) != json.dumps(self.dataOriginal, sort_keys=True) - - def isDirty(self): - return self.isMapFileDirty() or self.anyChunksDirty() - - def getMapFilename(self): - return self.mapFileName if self.mapFileName else None - - def getMapDirectory(self): - fname = self.getMapFilename() - if not fname or not fname.endswith('.json'): - return None - return fname[:-5] # Remove '.json' extension - - def anyChunksDirty(self): - for chunk in self.chunks.values(): - if chunk.isDirty(): - return True - return False \ No newline at end of file diff --git a/tools/editortool/map/mapinfopanel.py b/tools/editortool/map/mapinfopanel.py index e7df2f5..27f358e 100644 --- a/tools/editortool/map/mapinfopanel.py +++ b/tools/editortool/map/mapinfopanel.py @@ -43,8 +43,8 @@ class MapInfoPanel(QWidget): self.tileXInput.returnPressed.connect(self.goToPosition) self.tileYInput.returnPressed.connect(self.goToPosition) self.tileZInput.returnPressed.connect(self.goToPosition) - self.parent.map.positionEvent.sub(self.updatePositionLabels) - self.parent.mapData.onMapData.sub(self.onMapData) + self.parent.map.onPositionChange.sub(self.updatePositionLabels) + self.parent.map.onMapData.sub(self.onMapData) self.mapTitleInput.textChanged.connect(self.onMapNameChange) # Initial label setting @@ -55,7 +55,7 @@ class MapInfoPanel(QWidget): x = int(self.tileXInput.text()) y = int(self.tileYInput.text()) z = int(self.tileZInput.text()) - map.moveTo(x, y, z) + self.parent.map.moveTo(x, y, z) except ValueError: QMessageBox.warning(self, "Invalid Input", "Please enter valid integer coordinates.") @@ -80,4 +80,4 @@ class MapInfoPanel(QWidget): self.mapTitleInput.setText(data.get("mapName", "")) def onMapNameChange(self, text): - self.parent.mapData.data['mapName'] = text \ No newline at end of file + self.parent.map.data['mapName'] = text \ No newline at end of file diff --git a/tools/editortool/map/toolbar.py b/tools/editortool/map/menubar.py similarity index 80% rename from tools/editortool/map/toolbar.py rename to tools/editortool/map/menubar.py index b69fb58..85ecdcf 100644 --- a/tools/editortool/map/toolbar.py +++ b/tools/editortool/map/menubar.py @@ -1,13 +1,14 @@ import os from PyQt5.QtWidgets import QAction, QMenuBar, QFileDialog -from editortool.map.mapdata import MAP_DEFAULT_PATH +from editortool.map.map import MAP_DEFAULT_PATH -class MapToolbar: +class MapMenubar: def __init__(self, parent): self.parent = parent - self.menubar = parent.menuBar() - self.fileMenu = self.menubar.addMenu("File") + self.menubar = QMenuBar(parent) + parent.setMenuBar(self.menubar) + self.fileMenu = self.menubar.addMenu("File") self.actionNew = QAction("New", parent) self.actionOpen = QAction("Open", parent) self.actionSave = QAction("Save", parent) @@ -23,17 +24,17 @@ class MapToolbar: self.fileMenu.addAction(self.actionSaveAs) def newFile(self): - self.parent.mapData.newFile() + self.parent.map.newFile() def openFile(self): filePath, _ = QFileDialog.getOpenFileName(self.menubar, "Open Map File", MAP_DEFAULT_PATH, "Map Files (*.json)") if filePath: - self.parent.mapData.load(filePath) + self.parent.map.load(filePath) def saveFile(self): - self.parent.mapData.save() + self.parent.map.save() def saveAsFile(self): filePath, _ = QFileDialog.getSaveFileName(self.menubar, "Save Map File As", MAP_DEFAULT_PATH, "Map Files (*.json)") if filePath: - self.parent.mapData.save(filePath) \ No newline at end of file + self.parent.map.save(filePath) \ No newline at end of file diff --git a/tools/editortool/map/selectbox.py b/tools/editortool/map/selectbox.py index 0e538ec..9e2b82d 100644 --- a/tools/editortool/map/selectbox.py +++ b/tools/editortool/map/selectbox.py @@ -10,9 +10,9 @@ class SelectBox: def draw(self): position = [ - self.parent.parent.map.position[0] * TILE_WIDTH - (TILE_WIDTH / 2.0), - self.parent.parent.map.position[1] * TILE_HEIGHT - (TILE_HEIGHT / 2.0), - self.parent.parent.map.position[2] * TILE_DEPTH - (TILE_DEPTH / 2.0) + self.parent.map.position[0] * TILE_WIDTH - (TILE_WIDTH / 2.0), + self.parent.map.position[1] * TILE_HEIGHT - (TILE_HEIGHT / 2.0), + self.parent.map.position[2] * TILE_DEPTH - (TILE_DEPTH / 2.0) ] vertices = [ diff --git a/tools/editortool/map/statusbar.py b/tools/editortool/map/statusbar.py index a65c03d..ccb167a 100644 --- a/tools/editortool/map/statusbar.py +++ b/tools/editortool/map/statusbar.py @@ -9,10 +9,10 @@ class StatusBar(QStatusBar): self.addWidget(self.leftLabel, 1) self.addPermanentWidget(self.rightLabel) - parent.mapData.onMapData.sub(self.onMapData) + parent.map.onMapData.sub(self.onMapData) def setStatus(self, message): self.leftLabel.setText(message) def onMapData(self, data): - self.rightLabel.setText(self.parent.mapData.mapFileName if self.parent.mapData.mapFileName else "Untitled.json") \ No newline at end of file + self.rightLabel.setText(self.parent.map.mapFileName if self.parent.map.mapFileName else "Untitled.json") \ No newline at end of file diff --git a/tools/editortool/maptool.py b/tools/editortool/maptool.py index b4434eb..4bac1f2 100644 --- a/tools/editortool/maptool.py +++ b/tools/editortool/maptool.py @@ -4,38 +4,39 @@ from PyQt5.QtCore import Qt from editortool.map.glwidget import GLWidget from editortool.map.chunkpanel import ChunkPanel from editortool.map.mapinfopanel import MapInfoPanel -from editortool.map.mapdata import MapData -from editortool.map.toolbar import MapToolbar +from editortool.map.menubar import MapMenubar from editortool.map.statusbar import StatusBar from editortool.map.map import Map from editortool.map.mapdefs import TILE_WIDTH, TILE_HEIGHT, TILE_DEPTH +from editortool.map.selectbox import SelectBox +from editortool.map.camera import Camera +from editortool.map.grid import Grid class MapWindow(QMainWindow): def __init__(self): super().__init__() # Subclasses - self.mapData = MapData() self.map = Map(self) + self.camera = Camera(self) + self.grid = Grid() + self.selectBox = SelectBox(self) # Window setup self.setWindowTitle("Dusk Map Editor") self.resize(1280, 720) - # Components - self.toolbar = MapToolbar(self) + # Menubar (TESTING) + self.menubar = MapMenubar(self) central = QWidget() self.setCentralWidget(central) - mainLayout = QHBoxLayout(central) # Left panel (ChunkPanel) self.chunkPanel = ChunkPanel(self) - leftWidget = QWidget() - leftWidget.setLayout(self.chunkPanel.layout()) - leftWidget.setFixedWidth(200) - mainLayout.addWidget(leftWidget) + self.chunkPanel.setFixedWidth(200) + mainLayout.addWidget(self.chunkPanel) # Center panel (GLWidget + controls) self.glWidget = GLWidget(self) @@ -52,21 +53,15 @@ class MapWindow(QMainWindow): self.setStatusBar(self.statusBar) self.installEventFilter(self) - self._install_event_filter_recursive(self) + self.installEventFilterRecursively(self) - def _install_event_filter_recursive(self, widget): + def installEventFilterRecursively(self, widget): for child in widget.findChildren(QWidget): child.installEventFilter(self) - self._install_event_filter_recursive(child) - - def generateData(self): - objOut = {} - self.fileSaving.invoke(objOut) - self.mapData.data = objOut - return objOut + self.installEventFilterRecursively(child) def closeEvent(self, event): - if not self.mapData.isDirty(): + if not self.map.isDirty(): event.accept() return @@ -79,7 +74,7 @@ class MapWindow(QMainWindow): ) if reply == QMessageBox.Save: self.saveFile() - if self.mapData.isDirty(): + if self.map.isDirty(): event.ignore() return elif reply == QMessageBox.Cancel: