diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 0000000..c6bb036 --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,35 @@ +# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Java CI with Maven + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + - name: Build with Maven + run: mvn -B package --file pom.xml + + # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive + - name: Update dependency graph + uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 diff --git a/README.md b/README.md index 54a9105..7cf146f 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,17 @@ It was originally developed SnowBro and later improved by Central MiB and Lab313 ## Changelog +### v0.23 (by hansbonini a.k.a Anime_World) +- Add swizzle modes for texture visualizations as tile +- Fix Block Dimensions setting up canvas dimensions, now they work individually. + +### v0.22 (by hansbonini a.k.a Anime_World) +- Add support for Mesen and Exodus CRAM Dump as Palette +- Add new codecs for visualization +- Add support for custom tile dimensions +- Add easy block size configuration +- Add statusbar configuration for tile and block dimensions + ### v0.21 (by toruzz) - New themes. Old custom system removed - Fractional scale support diff --git a/pom.xml b/pom.xml index a2af0ef..73dfc44 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 tm tilemolester - 1.0-SNAPSHOT + 0.23 jar UTF-8 diff --git a/src/main/java/tm/TMFileResources.java b/src/main/java/tm/TMFileResources.java index 8fb80b5..ffdd93d 100644 --- a/src/main/java/tm/TMFileResources.java +++ b/src/main/java/tm/TMFileResources.java @@ -163,6 +163,10 @@ public void addToBookmarksTree(Element e, FolderNode folder) { // String palID = e.getAttribute("palette"); String codecID = e.getAttribute("codec"); TileCodec codec = ui.getTileCodecByID(codecID); + String swizzlePattern = e.getAttribute("swizzlepattern"); + if (swizzlePattern == null || swizzlePattern.isEmpty()) { + swizzlePattern = TileCodec.SWIZZLE_NONE; // Default value for backward compatibility + } String desc = XMLParser.getNodeValue(getChildTag(e, "description", 0)); BookmarkItemNode bookmark = new BookmarkItemNode( offset, @@ -175,6 +179,7 @@ public void addToBookmarksTree(Element e, FolderNode folder) { mode, palIndex, codec, + swizzlePattern, desc); folder.add(bookmark); } diff --git a/src/main/java/tm/canvases/TMEditorCanvas.java b/src/main/java/tm/canvases/TMEditorCanvas.java index 237e97a..c9aa9fb 100644 --- a/src/main/java/tm/canvases/TMEditorCanvas.java +++ b/src/main/java/tm/canvases/TMEditorCanvas.java @@ -65,7 +65,6 @@ public class TMEditorCanvas extends TMTileCanvas implements MouseInputListener { private Point moveViewPoint; private Point moveMousePoint; - private int[] tempPixels = new int[8*8]; private Vector modifiedTiles = new Vector(); // tiles modified by operation private Vector modifiedPixels = new Vector(); private Point[][] gridCoords; @@ -73,6 +72,7 @@ public class TMEditorCanvas extends TMTileCanvas implements MouseInputListener { private int blockWidth=1; private int blockHeight=1; private boolean rowInterleaved=false; + private String swizzlePattern = tm.tilecodecs.TileCodec.SWIZZLE_NONE; private boolean showBlockGrid=false; /** @@ -646,7 +646,9 @@ private void drawLine(int x1, int y1, int x2, int y2, boolean trace) { protected void setPixelTraceable(int x, int y, int argb) { // mark the tile as modified - tileModified(x/8, y/8); + int tileWidth = (codec != null) ? codec.getTileWidth() : 8; + int tileHeight = (codec != null) ? codec.getTileHeight() : 8; + tileModified(x/tileWidth, y/tileHeight); setPixel(x, y, argb); } @@ -662,8 +664,12 @@ private void tileModified(int col, int row) { if (!modifiedTiles.contains(p)) { modifiedTiles.add(p); // save original pixels + int tileWidth = (codec != null) ? codec.getTileWidth() : 8; + int tileHeight = (codec != null) ? codec.getTileHeight() : 8; + int tileSize = tileWidth * tileHeight; + int[] tempPixels = new int[tileSize]; copyTilePixelsToBuffer(col, row, tempPixels, 0); - IntBuffer ib = IntBuffer.allocate(8*8); + IntBuffer ib = IntBuffer.allocate(tileSize); ib.put(tempPixels); modifiedPixels.add(ib); } @@ -677,12 +683,19 @@ private void tileModified(int col, int row) { **/ public void copyTilePixelsToBuffer(int x, int y, int[] buf, int ofs) { - int pixOfs = (y * 8 * canvasWidth) + (x * 8); - for (int i=0; i<8; i++) { - for (int j=0; j<8; j++) { - buf[ofs++] = pixels[pixOfs++]; + int tileWidth = (codec != null) ? codec.getTileWidth() : 8; + int tileHeight = (codec != null) ? codec.getTileHeight() : 8; + int pixOfs = (y * tileHeight * canvasWidth) + (x * tileWidth); + for (int i=0; ibits * as the encoded tile data source. -* Doesn't work quite right? +* Uses default 8x8 tiles if codec is not set. **/ public TMTileCanvas(byte[] bits, int cols, int rows) { - super(bits, cols*8, rows*8); + super(bits, cols * 8, rows * 8); // default 8x8 tiles this.cols = cols; this.rows = rows; } @@ -98,7 +97,32 @@ public int getMode() { **/ public void setCodec(TileCodec codec) { + // Preserve current tile dimensions when switching codecs + if (this.codec != null) { + codec.setTileDimensions(this.codec.getTileWidth(), this.codec.getTileHeight()); + } this.codec = codec; + updatePixdataSize(); + } + +/** +* +* Updates the pixdata array size based on current tile dimensions. +* +**/ + + private void updatePixdataSize() { + if (codec != null) { + int tileSize = codec.getTileWidth() * codec.getTileHeight(); + if (tileSize > 0 && (pixdata == null || pixdata.length != tileSize)) { + pixdata = new int[tileSize]; + } + } else { + // Default to 8x8 if no codec is set + if (pixdata == null || pixdata.length != 64) { + pixdata = new int[64]; + } + } } /** @@ -158,9 +182,13 @@ public int getPalIndex() { **/ public void setGridSize(int cols, int rows) { - setCanvasSize(cols*8, rows*8); + int tileWidth = (codec != null) ? codec.getTileWidth() : 8; + int tileHeight = (codec != null) ? codec.getTileHeight() : 8; + setCanvasSize(cols * tileWidth, rows * tileHeight); this.cols = cols; this.rows = rows; + // Ensure pixdata array is properly sized + updatePixdataSize(); } /** @@ -175,27 +203,42 @@ public void packTile(int x, int y) { // encode single atomic tile bitsOfs = getTileBitsOffset(x, y); if (bitsOfs >= 0) { + // Ensure pixdata array is properly sized + updatePixdataSize(); + + int tileWidth = codec.getTileWidth(); + int tileHeight = codec.getTileHeight(); // copy pixels - pixOfs = (y * 8 * canvasWidth) + (x * 8); + pixOfs = (y * tileHeight * canvasWidth) + (x * tileWidth); pos = 0; if (codec.getBitsPerPixel() <= 8) { int colorCount = codec.getColorCount(); int colorIndex = palIndex * colorCount; // map RGB values to palette indices - for (int p=0; p<8; p++) { - for (int q=0; q<8; q++) { - pixdata[pos++] = palette.indexOf(colorIndex, pixels[pixOfs++]); + for (int p=0; p 0) && (getTileHeight() > 0) + && (getTileWidth() <= 256) && (getTileHeight() <= 256)); + } + +} diff --git a/src/main/java/tm/reversibleaction/ReversibleTileModifyAction.java b/src/main/java/tm/reversibleaction/ReversibleTileModifyAction.java index 153d4fe..fe6b889 100644 --- a/src/main/java/tm/reversibleaction/ReversibleTileModifyAction.java +++ b/src/main/java/tm/reversibleaction/ReversibleTileModifyAction.java @@ -57,9 +57,12 @@ public ReversibleTileModifyAction( public void undo() { canvas.getView().gotoBookmark(bookmark); + int tileWidth = (canvas.getCodec() != null) ? canvas.getCodec().getTileWidth() : 8; + int tileHeight = (canvas.getCodec() != null) ? canvas.getCodec().getTileHeight() : 8; + int tileSize = tileWidth * tileHeight; for (int i=0; i>= p; } codecs[i].encode(pixels, bits, ofs, stride); diff --git a/src/main/java/tm/tilecodecs/DirectColorTileCodec.java b/src/main/java/tm/tilecodecs/DirectColorTileCodec.java index 5b3cde4..077aaaa 100644 --- a/src/main/java/tm/tilecodecs/DirectColorTileCodec.java +++ b/src/main/java/tm/tilecodecs/DirectColorTileCodec.java @@ -111,12 +111,13 @@ private static int msb(int mask) { **/ public int[] decode(byte[] bits, int ofs, int stride) { + int[] pixels = new int[tileWidth * tileHeight]; int v, r, g, b, a, s; int pos=0; stride *= bytesPerRow; - for (int i=0; i<8; i++) { + for (int i=0; i> bitsPerPixel*m) & pixelMask; + // decode one pixel, but don't exceed tile width + if (pixelsInRow < tileWidth && pos < pixels.length) { + pixels[pos++] = (b >> bitsPerPixel*m) & pixelMask; + pixelsInRow++; + } } } ofs += stride; @@ -148,14 +153,18 @@ public int[] decode(byte[] bits, int ofs, int stride) { public void encode(int[] pixels, byte[] bits, int ofs, int stride) { int pos = 0; stride *= bytesPerRow; - for (int i=0; i<8; i++) { + for (int i=0; i> k) & 0x01) << (7-j); + + for (int i = 0; i < tileHeight; i++) { + // Process pixels in 8-pixel blocks for this row + int pixelsProcessed = 0; + int rowOfs = ofs; + + while (pixelsProcessed < tileWidth) { + // Reset bitplanes for this 8-pixel block + for (int j = 0; j < bitsPerPixel; j++) { + if (rowOfs + bpOffsets[j] < bits.length) { + bits[rowOfs + bpOffsets[j]] = 0; + } + } + + // Encode up to 8 pixels in this block + int pixelsInThisBlock = Math.min(8, tileWidth - pixelsProcessed); + for (int j = 0; j < pixelsInThisBlock; j++) { + int p = (pos < pixels.length) ? pixels[pos++] : 0; + + for (int k = 0; k < bitsPerPixel; k++) { + if (rowOfs + bpOffsets[k] < bits.length) { + bits[rowOfs + bpOffsets[k]] |= ((p >> k) & 0x01) << (7 - j); + } + } } - pos++; + + pixelsProcessed += pixelsInThisBlock; + + // Move to next 8-pixel block in the same row + rowOfs += bytesPerRow; } + + // Move to next row ofs += stride; } } diff --git a/src/main/java/tm/tilecodecs/SwizzleUtil.java b/src/main/java/tm/tilecodecs/SwizzleUtil.java new file mode 100644 index 0000000..5d22b2c --- /dev/null +++ b/src/main/java/tm/tilecodecs/SwizzleUtil.java @@ -0,0 +1,415 @@ +/* +* +* Copyright (C) 2024 Hans Bonini. +* +* This file is part of Tile Molester. +* +* Tile Molester is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 2 of the License, or +* (at your option) any later version. +* +* Tile Molester is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +*/ + +package tm.tilecodecs; + +/** +* +* Utility class for swizzle/deswizzle operations. +* Supports various platform-specific swizzle patterns with optimized implementations. +* +* Each swizzle pattern is designed to work with specific tile dimensions for optimal results: +* - BC, WII, SWITCH: 4x4 tiles (block-based formats) +* - NDS, 3DS: 8x8 tiles (traditional tile formats) +* - PSP: Variable dimensions (morton-based) +* - Custom: User-configurable +* +**/ + +public class SwizzleUtil { + + // Platform-specific optimal tile dimensions + private static final int BLOCK_FORMAT_TILE_SIZE = 4; // BC, WII, SWITCH + private static final int NINTENDO_TILE_SIZE = 8; // NDS, 3DS + + /** + * Applies swizzling to pixel coordinates based on the specified pattern. + * + * @param x X coordinate + * @param y Y coordinate + * @param width Tile width + * @param height Tile height + * @param pattern Swizzle pattern + * @return Swizzled linear index + */ + public static int applySwizzle(int x, int y, int width, int height, String pattern) { + if (TileCodec.SWIZZLE_NONE.equals(pattern)) { + return y * width + x; + } + + switch (pattern) { + case TileCodec.SWIZZLE_BC: + return applyBCSwizzle(x, y, width, height); + case TileCodec.SWIZZLE_PSP: + return applyPSPSwizzle(x, y, width, height); + case TileCodec.SWIZZLE_NDS: + return applyNDSSwizzle(x, y, width, height); + case TileCodec.SWIZZLE_3DS: + return apply3DSSwizzle(x, y, width, height); + case TileCodec.SWIZZLE_WII: + return applyWiiSwizzle(x, y, width, height); + case TileCodec.SWIZZLE_SWITCH: + return applySwitchSwizzle(x, y, width, height); + case TileCodec.SWIZZLE_CUSTOM: + return applyCustomSwizzle(x, y, width, height, 4, 4, true); // Default custom values + default: + return y * width + x; // No swizzling + } + } + + /** + * Sets optimal tile dimensions for a given swizzle pattern. + * + * @param pattern Swizzle pattern + * @param codec TileCodec to update + */ + public static void setOptimalTileDimensions(String pattern, TileCodec codec) { + if (codec == null) return; + + switch (pattern) { + case TileCodec.SWIZZLE_SWITCH: + case TileCodec.SWIZZLE_BC: + case TileCodec.SWIZZLE_WII: + // Block-based formats work best with 4x4 tile dimensions + codec.setTileDimensions(BLOCK_FORMAT_TILE_SIZE, BLOCK_FORMAT_TILE_SIZE); + break; + case TileCodec.SWIZZLE_NDS: + case TileCodec.SWIZZLE_3DS: + // Nintendo handheld formats typically use 8x8 tiles + codec.setTileDimensions(NINTENDO_TILE_SIZE, NINTENDO_TILE_SIZE); + break; + // PSP and Custom don't change tile dimensions automatically + case TileCodec.SWIZZLE_PSP: + case TileCodec.SWIZZLE_CUSTOM: + case TileCodec.SWIZZLE_NONE: + default: + // Keep existing dimensions + break; + } + } + + /** + * Applies swizzling with custom parameters. + * + * @param x X coordinate + * @param y Y coordinate + * @param width Tile width + * @param height Tile height + * @param pattern Swizzle pattern + * @param customBlockWidth Custom block width (for custom pattern) + * @param customBlockHeight Custom block height (for custom pattern) + * @param customMortonOrder Whether to use Morton order (for custom pattern) + * @return Swizzled linear index + */ + public static int applySwizzle(int x, int y, int width, int height, String pattern, + int customBlockWidth, int customBlockHeight, boolean customMortonOrder) { + if (TileCodec.SWIZZLE_CUSTOM.equals(pattern)) { + return applyCustomSwizzle(x, y, width, height, customBlockWidth, customBlockHeight, customMortonOrder); + } else { + return applySwizzle(x, y, width, height, pattern); + } + } + + /** + * Reverses swizzling to get original coordinates. + * + * @param index Linear index + * @param width Tile width + * @param height Tile height + * @param pattern Swizzle pattern + * @return Array containing {x, y} coordinates + */ + public static int[] reverseSwizzle(int index, int width, int height, String pattern) { + if (TileCodec.SWIZZLE_NONE.equals(pattern)) { + return new int[] { index % width, index / width }; + } + + // For reverse operations, we need to find which x,y produces the given index + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + if (applySwizzle(x, y, width, height, pattern) == index) { + return new int[] { x, y }; + } + } + } + + // Fallback to linear + return new int[] { index % width, index / width }; + } + + /** + * Block Compression (BC) swizzle pattern. + * Used by GPU texture compression formats like BC1-BC7. + * Optimized for 4x4 tile dimensions. + */ + private static int applyBCSwizzle(int x, int y, int width, int height) { + // BC formats use 4x4 blocks + final int blockSize = BLOCK_FORMAT_TILE_SIZE; + + int blockX = x / blockSize; + int blockY = y / blockSize; + int inBlockX = x % blockSize; + int inBlockY = y % blockSize; + + int blocksPerRow = (width + blockSize - 1) / blockSize; + int blockIndex = blockY * blocksPerRow + blockX; + int pixelInBlock = inBlockY * blockSize + inBlockX; + + return blockIndex * (blockSize * blockSize) + pixelInBlock; + } + + /** + * PSP swizzle pattern. + * PSP uses Morton order (Z-order curve) for better cache performance. + * Works with variable tile dimensions. + */ + private static int applyPSPSwizzle(int x, int y, int width, int height) { + // PSP uses pure Morton order - interleave bits of x and y coordinates + return mortonEncode2D(x, y); + } + + /** + * Nintendo DS swizzle pattern. + * NDS uses 8x8 tiles with Morton order within each tile. + * Optimized for 8x8 tile dimensions. + */ + private static int applyNDSSwizzle(int x, int y, int width, int height) { + final int tileSize = NINTENDO_TILE_SIZE; + + // Calculate which 8x8 tile this pixel belongs to + int tileX = x / tileSize; + int tileY = y / tileSize; + int inTileX = x % tileSize; + int inTileY = y % tileSize; + + int tilesPerRow = (width + tileSize - 1) / tileSize; + int tileIndex = tileY * tilesPerRow + tileX; + int pixelInTile = mortonEncode2D(inTileX, inTileY); + + return tileIndex * (tileSize * tileSize) + pixelInTile; + } + + /** + * Nintendo 3DS swizzle pattern. + * 3DS uses 8x8 tiles with Morton order, similar to NDS but with enhanced bit arrangement. + * Optimized for 8x8 tile dimensions. + */ + private static int apply3DSSwizzle(int x, int y, int width, int height) { + final int tileSize = NINTENDO_TILE_SIZE; + + // Calculate which 8x8 tile this pixel belongs to + int tileX = x / tileSize; + int tileY = y / tileSize; + int inTileX = x % tileSize; + int inTileY = y % tileSize; + + int tilesPerRow = (width + tileSize - 1) / tileSize; + int tileIndex = tileY * tilesPerRow + tileX; + int pixelInTile = mortonEncode2D(inTileX, inTileY); + + return tileIndex * (tileSize * tileSize) + pixelInTile; + } + + /** + * Nintendo Switch swizzle pattern. + * Switch uses block-linear format optimized for GPU memory access. + * Implements a simplified version of the Switch's GOB (Group of Blocks) structure. + * Optimized for 4x4 tile dimensions. + */ + private static int applySwitchSwizzle(int x, int y, int width, int height) { + final int blockSize = BLOCK_FORMAT_TILE_SIZE; + + // Calculate block coordinates + int blockX = x / blockSize; + int blockY = y / blockSize; + int pixelX = x % blockSize; + int pixelY = y % blockSize; + + // Switch uses GOBs (Groups of Blocks) for memory layout optimization + // For simplicity, we'll use 8x2 blocks per GOB (32x8 pixels) which works well for most formats + final int gobWidthInBlocks = 8; + final int gobHeightInBlocks = 2; + + int blocksPerRow = (width + blockSize - 1) / blockSize; + + // GOB coordinates + int gobX = blockX / gobWidthInBlocks; + int gobY = blockY / gobHeightInBlocks; + int gobsPerRow = (blocksPerRow + gobWidthInBlocks - 1) / gobWidthInBlocks; + + // Block position within GOB + int blockInGobX = blockX % gobWidthInBlocks; + int blockInGobY = blockY % gobHeightInBlocks; + + // Switch uses a specific block ordering within GOBs for optimal memory access + int blockInGobIndex = blockInGobY * gobWidthInBlocks + blockInGobX; + + // Pixel position within block (linear order) + int pixelInBlock = pixelY * blockSize + pixelX; + + // Calculate final position + int gobIndex = gobY * gobsPerRow + gobX; + int pixelsPerGob = gobWidthInBlocks * gobHeightInBlocks * blockSize * blockSize; + int pixelsPerBlock = blockSize * blockSize; + + return gobIndex * pixelsPerGob + blockInGobIndex * pixelsPerBlock + pixelInBlock; + } + + /** + * Nintendo Wii swizzle pattern. + * Uses 4x4 blocks arranged in 2x2 super-blocks for optimal cache performance. + * Optimized for 4x4 tile dimensions. + */ + private static int applyWiiSwizzle(int x, int y, int width, int height) { + final int blockSize = BLOCK_FORMAT_TILE_SIZE; + + // Calculate which 4x4 block this pixel belongs to + int blockX = x / blockSize; + int blockY = y / blockSize; + int inBlockX = x % blockSize; + int inBlockY = y % blockSize; + + int blocksPerRow = (width + blockSize - 1) / blockSize; + + // Wii arranges 4x4 blocks in 2x2 super-blocks for better cache locality + int superBlockX = blockX / 2; + int superBlockY = blockY / 2; + int blockInSuperX = blockX % 2; + int blockInSuperY = blockY % 2; + + int superBlocksPerRow = (blocksPerRow + 1) / 2; + int superBlockIndex = superBlockY * superBlocksPerRow + superBlockX; + + // Within a super-block, blocks are arranged as: + // 0 1 + // 2 3 + int blockInSuper = blockInSuperY * 2 + blockInSuperX; + + // Within a 4x4 block, pixels are stored linearly + int pixelInBlock = inBlockY * blockSize + inBlockX; + + return superBlockIndex * (4 * blockSize * blockSize) + blockInSuper * (blockSize * blockSize) + pixelInBlock; + } + + /** + * Custom swizzle pattern with configurable parameters. + * + * @param x X coordinate + * @param y Y coordinate + * @param width Tile width + * @param height Tile height + * @param blockWidth Block width + * @param blockHeight Block height + * @param useMortonOrder Whether to use Morton order within blocks + * @return Swizzled linear index + */ + private static int applyCustomSwizzle(int x, int y, int width, int height, + int blockWidth, int blockHeight, boolean useMortonOrder) { + // Calculate which block this pixel belongs to + int blockX = x / blockWidth; + int blockY = y / blockHeight; + int inBlockX = x % blockWidth; + int inBlockY = y % blockHeight; + + int blocksPerRow = (width + blockWidth - 1) / blockWidth; + int blockIndex = blockY * blocksPerRow + blockX; + + int pixelInBlock; + if (useMortonOrder) { + // Use Morton order (Z-order) within the block + pixelInBlock = mortonEncode2D(inBlockX, inBlockY); + } else { + // Use linear order within the block + pixelInBlock = inBlockY * blockWidth + inBlockX; + } + + return blockIndex * (blockWidth * blockHeight) + pixelInBlock; + } + + /** + * Morton encoding (Z-order curve) for 2D coordinates. + * Efficiently interleaves bits of x and y coordinates using bit manipulation. + * + * @param x X coordinate (must be < 65536 for 16-bit processing) + * @param y Y coordinate (must be < 65536 for 16-bit processing) + * @return Morton-encoded value + */ + private static int mortonEncode2D(int x, int y) { + // Use efficient bit interleaving for coordinates up to 16 bits + x = (x | (x << 8)) & 0x00FF00FF; + x = (x | (x << 4)) & 0x0F0F0F0F; + x = (x | (x << 2)) & 0x33333333; + x = (x | (x << 1)) & 0x55555555; + + y = (y | (y << 8)) & 0x00FF00FF; + y = (y | (y << 4)) & 0x0F0F0F0F; + y = (y | (y << 2)) & 0x33333333; + y = (y | (y << 1)) & 0x55555555; + + return x | (y << 1); + } + + /** + * Gets the optimal tile width for a given swizzle pattern. + * + * @param pattern Swizzle pattern + * @return Optimal tile width, or -1 if no specific recommendation + */ + public static int getOptimalTileWidth(String pattern) { + switch (pattern) { + case TileCodec.SWIZZLE_SWITCH: + case TileCodec.SWIZZLE_BC: + case TileCodec.SWIZZLE_WII: + return BLOCK_FORMAT_TILE_SIZE; + case TileCodec.SWIZZLE_NDS: + case TileCodec.SWIZZLE_3DS: + return NINTENDO_TILE_SIZE; + default: + return -1; // No specific recommendation + } + } + + /** + * Gets the optimal tile height for a given swizzle pattern. + * + * @param pattern Swizzle pattern + * @return Optimal tile height, or -1 if no specific recommendation + */ + public static int getOptimalTileHeight(String pattern) { + return getOptimalTileWidth(pattern); // All current patterns use square tiles + } + + /** + * Checks if the given tile dimensions are optimal for the swizzle pattern. + * + * @param pattern Swizzle pattern + * @param width Current tile width + * @param height Current tile height + * @return true if dimensions are optimal, false otherwise + */ + public static boolean areOptimalDimensions(String pattern, int width, int height) { + int optimalWidth = getOptimalTileWidth(pattern); + int optimalHeight = getOptimalTileHeight(pattern); + + if (optimalWidth == -1 || optimalHeight == -1) { + return true; // No specific requirements + } + + return width == optimalWidth && height == optimalHeight; + } +} diff --git a/src/main/java/tm/tilecodecs/TileCodec.java b/src/main/java/tm/tilecodecs/TileCodec.java index 0df3931..02feeaa 100644 --- a/src/main/java/tm/tilecodecs/TileCodec.java +++ b/src/main/java/tm/tilecodecs/TileCodec.java @@ -20,7 +20,7 @@ /** * -* Abstract class for 8x8 ("atomic") tile codecs. +* Abstract class for configurable size tile codecs. * To add a new tile format, simply extend this class and implement decode() and encode(). * **/ @@ -30,13 +30,31 @@ public abstract class TileCodec { public static final int MODE_1D=1; public static final int MODE_2D=2; + // Swizzle pattern constants + public static final String SWIZZLE_NONE = "None"; + public static final String SWIZZLE_BC = "BC"; + public static final String SWIZZLE_PSP = "PSP"; + public static final String SWIZZLE_NDS = "NDS"; + public static final String SWIZZLE_3DS = "3DS"; + public static final String SWIZZLE_WII = "WII"; + public static final String SWIZZLE_SWITCH = "SWITCH"; + public static final String SWIZZLE_CUSTOM = "Custom"; + private String id; private String description; protected int[] pixels; // destination for DEcoded tile data protected int bitsPerPixel; - protected int bytesPerRow; // row = 8 pixels + protected int bytesPerRow; // row = tileWidth pixels protected long colorCount; protected int tileSize; // size of one encoded tile + protected int tileWidth; // width of tile in pixels + protected int tileHeight; // height of tile in pixels + protected String swizzlePattern; // current swizzle pattern + + // Custom swizzle parameters + protected int customBlockWidth = 4; // custom swizzle block width + protected int customBlockHeight = 4; // custom swizzle block height + protected boolean customMortonOrder = true; // use morton order within blocks /** * @@ -47,13 +65,30 @@ public abstract class TileCodec { **/ public TileCodec(String id, int bitsPerPixel, String description) { + this(id, bitsPerPixel, description, 8, 8); + } + +/** +* +* Constructor with configurable tile dimensions. +* +* @param bitsPerPixel Bits per pixel +* @param tileWidth Width of tile in pixels +* @param tileHeight Height of tile in pixels +* +**/ + + public TileCodec(String id, int bitsPerPixel, String description, int tileWidth, int tileHeight) { this.id = id; this.bitsPerPixel = bitsPerPixel; this.description = description; - bytesPerRow = bitsPerPixel; // because (bitsPerPixel*8)/8 = bitsPerPixel - tileSize = bytesPerRow*8; + this.tileWidth = tileWidth; + this.tileHeight = tileHeight; + this.swizzlePattern = SWIZZLE_NONE; // default to no swizzling + bytesPerRow = (bitsPerPixel * tileWidth + 7) / 8; // round up to nearest byte + tileSize = bytesPerRow * tileHeight; colorCount = 1 << bitsPerPixel; - pixels = new int[8*8]; + pixels = new int[tileWidth * tileHeight]; } /** @@ -89,7 +124,7 @@ public int getBitsPerPixel() { /** * -* Gets the # of bytes per row (8 pixels) for the tile format. +* Gets the # of bytes per row (tileWidth pixels) for the tile format. * **/ @@ -97,6 +132,79 @@ public int getBytesPerRow() { return bytesPerRow; } +/** +* +* Gets the width of the tile in pixels. +* +**/ + + public int getTileWidth() { + return tileWidth; + } + +/** +* +* Gets the height of the tile in pixels. +* +**/ + + public int getTileHeight() { + return tileHeight; + } + +/** +* +* Sets the tile dimensions and recalculates dependent values. +* +**/ + + public void setTileDimensions(int width, int height) { + this.tileWidth = width; + this.tileHeight = height; + bytesPerRow = (bitsPerPixel * tileWidth + 7) / 8; // round up to nearest byte + tileSize = bytesPerRow * tileHeight; + pixels = new int[tileWidth * tileHeight]; + } + +/** +* +* Gets the current swizzle pattern. +* +**/ + + public String getSwizzlePattern() { + return swizzlePattern; + } + +/** +* +* Sets the swizzle pattern. +* +**/ + + public void setSwizzlePattern(String swizzlePattern) { + this.swizzlePattern = swizzlePattern; + } + +/** +* +* Gets all available swizzle patterns. +* +**/ + + public static String[] getAvailableSwizzlePatterns() { + return new String[] { + SWIZZLE_NONE, + SWIZZLE_BC, + SWIZZLE_PSP, + SWIZZLE_NDS, + SWIZZLE_3DS, + SWIZZLE_WII, + SWIZZLE_SWITCH, + SWIZZLE_CUSTOM + }; + } + /** * * @@ -147,4 +255,46 @@ public String toString() { return description; } + /** + * Gets the custom swizzle block width. + */ + public int getCustomBlockWidth() { + return customBlockWidth; + } + + /** + * Sets the custom swizzle block width. + */ + public void setCustomBlockWidth(int width) { + this.customBlockWidth = width; + } + + /** + * Gets the custom swizzle block height. + */ + public int getCustomBlockHeight() { + return customBlockHeight; + } + + /** + * Sets the custom swizzle block height. + */ + public void setCustomBlockHeight(int height) { + this.customBlockHeight = height; + } + + /** + * Gets whether custom swizzle uses Morton order. + */ + public boolean getCustomMortonOrder() { + return customMortonOrder; + } + + /** + * Sets whether custom swizzle uses Morton order. + */ + public void setCustomMortonOrder(boolean useMorton) { + this.customMortonOrder = useMorton; + } + } \ No newline at end of file diff --git a/src/main/java/tm/tilecodecs/_3BPPLinearTileCodec.java b/src/main/java/tm/tilecodecs/_3BPPLinearTileCodec.java index bf04f71..58d7bb7 100644 --- a/src/main/java/tm/tilecodecs/_3BPPLinearTileCodec.java +++ b/src/main/java/tm/tilecodecs/_3BPPLinearTileCodec.java @@ -17,22 +17,41 @@ public _3BPPLinearTileCodec() { **/ public int[] decode(byte[] bits, int ofs, int stride) { + int[] pixels = new int[tileWidth * tileHeight]; int pos=0; int b1, b2, b3; stride *= bytesPerRow; - for (int i=0; i<8; i++) { + for (int i=0; i> 5) & 7; - pixels[pos++] = (b1 >> 2) & 7; - pixels[pos++] = ((b1 & 3) << 1) | ((b2 >> 7) & 1); - pixels[pos++] = (b2 >> 4) & 7; - pixels[pos++] = (b2 >> 1) & 7; - pixels[pos++] = ((b2 & 1) << 2) | ((b3 >> 6) & 3); - pixels[pos++] = (b3 >> 3) & 7; - pixels[pos++] = b3 & 7; + + // Original 8-pixel decoding with bounds checking + if (pos + 7 < pixels.length) { + pixels[pos++] = (b1 >> 5) & 7; + pixels[pos++] = (b1 >> 2) & 7; + pixels[pos++] = ((b1 & 3) << 1) | ((b2 >> 7) & 1); + pixels[pos++] = (b2 >> 4) & 7; + pixels[pos++] = (b2 >> 1) & 7; + pixels[pos++] = ((b2 & 1) << 2) | ((b3 >> 6) & 3); + pixels[pos++] = (b3 >> 3) & 7; + pixels[pos++] = b3 & 7; + } else { + // Handle tiles narrower than 8 pixels + for (int j = 0; j < 8 && pos < pixels.length; j++) { + switch (j) { + case 0: pixels[pos++] = (b1 >> 5) & 7; break; + case 1: pixels[pos++] = (b1 >> 2) & 7; break; + case 2: pixels[pos++] = ((b1 & 3) << 1) | ((b2 >> 7) & 1); break; + case 3: pixels[pos++] = (b2 >> 4) & 7; break; + case 4: pixels[pos++] = (b2 >> 1) & 7; break; + case 5: pixels[pos++] = ((b2 & 1) << 2) | ((b3 >> 6) & 3); break; + case 6: pixels[pos++] = (b3 >> 3) & 7; break; + case 7: pixels[pos++] = b3 & 7; break; + } + } + } ofs += stride; } return pixels; @@ -48,18 +67,40 @@ public void encode(int[] pixels, byte[] bits, int ofs, int stride) { int pos = 0; int b1, b2, b3; stride *= bytesPerRow; - for (int i=0; i<8; i++) { + for (int i=0; i> 1; - b2 = (pixels[pos++] & 1) << 7; - b2 |= (pixels[pos++] & 7) << 4; - b2 |= (pixels[pos++] & 7) << 1; - b2 |= (pixels[pos] & 4) >> 2; - b3 = (pixels[pos++] & 3) << 6; - b3 |= (pixels[pos++] & 7) << 3; - b3 |= (pixels[pos++] & 7); + if (pos + 7 < pixels.length) { + // Standard 8-pixel encoding + b1 = (pixels[pos++] & 7) << 5; + b1 |= (pixels[pos++] & 7) << 2; + b1 |= (pixels[pos] & 6) >> 1; + b2 = (pixels[pos++] & 1) << 7; + b2 |= (pixels[pos++] & 7) << 4; + b2 |= (pixels[pos++] & 7) << 1; + b2 |= (pixels[pos] & 4) >> 2; + b3 = (pixels[pos++] & 3) << 6; + b3 |= (pixels[pos++] & 7) << 3; + b3 |= (pixels[pos++] & 7); + } else { + // Handle tiles narrower than 8 pixels + b1 = b2 = b3 = 0; + int[] rowPixels = new int[8]; + for (int j = 0; j < 8; j++) { + rowPixels[j] = (pos < pixels.length) ? pixels[pos++] : 0; + } + + b1 = (rowPixels[0] & 7) << 5; + b1 |= (rowPixels[1] & 7) << 2; + b1 |= (rowPixels[2] & 6) >> 1; + b2 = (rowPixels[2] & 1) << 7; + b2 |= (rowPixels[3] & 7) << 4; + b2 |= (rowPixels[4] & 7) << 1; + b2 |= (rowPixels[5] & 4) >> 2; + b3 = (rowPixels[5] & 3) << 6; + b3 |= (rowPixels[6] & 7) << 3; + b3 |= (rowPixels[7] & 7); + } + bits[ofs++] = (byte)b1; // byte 1: 0001 1122 bits[ofs++] = (byte)b2; // byte 2: 2333 4445 bits[ofs++] = (byte)b3; // byte 3: 5566 6777 diff --git a/src/main/java/tm/tilecodecs/_6BPPLinearTileCodec.java b/src/main/java/tm/tilecodecs/_6BPPLinearTileCodec.java index 81027a9..c7b5e69 100644 --- a/src/main/java/tm/tilecodecs/_6BPPLinearTileCodec.java +++ b/src/main/java/tm/tilecodecs/_6BPPLinearTileCodec.java @@ -17,38 +17,50 @@ public _6BPPLinearTileCodec() { **/ public int[] decode(byte[] bits, int ofs, int stride) { + int[] pixels = new int[tileWidth * tileHeight]; int pos=0; int b1, b2, b3, b4, b5, b6; stride *= bytesPerRow; - for (int i=0; i<8; i++) { - // do one row - /* BPP3 Linear: - * byte 1: 0001 1122 - byte 2: 2333 4445 - byte 3: 5566 6777 - */ - /* BPP6 Linear: - * byte 1: 0000 0011 - byte 2: 1111 2222 - byte 3: 2233 3333 - byte 4: 4444 4455 - byte 5: 5555 6666 - byte 6: 6677 7777 - */ - b6 = bits[ofs++] & 0xFF; // byte 1: 0000 0011 - b5 = bits[ofs++] & 0xFF; // byte 2: 1111 2222 - b4 = bits[ofs++] & 0xFF; // byte 3: 2233 3333 - b3 = bits[ofs++] & 0xFF; // byte 4: 4444 4455 - b2 = bits[ofs++] & 0xFF; // byte 5: 5555 6666 - b1 = bits[ofs++] & 0xFF; // byte 6: 6677 7777 - pixels[pos++] = (b1 >> 2) & 63; - pixels[pos++] = ((b1 & 3) << 4) | ((b2 >> 4) & 15); - pixels[pos++] = ((b2 & 15) << 2) | ((b3 >> 6) & 3); - pixels[pos++] = b3 & 63; - pixels[pos++] = (b4 >> 2) & 63; - pixels[pos++] = ((b4 & 3) << 4) | ((b5 >> 4) & 15); - pixels[pos++] = ((b5 & 15) << 2) | ((b6 >> 6) & 3); - pixels[pos++] = b6 & 63; + for (int i=0; i> 2) & 63; + decodedPixels[1] = ((b1 & 3) << 4) | ((b2 >> 4) & 15); + decodedPixels[2] = ((b2 & 15) << 2) | ((b3 >> 6) & 3); + decodedPixels[3] = b3 & 63; + decodedPixels[4] = (b4 >> 2) & 63; + decodedPixels[5] = ((b4 & 3) << 4) | ((b5 >> 4) & 15); + decodedPixels[6] = ((b5 & 15) << 2) | ((b6 >> 6) & 3); + decodedPixels[7] = b6 & 63; + + // Copy only the pixels we need for this row + int pixelsToTake = Math.min(8, tileWidth - pixelsProcessed); + for (int p = 0; p < pixelsToTake && pos < pixels.length; p++) { + pixels[pos++] = decodedPixels[p]; + } + pixelsProcessed += pixelsToTake; + } else { + // Not enough data, fill with zeros + while (pixelsProcessed < tileWidth && pos < pixels.length) { + pixels[pos++] = 0; + pixelsProcessed++; + } + } + } ofs += stride; } return pixels; @@ -64,48 +76,58 @@ public void encode(int[] pixels, byte[] bits, int ofs, int stride) { int pos = 0; int b1, b2, b3, b4, b5, b6; stride *= bytesPerRow; - for (int i=0; i<8; i++) { - /* BPP3 Linear: - * byte 1: 0001 1122 - byte 2: 2333 4445 - byte 3: 5566 6777 - */ - /* BPP6 Linear: - * byte 1: 0000 0011 - byte 2: 1111 2222 - byte 3: 2233 3333 - byte 4: 4444 4455 - byte 5: 5555 6666 - byte 6: 6677 7777 - */ - - // do one row - b1 = (pixels[pos++] & 63) << 2; - b1 |= (pixels[pos] & 48) >> 4; - - b2 = (pixels[pos++] & 15) << 4; - b2 |= (pixels[pos] & 60) >> 2; - - b3 = (pixels[pos++] & 3) << 6; - b3 |= (pixels[pos++] & 63); - - - b4 = (pixels[pos++] & 63) << 2; - b4 |= (pixels[pos] & 48) >> 4; - - b5 = (pixels[pos++] & 15) << 4; - b5 |= (pixels[pos] & 60) >> 2; - - b6 = (pixels[pos++] & 3) << 6; - b6 |= (pixels[pos++] & 63); + for (int i=0; i> 4; + + b2 = (pixelsToEncode[1] & 15) << 4; + b2 |= (pixelsToEncode[2] & 60) >> 2; + + b3 = (pixelsToEncode[2] & 3) << 6; + b3 |= (pixelsToEncode[3] & 63); + + b4 = (pixelsToEncode[4] & 63) << 2; + b4 |= (pixelsToEncode[5] & 48) >> 4; + + b5 = (pixelsToEncode[5] & 15) << 4; + b5 |= (pixelsToEncode[6] & 60) >> 2; + + b6 = (pixelsToEncode[6] & 3) << 6; + b6 |= (pixelsToEncode[7] & 63); + + // Write the 6 bytes + if (ofs + 5 < bits.length) { + bits[ofs++] = (byte)b6; // byte 1: 0000 0011 + bits[ofs++] = (byte)b5; // byte 2: 1111 2222 + bits[ofs++] = (byte)b4; // byte 3: 2233 3333 + bits[ofs++] = (byte)b3; // byte 4: 4444 4455 + bits[ofs++] = (byte)b2; // byte 5: 5555 6666 + bits[ofs++] = (byte)b1; // byte 6: 6677 7777 + } else { + // Not enough space, skip + break; + } + + pixelsProcessed += pixelsToTake; + } ofs += stride; } } diff --git a/src/main/java/tm/treenodes/BookmarkItemNode.java b/src/main/java/tm/treenodes/BookmarkItemNode.java index 07ac57f..3c27bc9 100644 --- a/src/main/java/tm/treenodes/BookmarkItemNode.java +++ b/src/main/java/tm/treenodes/BookmarkItemNode.java @@ -42,6 +42,7 @@ public class BookmarkItemNode extends TMTreeNode { private String description; // entered by the user to describe/name the bookmark private String paletteID; private int palIndex; + private String swizzlePattern; public BookmarkItemNode(int offset, int cols, @@ -53,6 +54,7 @@ public BookmarkItemNode(int offset, int mode, int palIndex, TileCodec codec, + String swizzlePattern, String description ) { super(); @@ -66,6 +68,7 @@ public BookmarkItemNode(int offset, this.mode = mode; this.palIndex = palIndex; this.codec = codec; + this.swizzlePattern = swizzlePattern; this.description = description; } @@ -129,6 +132,16 @@ public boolean getRowInterleaved() { return rowInterleaved; } +/** +* +* Gets the swizzle pattern. +* +**/ + + public String getSwizzlePattern() { + return swizzlePattern; + } + /** * * @@ -207,6 +220,7 @@ public String toXML() { s.append(" blockheight=\"").append(blockHeight).append("\""); s.append(" rowinterleaved=\"").append(rowInterleaved).append("\""); s.append(" sizeblocktocanvas=\"").append(sizeBlockToCanvas).append("\""); + s.append(" swizzlepattern=\"").append(swizzlePattern).append("\""); if (mode == TileCodec.MODE_1D) { s.append(" mode=\"1D\""); } diff --git a/src/main/java/tm/ui/CustomSwizzleDialog.java b/src/main/java/tm/ui/CustomSwizzleDialog.java new file mode 100644 index 0000000..18f3b36 --- /dev/null +++ b/src/main/java/tm/ui/CustomSwizzleDialog.java @@ -0,0 +1,160 @@ +/* +* +* Copyright (C) 2024 Hans Bonini. +* +* This file is part of Tile Molester. +* +* Tile Molester is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 2 of the License, or +* (at your option) any later version. +* +* Tile Molester is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +*/ + +package tm.ui; + +import tm.tilecodecs.TileCodec; +import javax.swing.*; +import java.awt.*; +import java.awt.event.*; + +/** +* +* Dialog for configuring custom swizzle settings. +* +**/ + +public class CustomSwizzleDialog extends JDialog { + + private boolean isOK = false; + private TileCodec codec; + + private JSpinner blockWidthSpinner; + private JSpinner blockHeightSpinner; + private JCheckBox mortonOrderCheckbox; + + private JButton okButton; + private JButton cancelButton; + + /** + * Creates a new custom swizzle dialog. + */ + public CustomSwizzleDialog(Frame parent, TileCodec codec) { + super(parent, "Custom Swizzle Settings", true); + this.codec = codec; + initComponents(); + setupDialog(); + } + + /** + * Initializes the dialog components. + */ + private void initComponents() { + setLayout(new BorderLayout()); + + // Main panel with form controls + JPanel mainPanel = new JPanel(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(5, 5, 5, 5); + + // Block Width + gbc.gridx = 0; gbc.gridy = 0; gbc.anchor = GridBagConstraints.WEST; + mainPanel.add(new JLabel("Block Width:"), gbc); + + gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL; + blockWidthSpinner = new JSpinner(new SpinnerNumberModel( + codec != null ? codec.getCustomBlockWidth() : 4, 1, 256, 1)); + mainPanel.add(blockWidthSpinner, gbc); + + // Block Height + gbc.gridx = 0; gbc.gridy = 1; gbc.fill = GridBagConstraints.NONE; + mainPanel.add(new JLabel("Block Height:"), gbc); + + gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL; + blockHeightSpinner = new JSpinner(new SpinnerNumberModel( + codec != null ? codec.getCustomBlockHeight() : 4, 1, 256, 1)); + mainPanel.add(blockHeightSpinner, gbc); + + // Morton Order checkbox + gbc.gridx = 0; gbc.gridy = 2; gbc.gridwidth = 2; gbc.fill = GridBagConstraints.NONE; + mortonOrderCheckbox = new JCheckBox("Use Morton Order (Z-order curve)", + codec != null ? codec.getCustomMortonOrder() : true); + mainPanel.add(mortonOrderCheckbox, gbc); + + // Description label + gbc.gridx = 0; gbc.gridy = 3; gbc.gridwidth = 2; gbc.fill = GridBagConstraints.HORIZONTAL; + JLabel descLabel = new JLabel("Morton order interleaves X,Y bits for cache-friendly access patterns.
" + + "Linear order stores pixels sequentially within each block.
"); + descLabel.setFont(descLabel.getFont().deriveFont(Font.PLAIN, 11.0f)); + mainPanel.add(descLabel, gbc); + + add(mainPanel, BorderLayout.CENTER); + + // Button panel + JPanel buttonPanel = new JPanel(new FlowLayout()); + okButton = new JButton("OK"); + cancelButton = new JButton("Cancel"); + + okButton.addActionListener(e -> { + saveSettings(); + isOK = true; + setVisible(false); + }); + + cancelButton.addActionListener(e -> { + isOK = false; + setVisible(false); + }); + + buttonPanel.add(okButton); + buttonPanel.add(cancelButton); + add(buttonPanel, BorderLayout.SOUTH); + } + + /** + * Sets up the dialog properties. + */ + private void setupDialog() { + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + pack(); + setLocationRelativeTo(getParent()); + setResizable(false); + + // Set default button + getRootPane().setDefaultButton(okButton); + + // ESC key closes dialog + KeyStroke escapeKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false); + getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(escapeKeyStroke, "ESCAPE"); + getRootPane().getActionMap().put("ESCAPE", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + isOK = false; + setVisible(false); + } + }); + } + + /** + * Saves the settings to the codec. + */ + private void saveSettings() { + if (codec != null) { + codec.setCustomBlockWidth((Integer) blockWidthSpinner.getValue()); + codec.setCustomBlockHeight((Integer) blockHeightSpinner.getValue()); + codec.setCustomMortonOrder(mortonOrderCheckbox.isSelected()); + } + } + + /** + * Returns whether the OK button was pressed. + */ + public boolean isOK() { + return isOK; + } +} diff --git a/src/main/java/tm/ui/TMStatusBar.java b/src/main/java/tm/ui/TMStatusBar.java index d9f8894..082115f 100644 --- a/src/main/java/tm/ui/TMStatusBar.java +++ b/src/main/java/tm/ui/TMStatusBar.java @@ -41,6 +41,19 @@ public class TMStatusBar extends JPanel { private JLabel modeLabel = new JLabel(" "); private JLabel tilesLabel = new JLabel(" "); private JLabel messageLabel = new JLabel(" "); + private JLabel swizzleLabel = new JLabel(" "); // Swizzle pattern information + + // Tile size controls + private JLabel tileSizeLabel = new JLabel("Tile Size:"); + private JSpinner tileWidthSpinner; + private JSpinner tileHeightSpinner; + + // Block size controls + private JLabel blockSizeLabel = new JLabel("Block Size:"); + private JSpinner blockWidthSpinner; + private JSpinner blockHeightSpinner; + + private TMUI parentUI; /** * @@ -48,8 +61,10 @@ public class TMStatusBar extends JPanel { * **/ - public TMStatusBar() { + public TMStatusBar(TMUI parentUI) { super(); + this.parentUI = parentUI; + JPanel p1 = new JPanel(); p1.setLayout(new GridLayout(1, 3)); p1.add(messageLabel); @@ -65,11 +80,67 @@ public TMStatusBar() { p3.setLayout(new GridLayout(1, 2)); p3.add(modeLabel); p3.add(tilesLabel); - - setLayout(new GridLayout(1, 4)); - add(p1); - add(p2); - add(p3); + + // Only add tile size controls if parentUI is provided + if (parentUI != null) { + // Initialize tile size spinners + tileWidthSpinner = new JSpinner(new SpinnerNumberModel(8, 1, 256, 1)); + tileHeightSpinner = new JSpinner(new SpinnerNumberModel(8, 1, 256, 1)); + + // Initialize block size spinners + blockWidthSpinner = new JSpinner(new SpinnerNumberModel(16, 1, 256, 1)); + blockHeightSpinner = new JSpinner(new SpinnerNumberModel(16, 1, 256, 1)); + + // Set preferred size for spinners + Dimension spinnerSize = new Dimension(50, 20); + tileWidthSpinner.setPreferredSize(spinnerSize); + tileHeightSpinner.setPreferredSize(spinnerSize); + blockWidthSpinner.setPreferredSize(spinnerSize); + blockHeightSpinner.setPreferredSize(spinnerSize); + + // Add change listeners to spinners + tileWidthSpinner.addChangeListener(e -> updateTileSize()); + tileHeightSpinner.addChangeListener(e -> updateTileSize()); + blockWidthSpinner.addChangeListener(e -> updateBlockSize()); + blockHeightSpinner.addChangeListener(e -> updateBlockSize()); + + // Create tile size panel + JPanel p4 = new JPanel(); + p4.setLayout(new FlowLayout(FlowLayout.LEFT, 2, 0)); + p4.add(tileSizeLabel); + p4.add(new JLabel("W:")); + p4.add(tileWidthSpinner); + p4.add(new JLabel("H:")); + p4.add(tileHeightSpinner); + + // Create block size panel + JPanel p5 = new JPanel(); + p5.setLayout(new FlowLayout(FlowLayout.LEFT, 2, 0)); + p5.add(blockSizeLabel); + p5.add(new JLabel("W:")); + p5.add(blockWidthSpinner); + p5.add(new JLabel("H:")); + p5.add(blockHeightSpinner); + + // Create swizzle panel + JPanel p6 = new JPanel(); + p6.setLayout(new FlowLayout(FlowLayout.LEFT, 2, 0)); + p6.add(new JLabel("Swizzle:")); + p6.add(swizzleLabel); + + setLayout(new GridLayout(1, 7)); + add(p1); + add(p2); + add(p3); + add(p4); + add(p5); + add(p6); + } else { + setLayout(new GridLayout(1, 4)); + add(p1); + add(p2); + add(p3); + } // pane.add(new JLabel(" ")); // just some whitespace //offsetLabel.setBorder(new BevelBorder(BevelBorder.LOWERED)); @@ -80,6 +151,103 @@ public TMStatusBar() { //tilesLabel.setBorder(new BevelBorder(BevelBorder.LOWERED)); } +/** +* +* Creates the status bar (legacy constructor for backward compatibility). +* +**/ + + public TMStatusBar() { + this(null); + } + +/** +* +* Updates tile size when spinner values change. +* +**/ + + private void updateTileSize() { + if (parentUI != null) { + int width = (Integer) tileWidthSpinner.getValue(); + int height = (Integer) tileHeightSpinner.getValue(); + parentUI.setTileSize(width, height); + } + } + +/** +* +* Updates block size when spinner values change. +* +**/ + + private void updateBlockSize() { + if (parentUI != null) { + // Block dimensions are always independent of canvas size + int width = (Integer) blockWidthSpinner.getValue(); + int height = (Integer) blockHeightSpinner.getValue(); + parentUI.setBlockSize(width, height); + } + } + +/** +* +* Updates the tile size spinners to reflect current tile dimensions. +* +**/ + + public void setTileSize(int width, int height) { + if (tileWidthSpinner != null && tileHeightSpinner != null) { + tileWidthSpinner.setValue(width); + tileHeightSpinner.setValue(height); + } + } + +/** +* +* Updates the block size spinners to reflect current block dimensions. +* +**/ + + public void setBlockSize(int width, int height) { + if (blockWidthSpinner != null && blockHeightSpinner != null) { + // Temporarily remove listeners to avoid triggering updates + ChangeListener[] widthListeners = blockWidthSpinner.getChangeListeners(); + ChangeListener[] heightListeners = blockHeightSpinner.getChangeListeners(); + + for (ChangeListener listener : widthListeners) { + blockWidthSpinner.removeChangeListener(listener); + } + for (ChangeListener listener : heightListeners) { + blockHeightSpinner.removeChangeListener(listener); + } + + // Update values + blockWidthSpinner.setValue(width); + blockHeightSpinner.setValue(height); + + // Restore listeners + for (ChangeListener listener : widthListeners) { + blockWidthSpinner.addChangeListener(listener); + } + for (ChangeListener listener : heightListeners) { + blockHeightSpinner.addChangeListener(listener); + } + } + } + + /** + * Updates the block size label to show that block dimensions are always independent. + */ + public void updateBlockSizeLabel(boolean isFullCanvas) { + // Block dimensions are always independent of canvas size + blockSizeLabel.setText("Block Size:"); + blockSizeLabel.setToolTipText("Block dimensions are independent of canvas size"); + // Always enable spinners since block size is always independent + if (blockWidthSpinner != null) blockWidthSpinner.setEnabled(true); + if (blockHeightSpinner != null) blockHeightSpinner.setEnabled(true); + } + /** * * Sets the text for general message. @@ -197,6 +365,8 @@ else if (ec.isDrawingLine()) { } if (view.getTileCodec() != null) { setCodec(view.getTileCodec().getDescription()); + // Update tile size spinners + setTileSize(view.getTileCodec().getTileWidth(), view.getTileCodec().getTileHeight()); } else { setCodec(""); @@ -204,6 +374,13 @@ else if (ec.isDrawingLine()) { setPalOffset(view.getPalette().getOffset()); setMode(view.getMode()); setTiles(view.getCols(), view.getRows()); + + // Update block size spinners and label + setBlockSize(view.getBlockWidth(), view.getBlockHeight()); + updateBlockSizeLabel(view.getSizeBlockToCanvas()); + + // Update swizzle information + setSwizzle(view.getSwizzlePattern(), view.getTileCodec()); } /** @@ -230,4 +407,36 @@ protected static void buildConstraints(GridBagConstraints gbc, int gx, int gy, i public void setCoords(String string) { coordsLabel.setText(string); } + + /** + * Sets the swizzle information text. + */ + public void setSwizzle(String swizzlePattern, tm.tilecodecs.TileCodec codec) { + if (swizzlePattern == null || tm.tilecodecs.TileCodec.SWIZZLE_NONE.equals(swizzlePattern)) { + swizzleLabel.setText("None"); + } else if (tm.tilecodecs.TileCodec.SWIZZLE_CUSTOM.equals(swizzlePattern) && codec != null) { + String mortonText = codec.getCustomMortonOrder() ? "Morton" : "Linear"; + swizzleLabel.setText(String.format("Custom... %dx%d %s", + codec.getCustomBlockWidth(), codec.getCustomBlockHeight(), mortonText)); + } else { + // For predefined patterns, just show the pattern name + swizzleLabel.setText(getSwizzleDisplayName(swizzlePattern)); + } + } + + /** + * Gets a user-friendly display name for a swizzle pattern. + * Names match those used in the menu system for consistency. + */ + private String getSwizzleDisplayName(String pattern) { + switch (pattern) { + case tm.tilecodecs.TileCodec.SWIZZLE_BC: return "BC Texture"; + case tm.tilecodecs.TileCodec.SWIZZLE_PSP: return "PlayStation Portable"; + case tm.tilecodecs.TileCodec.SWIZZLE_NDS: return "Nintendo DS"; + case tm.tilecodecs.TileCodec.SWIZZLE_3DS: return "Nintendo 3DS"; + case tm.tilecodecs.TileCodec.SWIZZLE_WII: return "Nintendo Wii"; + case tm.tilecodecs.TileCodec.SWIZZLE_SWITCH: return "Nintendo Switch"; + default: return pattern != null ? pattern : "None"; + } + } } \ No newline at end of file diff --git a/src/main/java/tm/ui/TMUI.java b/src/main/java/tm/ui/TMUI.java index f49998f..fb0d200 100644 --- a/src/main/java/tm/ui/TMUI.java +++ b/src/main/java/tm/ui/TMUI.java @@ -76,7 +76,7 @@ public class TMUI extends JFrame { // UI components private mxScrollableDesktop desktop = new mxScrollableDesktop(); - private TMStatusBar statusBar = new TMStatusBar(); + private TMStatusBar statusBar; private JToolBar toolBar = new JToolBar(JToolBar.HORIZONTAL); private JToolBar toolBarMDI = new JToolBar(JToolBar.HORIZONTAL); private JToolBar toolPalette = new JToolBar(JToolBar.VERTICAL); @@ -105,6 +105,7 @@ public class TMUI extends JFrame { private TMStretchDialog stretchDialog; private TMCanvasSizeDialog canvasSizeDialog; private TMBlockSizeDialog blockSizeDialog; + private TMTileSizeDialog tileSizeDialog; private TMAddToTreeDialog addBookmarkDialog; private TMAddToTreeDialog addPaletteDialog; private TMOrganizeTreeDialog organizeBookmarksDialog; @@ -239,8 +240,23 @@ public class TMUI extends JFrame { private JMenuItem _3200MenuItem = new JMenuItem("3200%"); private JMenu blockSizeMenu = new JMenu("Block Size"); private JCheckBoxMenuItem sizeBlockToCanvasMenuItem = new JCheckBoxMenuItem("Full Canvas"); + private JCheckBoxMenuItem sizeBlockToCanvasMenuItem8x8 = new JCheckBoxMenuItem("8x8 pixels"); + private JCheckBoxMenuItem sizeBlockToCanvasMenuItem8x16 = new JCheckBoxMenuItem("8x16 pixels"); + private JCheckBoxMenuItem sizeBlockToCanvasMenuItem16x16 = new JCheckBoxMenuItem("16x16 pixels"); + private JCheckBoxMenuItem sizeBlockToCanvasMenuItem24x24 = new JCheckBoxMenuItem("24x24 pixels"); + private JCheckBoxMenuItem sizeBlockToCanvasMenuItem32x32 = new JCheckBoxMenuItem("32x32 pixels"); private JMenuItem customBlockSizeMenuItem = new JMenuItem("Custom..."); + private JMenuItem customTileSizeMenuItem = new JMenuItem("Custom Tile Size..."); private JRadioButtonMenuItem rowInterleaveBlocksMenuItem = new JRadioButtonMenuItem("Row-interleave Blocks"); + private JMenu swizzleMenu = new JMenu("Swizzle"); + private JRadioButtonMenuItem swizzleNoneMenuItem = new JRadioButtonMenuItem("None"); + private JRadioButtonMenuItem swizzleBCMenuItem = new JRadioButtonMenuItem("BC"); + private JRadioButtonMenuItem swizzlePSPMenuItem = new JRadioButtonMenuItem("PSP"); + private JRadioButtonMenuItem swizzleNDSMenuItem = new JRadioButtonMenuItem("NDS"); + private JRadioButtonMenuItem swizzle3DSMenuItem = new JRadioButtonMenuItem("3DS"); + private JRadioButtonMenuItem swizzleWiiMenuItem = new JRadioButtonMenuItem("WII"); + private JRadioButtonMenuItem swizzleSWITCHMenuItem = new JRadioButtonMenuItem("SWITCH"); + private JRadioButtonMenuItem swizzleCustomMenuItem = new JRadioButtonMenuItem("Custom"); private JMenu modeMenu = new JMenu("Mode"); private JRadioButtonMenuItem _1DimensionalMenuItem = new JRadioButtonMenuItem("1-Dimensional"); private JRadioButtonMenuItem _2DimensionalMenuItem = new JRadioButtonMenuItem("2-Dimensional"); @@ -292,6 +308,7 @@ public class TMUI extends JFrame { private ButtonGroup paletteButtonGroup = new ButtonGroup(); private ButtonGroup modeButtonGroup = new ButtonGroup(); private ButtonGroup paletteEndiannessButtonGroup = new ButtonGroup(); + private ButtonGroup swizzleButtonGroup = new ButtonGroup(); private Hashtable tileCodecButtonHashtable = new Hashtable(); private Hashtable colorCodecButtonHashtable = new Hashtable(); @@ -401,6 +418,15 @@ public TMUI() { sizeBlockToCanvasMenuItem.setText(xlate("Full_Canvas")); customBlockSizeMenuItem.setText(xlate("Custom_Block_Size")); rowInterleaveBlocksMenuItem.setText(xlate("Row_Interleave_Blocks")); + swizzleMenu.setText(xlate("Swizzle")); + swizzleNoneMenuItem.setText(xlate("Swizzle_None")); + swizzleBCMenuItem.setText(xlate("Swizzle_BC")); + swizzlePSPMenuItem.setText(xlate("Swizzle_PSP")); + swizzleNDSMenuItem.setText(xlate("Swizzle_NDS")); + swizzle3DSMenuItem.setText(xlate("Swizzle_3DS")); + swizzleWiiMenuItem.setText(xlate("Swizzle_WII")); + swizzleSWITCHMenuItem.setText(xlate("Swizzle_SWITCH")); + swizzleCustomMenuItem.setText(xlate("Swizzle_Custom")); blockGridMenuItem.setText(xlate("Block_Grid")); tileGridMenuItem.setText(xlate("Tile_Grid")); pixelGridMenuItem.setText(xlate("Pixel_Grid")); @@ -495,6 +521,7 @@ public TMUI() { stretchDialog = new TMStretchDialog(this, xl); canvasSizeDialog = new TMCanvasSizeDialog(this, xl); blockSizeDialog = new TMBlockSizeDialog(this, xl); + tileSizeDialog = new TMTileSizeDialog(this, xl); addBookmarkDialog = new TMAddToTreeDialog(this, "Add_To_Bookmarks_Dialog_Title", xl); addPaletteDialog = new TMAddToTreeDialog(this, "Add_To_Palettes_Dialog_Title", xl); organizeBookmarksDialog = new TMOrganizeTreeDialog(this, "Organize_Bookmarks_Dialog_Title", xl); @@ -506,6 +533,9 @@ public TMUI() { newPaletteDialog.setCodecs(colorcodecs); importInternalPaletteDialog.setCodecs(colorcodecs); + // Initialize status bar with reference to this TMUI instance + statusBar = new TMStatusBar(this); + // Set up the GUI. // main contentpane JPanel pane = new JPanel(); @@ -1424,9 +1454,47 @@ public void actionPerformed(ActionEvent e) { doSizeBlockToCanvasCommand(); } }); + sizeBlockToCanvasMenuItem8x8.addActionListener( + new ActionListener() { + public void actionPerformed(ActionEvent e) { + doSizeBlock8x8ToCanvasCommand(); + } + }); + sizeBlockToCanvasMenuItem8x16.addActionListener( + new ActionListener() { + public void actionPerformed(ActionEvent e) { + doSizeBlock8x16ToCanvasCommand(); + } + }); + sizeBlockToCanvasMenuItem16x16.addActionListener( + new ActionListener() { + public void actionPerformed(ActionEvent e) { + doSizeBlock16x16ToCanvasCommand(); + } + }); + sizeBlockToCanvasMenuItem24x24.addActionListener( + new ActionListener() { + public void actionPerformed(ActionEvent e) { + doSizeBlock24x24ToCanvasCommand(); + } + }); + sizeBlockToCanvasMenuItem32x32.addActionListener( + new ActionListener() { + public void actionPerformed(ActionEvent e) { + doSizeBlock32x32ToCanvasCommand(); + } + }); blockSizeMenu.add(sizeBlockToCanvasMenuItem); // blockSizeMenu.addSeparator(); + // Pre Defined Block Size + blockSizeMenu.add(sizeBlockToCanvasMenuItem8x8); + blockSizeMenu.add(sizeBlockToCanvasMenuItem8x16); + blockSizeMenu.add(sizeBlockToCanvasMenuItem16x16); + blockSizeMenu.add(sizeBlockToCanvasMenuItem24x24); + blockSizeMenu.add(sizeBlockToCanvasMenuItem32x32); + // + blockSizeMenu.addSeparator(); // Custom Block Size customBlockSizeMenuItem.setMnemonic(KeyEvent.VK_C); customBlockSizeMenuItem.addActionListener( @@ -1437,6 +1505,15 @@ public void actionPerformed(ActionEvent e) { }); blockSizeMenu.add(customBlockSizeMenuItem); viewMenu.add(blockSizeMenu); + // Custom Tile Size + customTileSizeMenuItem.setText(xlate("Custom_Tile_Size")); + customTileSizeMenuItem.addActionListener( + new ActionListener() { + public void actionPerformed(ActionEvent e) { + doCustomTileSizeCommand(); + } + }); + viewMenu.add(customTileSizeMenuItem); // Row-interleave Blocks rowInterleaveBlocksMenuItem.setMnemonic(KeyEvent.VK_R); rowInterleaveBlocksMenuItem.addActionListener( @@ -1446,6 +1523,50 @@ public void actionPerformed(ActionEvent e) { } }); viewMenu.add(rowInterleaveBlocksMenuItem); + // Swizzle submenu + swizzleNoneMenuItem.setMnemonic(KeyEvent.VK_N); + swizzleNoneMenuItem.addActionListener(e -> doSwizzleCommand(TileCodec.SWIZZLE_NONE)); + swizzleButtonGroup.add(swizzleNoneMenuItem); + swizzleMenu.add(swizzleNoneMenuItem); + + swizzleBCMenuItem.setMnemonic(KeyEvent.VK_B); + swizzleBCMenuItem.addActionListener(e -> doSwizzleCommand(TileCodec.SWIZZLE_BC)); + swizzleButtonGroup.add(swizzleBCMenuItem); + swizzleMenu.add(swizzleBCMenuItem); + + swizzlePSPMenuItem.setMnemonic(KeyEvent.VK_P); + swizzlePSPMenuItem.addActionListener(e -> doSwizzleCommand(TileCodec.SWIZZLE_PSP)); + swizzleButtonGroup.add(swizzlePSPMenuItem); + swizzleMenu.add(swizzlePSPMenuItem); + + swizzleNDSMenuItem.setMnemonic(KeyEvent.VK_D); + swizzleNDSMenuItem.addActionListener(e -> doSwizzleCommand(TileCodec.SWIZZLE_NDS)); + swizzleButtonGroup.add(swizzleNDSMenuItem); + swizzleMenu.add(swizzleNDSMenuItem); + + swizzle3DSMenuItem.setMnemonic(KeyEvent.VK_3); + swizzle3DSMenuItem.addActionListener(e -> doSwizzleCommand(TileCodec.SWIZZLE_3DS)); + swizzleButtonGroup.add(swizzle3DSMenuItem); + swizzleMenu.add(swizzle3DSMenuItem); + + swizzleWiiMenuItem.setMnemonic(KeyEvent.VK_W); + swizzleWiiMenuItem.addActionListener(e -> doSwizzleCommand(TileCodec.SWIZZLE_WII)); + swizzleButtonGroup.add(swizzleWiiMenuItem); + swizzleMenu.add(swizzleWiiMenuItem); + + swizzleSWITCHMenuItem.setMnemonic(KeyEvent.VK_H); + swizzleSWITCHMenuItem.addActionListener(e -> doSwizzleCommand(TileCodec.SWIZZLE_SWITCH)); + swizzleButtonGroup.add(swizzleSWITCHMenuItem); + swizzleMenu.add(swizzleSWITCHMenuItem); + + swizzleCustomMenuItem.setMnemonic(KeyEvent.VK_C); + swizzleCustomMenuItem.addActionListener(e -> doCustomSwizzleCommand()); + swizzleButtonGroup.add(swizzleCustomMenuItem); + swizzleMenu.add(swizzleCustomMenuItem); + + // Default to None + swizzleNoneMenuItem.setSelected(true); + viewMenu.add(swizzleMenu); // viewMenu.addSeparator(); // Block Grid @@ -2827,6 +2948,123 @@ public void doSizeBlockToCanvasCommand() { if (view != null) { view.setSizeBlockToCanvas(!view.getSizeBlockToCanvas()); sizeBlockToCanvasMenuItem.setSelected(view.getSizeBlockToCanvas()); + sizeBlockToCanvasMenuItem8x8.setSelected(false); + sizeBlockToCanvasMenuItem8x16.setSelected(false); + sizeBlockToCanvasMenuItem16x16.setSelected(false); + sizeBlockToCanvasMenuItem24x24.setSelected(false); + sizeBlockToCanvasMenuItem32x32.setSelected(false); + + // Update status bar to reflect the change + if (statusBar != null) { + statusBar.updateBlockSizeLabel(view.getSizeBlockToCanvas()); + statusBar.setBlockSize(view.getBlockWidth(), view.getBlockHeight()); + } + } + } + + /** + * + * + * + **/ + + public void doSizeBlock8x8ToCanvasCommand() { + TMView view = getSelectedView(); + if (view != null) { + // TMBlockSizeDialog blockSizeDialog = new TMBlockSizeDialog(this, xl); + view.setSizeBlockToCanvas(false); + sizeBlockToCanvasMenuItem.setSelected(false); + sizeBlockToCanvasMenuItem8x8.setSelected(true); + sizeBlockToCanvasMenuItem8x16.setSelected(false); + sizeBlockToCanvasMenuItem16x16.setSelected(false); + sizeBlockToCanvasMenuItem24x24.setSelected(false); + sizeBlockToCanvasMenuItem32x32.setSelected(false); + view.setBlockDimensions(1, 1); + } + } + + /** + * + * + * + **/ + + public void doSizeBlock8x16ToCanvasCommand() { + TMView view = getSelectedView(); + if (view != null) { + // TMBlockSizeDialog blockSizeDialog = new TMBlockSizeDialog(this, xl); + view.setSizeBlockToCanvas(false); + sizeBlockToCanvasMenuItem.setSelected(false); + sizeBlockToCanvasMenuItem8x8.setSelected(false); + sizeBlockToCanvasMenuItem8x16.setSelected(true); + sizeBlockToCanvasMenuItem16x16.setSelected(false); + sizeBlockToCanvasMenuItem24x24.setSelected(false); + sizeBlockToCanvasMenuItem32x32.setSelected(false); + view.setBlockDimensions(1, 2); + } + } + + /** + * + * + * + **/ + + public void doSizeBlock16x16ToCanvasCommand() { + TMView view = getSelectedView(); + if (view != null) { + // TMBlockSizeDialog blockSizeDialog = new TMBlockSizeDialog(this, xl); + view.setSizeBlockToCanvas(false); + sizeBlockToCanvasMenuItem.setSelected(false); + sizeBlockToCanvasMenuItem8x8.setSelected(false); + sizeBlockToCanvasMenuItem8x16.setSelected(false); + sizeBlockToCanvasMenuItem16x16.setSelected(true); + sizeBlockToCanvasMenuItem24x24.setSelected(false); + sizeBlockToCanvasMenuItem32x32.setSelected(false); + view.setBlockDimensions(2, 2); + } + } + + + /** + * + * + * + **/ + + public void doSizeBlock24x24ToCanvasCommand() { + TMView view = getSelectedView(); + if (view != null) { + // TMBlockSizeDialog blockSizeDialog = new TMBlockSizeDialog(this, xl); + view.setSizeBlockToCanvas(false); + sizeBlockToCanvasMenuItem.setSelected(false); + sizeBlockToCanvasMenuItem8x8.setSelected(false); + sizeBlockToCanvasMenuItem8x16.setSelected(false); + sizeBlockToCanvasMenuItem16x16.setSelected(false); + sizeBlockToCanvasMenuItem24x24.setSelected(true); + sizeBlockToCanvasMenuItem32x32.setSelected(false); + view.setBlockDimensions(3, 3); + } + } + + /** + * + * + * + **/ + + public void doSizeBlock32x32ToCanvasCommand() { + TMView view = getSelectedView(); + if (view != null) { + // TMBlockSizeDialog blockSizeDialog = new TMBlockSizeDialog(this, xl); + view.setSizeBlockToCanvas(false); + sizeBlockToCanvasMenuItem.setSelected(false); + sizeBlockToCanvasMenuItem8x8.setSelected(false); + sizeBlockToCanvasMenuItem8x16.setSelected(false); + sizeBlockToCanvasMenuItem16x16.setSelected(false); + sizeBlockToCanvasMenuItem24x24.setSelected(false); + sizeBlockToCanvasMenuItem32x32.setSelected(true); + view.setBlockDimensions(4, 4); } } @@ -2849,6 +3087,74 @@ public void doCustomBlockSizeCommand() { } } + /** + * + * Handles menu command "Custom Tile Size". + * + **/ + public void doCustomTileSizeCommand() { + TMView view = getSelectedView(); + if (view != null) { + TileCodec codec = view.getTileCodec(); + if (codec != null) { + int currentWidth = codec.getTileWidth(); + int currentHeight = codec.getTileHeight(); + int retVal = tileSizeDialog.showDialog(currentWidth, currentHeight); + if (retVal == JOptionPane.OK_OPTION) { + int newWidth = tileSizeDialog.getTileWidth(); + int newHeight = tileSizeDialog.getTileHeight(); + codec.setTileDimensions(newWidth, newHeight); + // Redimensionar o canvas para acomodar os novos tamanhos de tile + TMTileCanvas canvas = view.getEditorCanvas(); + canvas.setGridSize(canvas.getCols(), canvas.getRows()); + // Limpar e redecodificar os pixels com as novas dimensões + canvas.unpackPixels(); + view.repaint(); + // Update status bar display + statusBar.setTileSize(newWidth, newHeight); + } + } + } + } + + /** + * + * Sets tile size directly (called from status bar spinners). + * + **/ + + public void setTileSize(int width, int height) { + TMView view = getSelectedView(); + if (view != null) { + TileCodec codec = view.getTileCodec(); + if (codec != null) { + codec.setTileDimensions(width, height); + // Redimensionar o canvas para acomodar os novos tamanhos de tile + TMTileCanvas canvas = view.getEditorCanvas(); + canvas.setGridSize(canvas.getCols(), canvas.getRows()); + // Limpar e redecodificar os pixels com as novas dimensões + canvas.unpackPixels(); + view.repaint(); + } + } + } + + /** + * + * Sets block size directly (called from status bar spinners). + * Block dimensions are always independent of canvas size. + * + **/ + + public void setBlockSize(int width, int height) { + TMView view = getSelectedView(); + if (view != null) { + view.setBlockDimensions(width, height); + // Update status bar display + statusBar.setBlockSize(width, height); + } + } + /** * * @@ -2863,6 +3169,66 @@ public void doRowInterleaveBlocksCommand() { } } + /** + * + * Sets the swizzle pattern for the current view. + * + **/ + + public void doSwizzleCommand(String swizzlePattern) { + TMView view = getSelectedView(); + if (view != null) { + // Set optimal tile dimensions for the selected swizzle pattern + tm.tilecodecs.SwizzleUtil.setOptimalTileDimensions(swizzlePattern, view.getTileCodec()); + + view.setSwizzlePattern(swizzlePattern); + updateSwizzleMenuSelection(swizzlePattern); + refreshStatusBar(); // Update status bar to show swizzle info + + // Refresh the view to apply new tile dimensions + view.getEditorCanvas().repaint(); + } + } + + /** + * + * Opens the custom swizzle configuration dialog. + * + **/ + + public void doCustomSwizzleCommand() { + TMView view = getSelectedView(); + if (view != null) { + CustomSwizzleDialog dialog = new CustomSwizzleDialog(this, view.getTileCodec()); + dialog.setVisible(true); + if (dialog.isOK()) { + view.setSwizzlePattern(TileCodec.SWIZZLE_CUSTOM); + updateSwizzleMenuSelection(TileCodec.SWIZZLE_CUSTOM); + refreshStatusBar(); // Update status bar to show swizzle info + } else { + // If cancelled, revert to previous selection + updateSwizzleMenuSelection(view.getSwizzlePattern()); + } + } + } + + /** + * + * Updates the swizzle menu selection based on the current pattern. + * + **/ + + private void updateSwizzleMenuSelection(String swizzlePattern) { + swizzleNoneMenuItem.setSelected(TileCodec.SWIZZLE_NONE.equals(swizzlePattern)); + swizzleBCMenuItem.setSelected(TileCodec.SWIZZLE_BC.equals(swizzlePattern)); + swizzlePSPMenuItem.setSelected(TileCodec.SWIZZLE_PSP.equals(swizzlePattern)); + swizzleNDSMenuItem.setSelected(TileCodec.SWIZZLE_NDS.equals(swizzlePattern)); + swizzle3DSMenuItem.setSelected(TileCodec.SWIZZLE_3DS.equals(swizzlePattern)); + swizzleWiiMenuItem.setSelected(TileCodec.SWIZZLE_WII.equals(swizzlePattern)); + swizzleSWITCHMenuItem.setSelected(TileCodec.SWIZZLE_SWITCH.equals(swizzlePattern)); + swizzleCustomMenuItem.setSelected(TileCodec.SWIZZLE_CUSTOM.equals(swizzlePattern)); + } + /** * * @@ -4205,6 +4571,7 @@ public void viewSelected(TMView view) { tileGridMenuItem.setSelected(ec.isTileGridVisible()); pixelGridMenuItem.setSelected(ec.isPixelGridVisible()); rowInterleaveBlocksMenuItem.setSelected(ec.getRowInterleaveBlocks()); + updateSwizzleMenuSelection(ec.getSwizzlePattern()); refreshModeSelection(view); refreshTileCodecSelection(view); @@ -4303,6 +4670,11 @@ public void refreshStatusBar() { TMView view = getSelectedView(); if (view != null) { statusBar.viewSelected(view); + // Update tile size spinners + TileCodec codec = view.getTileCodec(); + if (codec != null) { + statusBar.setTileSize(codec.getTileWidth(), codec.getTileHeight()); + } } } diff --git a/src/main/java/tm/ui/TMView.java b/src/main/java/tm/ui/TMView.java index 59ca5c9..70755f9 100644 --- a/src/main/java/tm/ui/TMView.java +++ b/src/main/java/tm/ui/TMView.java @@ -110,7 +110,9 @@ public void internalFrameClosing(InternalFrameEvent e) { addComponentListener(new ComponentAdapter() { public void componentResized(ComponentEvent e) { - slider.setSize(slider.getWidth(), editorCanvas.getHeight()); + if (editorCanvas != null) { + slider.setSize(slider.getWidth(), editorCanvas.getHeight()); + } // slider.setSize(slider.getWidth(), // getHeight()-((BasicInternalFrameUI)getUI()).getNorthPane().getHeight()); } @@ -389,6 +391,8 @@ private void updateSlider() { /** * * Sets the size of the tile grid. + * + * Block dimensions are always independent of canvas size. * **/ @@ -402,9 +406,7 @@ else if (cols > 1024) else if (rows > 1024) rows = 1024; editorCanvas.setGridSize(cols, rows); - if (sizeBlockToCanvas) { - editorCanvas.setBlockDimensions(cols, rows); - } + // Block dimensions are always independent of canvas size updateSlider(); editorCanvas.unpackPixels(); setScale(getScale()); @@ -808,6 +810,7 @@ public void gotoBookmark(BookmarkItemNode bookmark) { editorCanvas.setBlockDimensions(bookmark.getBlockWidth(), bookmark.getBlockHeight()); sizeBlockToCanvas = bookmark.getSizeBlockToCanvas(); editorCanvas.setRowInterleaveBlocks(bookmark.getRowInterleaved()); + editorCanvas.setSwizzlePattern(bookmark.getSwizzlePattern()); editorCanvas.setMode(bookmark.getMode()); setAbsoluteOffset(bookmark.getOffset()); // editorCanvas.setPalette( @@ -837,6 +840,7 @@ public BookmarkItemNode createBookmark(String description) { getMode(), getPalIndex(), getTileCodec(), + getSwizzlePattern(), description); } @@ -953,20 +957,20 @@ public int getBlockHeight() { } /** - * - * Gets whether blocks should be row-interleaved. - * - **/ + * + * Gets whether blocks should be row-interleaved. + * + **/ public boolean getRowInterleaveBlocks() { return editorCanvas.getRowInterleaveBlocks(); } /** - * - * Sets whether blocks should be row-interleaved. - * - **/ + * + * Sets whether blocks should be row-interleaved. + * + **/ public void setRowInterleaveBlocks(boolean rowInterleaved) { editorCanvas.setRowInterleaveBlocks(rowInterleaved); @@ -975,6 +979,26 @@ public void setRowInterleaveBlocks(boolean rowInterleaved) { } /** + * + * Gets the current swizzle pattern. + * + **/ + + public String getSwizzlePattern() { + return editorCanvas.getSwizzlePattern(); + } + + /** + * + * Sets the swizzle pattern for tile rendering. + * + **/ + + public void setSwizzlePattern(String swizzlePattern) { + editorCanvas.setSwizzlePattern(swizzlePattern); + editorCanvas.unpackPixels(); + editorCanvas.repaint(); + } /** * * Sets whether the block size should follow the canvas size. * diff --git a/src/main/resources/languages/language.properties b/src/main/resources/languages/language.properties index c26e21c..01307e8 100644 --- a/src/main/resources/languages/language.properties +++ b/src/main/resources/languages/language.properties @@ -33,6 +33,16 @@ Tile_Grid = Tile Grid Pixel_Grid = Pixel Grid Dark_Mode = Dark Mode +# Swizzle settings +Swizzle = Swizzle +Swizzle_None = None +Swizzle_BC = BC Texture +Swizzle_PSP = PlayStation Portable +Swizzle_NDS = Nintendo DS +Swizzle_3DS = Nintendo 3DS +Swizzle_WII = Nintendo Wii +Swizzle_SWITCH = Nintendo Switch + # Edit Menu Edit = Edit @@ -203,6 +213,10 @@ Offset_Prompt = Offset: Size_Prompt = Size: Columns_Prompt = Columns: Rows_Prompt = Rows: +Tile_Width_Prompt = Tile Width (pixels): +Tile_Height_Prompt = Tile Height (pixels): +Tile_Size_Dialog_Title = Tile Size +Custom_Tile_Size = Custom Tile Size... # Other diff --git a/src/main/resources/languages/language_en_US.properties b/src/main/resources/languages/language_en_US.properties index c26e21c..a467365 100644 --- a/src/main/resources/languages/language_en_US.properties +++ b/src/main/resources/languages/language_en_US.properties @@ -33,7 +33,16 @@ Tile_Grid = Tile Grid Pixel_Grid = Pixel Grid Dark_Mode = Dark Mode -# Edit Menu +# Swizzle settings +Swizzle = Swizzle +Swizzle_None = None +Swizzle_BC = BC Texture +Swizzle_PSP = PlayStation Portable +Swizzle_NDS = Nintendo DS +Swizzle_3DS = Nintendo 3DS +Swizzle_WII = Nintendo Wii +Swizzle_SWITCH = Nintendo Switch +Swizzle_Custom = Custom... Edit = Edit Undo = Undo @@ -203,6 +212,10 @@ Offset_Prompt = Offset: Size_Prompt = Size: Columns_Prompt = Columns: Rows_Prompt = Rows: +Tile_Width_Prompt = Tile Width (pixels): +Tile_Height_Prompt = Tile Height (pixels): +Tile_Size_Dialog_Title = Tile Size +Custom_Tile_Size = Custom Tile Size... # Other diff --git a/tmspec.xml b/tmspec.xml index 91d70c5..c7b2de8 100644 --- a/tmspec.xml +++ b/tmspec.xml @@ -30,6 +30,10 @@ 000000990000009900CC6600000099990099009999CCCCCC666666FF666666FF66FFFF666666FFFF66FF66FFFFFFFFFF Enhanced Graphics Adapter (EGA) + + 000000111111222222333333444444555555666666777777888888999999AAAAAABBBBBBCCCCCCDDDDDDEEEEEEFFFFFF + Grayscale + @@ -42,12 +46,18 @@ 2bpp planar + + 2bpp planar reverse + 3bpp planar 4bpp planar + + 4bpp planar reverse + 5bpp planar @@ -65,12 +75,24 @@ 2bpp planar, composite + + 3bpp planar, composite (3x1bpp) + 3bpp planar, composite (2bpp+1bpp) + + 3bpp planar reverse, composite (2bpp+1bpp) + + + 4bpp planar, composite (4x1bpp) + 4bpp planar, composite (2x2bpp) + + 4bpp planar reverse, composite (2x2bpp) + 8bpp planar, composite (4x2bpp) @@ -206,31 +228,31 @@ FCEUltra Save States (*.fc?) - Genecyst/Kega/Gens Save States (*.gs?) - + + Sega Genesis CRAM Dump (*.bin) + NESticle Save States (*.st?) - ZSNES Save States (*.zs?) - + + Mesen SNES CGRAM Dump (*.dmp) + Tile Layer Pro palette (*.tpl) - Windows Palette (*.pal) - Raw palette file (*.col,*.pal) - + @@ -239,7 +261,7 @@ - +