changeset 106:d5e69fe5c81a

NES_ROM: implement circular byte shifting For ROMs that have no CHR, sometimes the tiles aren't aligned to 16-byte boundaries. Circularly shifting the PRG data may be necessary to get proper tiles. In order to do so efficiently, this also requires changing the Tile object to be more like a cached look into the underlying tile data. Thus, I turned its `pixmap` and the `raw_tile` properties into descriptors so I can properly clear those cached properties and can thus lazily update them, as required by the update functions.
author Jordi Gutiérrez Hermoso <jordigh@octave.org>
date Sun, 15 Sep 2019 20:47:01 -0400
parents 37e3f2781fdf
children 06d752537e21
files img/darkside/go-down.svg img/darkside/go-up.svg img/lightside/go-down.svg img/lightside/go-up.svg nes.py widgets.py
diffstat 6 files changed, 122 insertions(+), 31 deletions(-) [+]
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/img/darkside/go-down.svg
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
+    <g color="#FFF" fill="#b8b8b8">
+        <path d="M3.707 5.293L2.293 6.707 8 12.414l5.707-5.707-1.414-1.414L8 9.586z" style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;shape-padding:0;isolation:auto;mix-blend-mode:normal;marker:none" font-weight="400" font-family="sans-serif" white-space="normal" overflow="visible"/>
+        <path d="M13 6V5h1v1zM2 6V5h1v1z" style="marker:none" overflow="visible"/>
+        <path d="M2 6c0-.554.446-1 1-1s1 .446 1 1-.446 1-1 1-1-.446-1-1zM12 6c0-.554.446-1 1-1s1 .446 1 1-.446 1-1 1-1-.446-1-1z" style="marker:none" overflow="visible"/>
+    </g>
+</svg>
new file mode 100644
--- /dev/null
+++ b/img/darkside/go-up.svg
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
+    <g color="#FFF" fill="#b8b8b8">
+        <path d="M8 3.586L2.293 9.293l1.414 1.414L8 6.414l4.293 4.293 1.414-1.414z" style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;shape-padding:0;isolation:auto;mix-blend-mode:normal;marker:none" font-weight="400" font-family="sans-serif" white-space="normal" overflow="visible"/>
+        <path d="M13 10v1h1v-1zM2 10v1h1v-1z" style="marker:none" overflow="visible"/>
+        <path d="M2 10c0 .554.446 1 1 1s1-.446 1-1-.446-1-1-1-1 .446-1 1zM12 10c0 .554.446 1 1 1s1-.446 1-1-.446-1-1-1-1 .446-1 1z" style="marker:none" overflow="visible"/>
+    </g>
+</svg>
new file mode 100644
--- /dev/null
+++ b/img/lightside/go-down.svg
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
+    <g color="#000" fill="#474747">
+        <path d="M3.707 5.293L2.293 6.707 8 12.414l5.707-5.707-1.414-1.414L8 9.586z" style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;shape-padding:0;isolation:auto;mix-blend-mode:normal;marker:none" font-weight="400" font-family="sans-serif" white-space="normal" overflow="visible"/>
+        <path d="M13 6V5h1v1zM2 6V5h1v1z" style="marker:none" overflow="visible"/>
+        <path d="M2 6c0-.554.446-1 1-1s1 .446 1 1-.446 1-1 1-1-.446-1-1zM12 6c0-.554.446-1 1-1s1 .446 1 1-.446 1-1 1-1-.446-1-1z" style="marker:none" overflow="visible"/>
+    </g>
+</svg>
new file mode 100644
--- /dev/null
+++ b/img/lightside/go-up.svg
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
+    <g color="#000" fill="#474747">
+        <path d="M8 3.586L2.293 9.293l1.414 1.414L8 6.414l4.293 4.293 1.414-1.414z" style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;shape-padding:0;isolation:auto;mix-blend-mode:normal;marker:none" font-weight="400" font-family="sans-serif" white-space="normal" overflow="visible"/>
+        <path d="M13 10v1h1v-1zM2 10v1h1v-1z" style="marker:none" overflow="visible"/>
+        <path d="M2 10c0 .554.446 1 1 1s1-.446 1-1-.446-1-1-1-1 .446-1 1zM12 10c0 .554.446 1 1 1s1-.446 1-1-.446-1-1-1-1 .446-1 1z" style="marker:none" overflow="visible"/>
+    </g>
+</svg>
--- a/nes.py
+++ b/nes.py
@@ -6,6 +6,10 @@
 class NES_ROM(object):
     def __init__(self, filename):
         self.filename = filename
+        # To track circular rotation of the ROM bytes, in case the
+        # tiles aren't aligned to 16-byte boundaries and the user
+        # moves it.
+        self.initial_position = 0
         self.read_rom()
 
     def read_rom(self):
@@ -20,23 +24,42 @@
         self.PRG = body[0:PRG_size]
         if CHR_size == 0:
             # No chr, just read the whole ROM as tiles
-            tile_data = body
+            self.tile_data = list(body)
             self.CHR = None
         else:
             self.CHR = body[PRG_size:PRG_size+CHR_size]
-            tile_data = self.CHR
+            self.tile_data = list(self.CHR)
+
+        self.tiles = [Tile(i, self.tile_data)
+                      for i in range(0, len(self.tile_data)//16)]
+
+    def update_tiles(self):
+        for tile in self.tiles:
+            tile.clear_caches()
 
-        self.tiles = [Tile(tile_data[i:i+16]) for i in range(0, len(tile_data), 16)]
+    def byte_forward(self):
+        x = self.tile_data.pop(0)
+        self.tile_data.append(x)
+        self.initial_position -= 1
+        self.update_tiles()
+
+    def byte_backward(self):
+        x = self.tile_data.pop(-1)
+        self.tile_data.insert(0, x)
+        self.initial_position += 1
+        self.update_tiles()
 
 
 class Tile(object):
 
     default_palette = TILE_PALETTES["Primary"]
 
-    def __init__(self, raw_tile):
+    def __init__(self, index, tile_data):
         super().__init__()
 
-        self.raw_tile = raw_tile
+        self.index = index
+        self.tile_data = tile_data
+        self.clear_caches()
         self.set_palette(self.default_palette.copy())
 
     def __repr__(self):
@@ -46,6 +69,11 @@
             for row in self.tile
         ) + "\n" + "-"*10
 
+    def clear_caches(self):
+        self._raw_tile = None
+        self._tile = None
+        self._pixmap = None
+
     def set_palette(self, new_palette):
         self.palette = new_palette.copy()
         self.update_pixmap()
@@ -54,24 +82,40 @@
         img_data = bytes(sum(self.tile, []))
         image = QG.QImage(img_data, 8, 8, QG.QImage.Format_Indexed8)
         image.setColorTable(palette_to_qt(self.palette))
-        self.pixmap = QG.QPixmap(image)
+        self._pixmap = QG.QPixmap(image)
+
+    def get_pixmap(self):
+        if self._pixmap is None:
+            self.update_pixmap()
+        return self._pixmap
+
+    def set_pixmap(self, pixmap):
+        # Not implemented
+        pass
+
+    pixmap = property(get_pixmap, set_pixmap, clear_caches, "Pixmap representation of tile")
+
+    def get_raw_tile(self):
+        if self._raw_tile is None:
+            self._raw_tile = self.tile_data[16*self.index:16*(self.index + 1)]
+        return self._raw_tile
+
+    def set_raw_tile(self, raw_tile):
+        self.clear_caches()
+        self.tile_data[16*self.index:16*(self.index + 1)] = raw_tile
+
+    raw_tile = property(get_raw_tile, set_raw_tile, clear_caches, "Raw tile bytes as found in ROM")
 
     def get_tile(self):
-        if getattr(self, "_tile", None) is None:
+        if self._tile is None:
             self._tile = self.parse_tile(self.raw_tile)
         return self._tile
 
     def set_tile(self, tile):
-        del self._tile
+        self.clear_caches()
         self.raw_tile = self.unparse_tile(tile)
 
-        self.update_pixmap()
-
-    def del_tile(self):
-        del self._tile
-        del self.raw_tile
-
-    tile = property(get_tile, set_tile, del_tile, "A tile parsed as an 8x8 array of ints")
+    tile = property(get_tile, set_tile, clear_caches, "A tile parsed as an 8x8 array of ints")
 
     @classmethod
     def parse_tile(cls, tile):
--- a/widgets.py
+++ b/widgets.py
@@ -5,6 +5,7 @@
 from nes import NES_ROM, Tile
 
 BUTTON_SIZE = 32
+ICON_SIZE = 24
 
 
 class GridHolder(object):
@@ -184,6 +185,16 @@
 
         self.tile_picker.picked_tile = tile
 
+    def byteForward(self):
+        self.rom.byte_forward()
+        self.update()
+        self.tile_picker.update()
+
+    def byteBackward(self):
+        self.rom.byte_backward()
+        self.update()
+        self.tile_picker.update()
+
 
 class TilePicker(TileGrid):
     CSS = """
@@ -422,23 +433,31 @@
 
         self.toolbar.addWidget(self.rom_palette)
 
-        for icon, tooltip, function in [
-            ("zoom-in", "Zoom in", "zoomIn"),
-            ("zoom-out", "Zoom out", "zoomOut"),
-            ("zoom-original", "Restore zoom", "zoomOriginal"),
-            ("increaseCols", "Increase number of columns", "increaseCols"),
-            ("decreaseCols", "Decrease number of columns", "decreaseCols"),
+        for group in [
+                [
+                    ("zoom-in", "Zoom in", "zoomIn"),
+                    ("zoom-out", "Zoom out", "zoomOut"),
+                    ("zoom-original", "Restore zoom", "zoomOriginal"),
+                ],
+                [
+                    ("increaseCols", "Increase number of columns", "increaseCols"),
+                    ("decreaseCols", "Decrease number of columns", "decreaseCols"),
+                ],
+                [
+                    ("go-up", "Move all tiles one byte forward", "byteForward"),
+                    ("go-down", "Move all tiles one byte backard", "byteBackward"),
+                ]
         ]:
-            button = QW.QPushButton("")
-            iconpath = widget_icon_path(button)
-            button.setIcon(QG.QIcon(f"img/{iconpath}/{icon}.svg"))
-            button.setFixedSize(QC.QSize(32, 32))
-            button.setIconSize(QC.QSize(24, 24))
-            button.setToolTip(tooltip)
-            button.pressed.connect(getattr(self.rom_canvas, function))
-            self.toolbar.addWidget(button)
-
-        self.toolbar.addStretch()
+            for icon, tooltip, function in group:
+                button = QW.QPushButton("")
+                iconpath = widget_icon_path(button)
+                button.setIcon(QG.QIcon(f"img/{iconpath}/{icon}.svg"))
+                button.setFixedSize(QC.QSize(BUTTON_SIZE, BUTTON_SIZE))
+                button.setIconSize(QC.QSize(ICON_SIZE, ICON_SIZE))
+                button.setToolTip(tooltip)
+                button.pressed.connect(getattr(self.rom_canvas, function))
+                self.toolbar.addWidget(button)
+            self.toolbar.addStretch()
 
         layout = QW.QVBoxLayout()
         layout.addWidget(scroll)