diff --git a/docsrc/manual/scripting-capabilities.rst b/docsrc/manual/scripting-capabilities.rst index 0651f7d9..4e016f6d 100644 --- a/docsrc/manual/scripting-capabilities.rst +++ b/docsrc/manual/scripting-capabilities.rst @@ -1222,6 +1222,58 @@ All tileset functions are callable via the global ``map`` object. :returns: the pixel data :rtype: array +.. |describe-metatile-layer-order| + replace:: where ``0`` is the bottom layer, ``1`` is the middle layer, and ``2`` is the top layer. The default order is ``[0, 1, 2]`` + +.. |describe-metatile-layer-order-handling| + replace:: If no elements are provided the layer order will be reset to the default. Any layer not listed in the provided ``order`` will not be rendered. Any additional elements after the first 3 are ignored + +.. js:function:: map.getMetatileLayerOrder() + + Gets the order that metatile layers are rendered for the current layout, |describe-metatile-layer-order|. + + If you'd like to get the default metatile layer order for all layouts, see :js:func:`utility.getMetatileLayerOrder` instead. + + :returns: array of layers + :rtype: array + +.. js:function:: map.setMetatileLayerOrder(order) + + Sets the order that metatile layers are rendered for the current layout, |describe-metatile-layer-order|. + + |describe-metatile-layer-order-handling|. + + If you'd like to set the default metatile layer order for all layouts, see :js:func:`utility.setMetatileLayerOrder` instead. + + :param order: array of layers + :type order: array + +.. |describe-metatile-layer-opacity| + replace:: where the first element is the bottom layer, the second element is the middle layer, and the third element is the top layer. The default opacities are ``[1.0, 1.0, 1.0]`` + +.. |describe-metatile-layer-opacity-handling| + replace:: Any additional elements after the first 3 are ignored. Any elements not provided will be rendered with opacity ``1.0`` + +.. js:function:: map.getMetatileLayerOpacity() + + Gets the opacities that metatile layers are rendered with for the current layout, |describe-metatile-layer-opacity|. + + If you'd like to get the default metatile layer opacities for all layouts, see :js:func:`utility.getMetatileLayerOpacity` instead. + + :returns: array of opacities for each layer + :rtype: array + +.. js:function:: map.setMetatileLayerOpacity(opacities) + + Sets the opacities that metatile layers are rendered with for the current layout, |describe-metatile-layer-opacity|. + + |describe-metatile-layer-opacity-handling|. + + If you'd like to set the default metatile layer opacities for all layouts, see :js:func:`utility.setMetatileLayerOpacity` instead. + + :param opacities: array of opacities for each layer + :type opacities: array + Overlay Functions ^^^^^^^^^^^^^^^^^ @@ -1811,30 +1863,42 @@ All settings functions are callable via the global ``utility`` object. .. js:function:: utility.getMetatileLayerOrder() - Gets the order that metatile layers are rendered. + Gets the order that metatile layers are rendered by default, |describe-metatile-layer-order|. - :returns: array of layers. The bottom layer is represented as 0. + If you'd like to get the metatile layer order for only the current layout, see :js:func:`map.getMetatileLayerOrder` instead. + + :returns: array of layers :rtype: array .. js:function:: utility.setMetatileLayerOrder(order) - Sets the order that metatile layers are rendered. + Sets the order that metatile layers are rendered by default, |describe-metatile-layer-order|. - :param order: array of layers. The bottom layer is represented as 0. + |describe-metatile-layer-order-handling|. + + If you'd like to set the metatile layer order for only the current layout, see :js:func:`map.setMetatileLayerOrder` instead. + + :param order: array of layers :type order: array .. js:function:: utility.getMetatileLayerOpacity() - Gets the opacities that metatile layers are rendered with. + Gets the opacities that metatile layers are rendered with by default, |describe-metatile-layer-opacity|. - :returns: array of opacities for each layer. The bottom layer is the first element. + If you'd like to get the metatile layer opacities for only the current layout, see :js:func:`map.getMetatileLayerOpacity` instead. + + :returns: array of opacities for each layer :rtype: array .. js:function:: utility.setMetatileLayerOpacity(opacities) - Sets the opacities that metatile layers are rendered with. + Sets the opacities that metatile layers are rendered with by default, |describe-metatile-layer-opacity|. - :param opacities: array of opacities for each layer. The bottom layer is the first element. + |describe-metatile-layer-opacity-handling|. + + If you'd like to set the metatile layer opacities for only the current layout, see :js:func:`map.setMetatileLayerOpacity` instead. + + :param opacities: array of opacities for each layer :type opacities: array diff --git a/forms/metatileimageexporter.ui b/forms/metatileimageexporter.ui new file mode 100644 index 00000000..7c6bfe49 --- /dev/null +++ b/forms/metatileimageexporter.ui @@ -0,0 +1,526 @@ + + + MetatileImageExporter + + + + 0 + 0 + 649 + 601 + + + + Export Metatiles Image + + + true + + + + + + + 0 + 0 + + + + Qt::FocusPolicy::ClickFocus + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::Shape::NoFrame + + + true + + + + + 0 + 0 + 304 + 532 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Tilesets + + + + 6 + + + 6 + + + + + <html><head/><body><p>If checked, automatically update the metatile range to include the full secondary tileset.</p></body></html> + + + Secondary Tileset + + + + + + + false + + + false + + + + + + + false + + + false + + + + + + + <html><head/><body><p>If checked, automatically update the metatile range to include the full primary tileset.</p></body></html> + + + Primary Tileset + + + true + + + + + + + Qt::Orientation::Horizontal + + + + 1 + 20 + + + + + + + + + + + false + + + Metatile Range + + + + 6 + + + 6 + + + + + Start + + + + + + + <html><head/><body><p>The metatile ID to start the rendered image at.</p></body></html> + + + + + + + End + + + + + + + <html><head/><body><p>The metatile ID to end the rendered image at.</p></body></html> + + + + + + + + + + <html><head/><body><p>Each metatile consists of 3 layers of tiles. These layers can be toggled here by clicking the checkbox, or rearranged by clicking and dragging them up or down in the list.</p></body></html> + + + Layers + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Qt::ScrollBarPolicy::ScrollBarAlwaysOff + + + Qt::ScrollBarPolicy::ScrollBarAlwaysOff + + + QAbstractScrollArea::SizeAdjustPolicy::AdjustToContentsOnFirstShow + + + true + + + QAbstractItemView::DragDropMode::InternalMove + + + Qt::DropAction::MoveAction + + + QListView::ResizeMode::Adjust + + + Qt::AlignmentFlag::AlignVCenter + + + + + + + + + + Transparency + + + + + + <html><head/><body><p>If checked, transparent pixels in the image will be rendered with alpha of 0.</p></body></html> + + + Normal + + + + + + + <html><head/><body><p>If checked, transparent pixels in the image will be rendered as black. This is the default in-game behavior.</p></body></html> + + + Black + + + + + + + <html><head/><body><p>If checked, transparent pixels in the image will be rendered using the first color in tileset palette 0. This is the default behavior of the GBA.</p></body></html> + + + First palette color + + + + + + + + + + Miscellaneous + + + + 6 + + + 6 + + + + + <html><head/><body><p>If checked, display the placeholder tiles that are rendered for the unused layer in-game. For a given metatile only 2 of the 3 tile layers are used, and the 3rd layer is filled with these placeholder tiles. The unused layer and placeholder tile change depending on the metatile's layer type.</p></body></html> + + + Render placeholder metatiles + + + + + + + Width (metatiles) + + + + + + + <html><head/><body><p>Width of the output image in metatiles.</p></body></html> + + + + + + + Width (pixels) + + + + + + + <html><head/><body><p>Width of the output image in pixels. Automatically rounded up to a multiple of a metatile's pixel width.</p></body></html> + + + 16 + + + 128 + + + 16 + + + 128 + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 1 + + + + + + + + + + + + + + Reset + + + false + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + Close + + + false + + + + + + + Save + + + false + + + + + + + + + + + + Preview + + + + 6 + + + 6 + + + 6 + + + 6 + + + + + QAbstractScrollArea::SizeAdjustPolicy::AdjustToContents + + + true + + + Qt::AlignmentFlag::AlignCenter + + + + + 0 + 0 + 285 + 551 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + false + + + false + + + QAbstractScrollArea::SizeAdjustPolicy::AdjustIgnored + + + QGraphicsView::DragMode::NoDrag + + + + + + + + + + + + + + + NoScrollComboBox + QComboBox +
noscrollcombobox.h
+
+ + UIntSpinBox + QAbstractSpinBox +
uintspinbox.h
+
+ + UIntHexSpinBox + UIntSpinBox +
uintspinbox.h
+
+ + ReorderableListWidget + QListWidget +
metatileimageexporter.h
+
+
+ + +
diff --git a/forms/projectsettingseditor.ui b/forms/projectsettingseditor.ui index f732f335..f2284c07 100644 --- a/forms/projectsettingseditor.ui +++ b/forms/projectsettingseditor.ui @@ -852,7 +852,7 @@ 0 0 570 - 798 + 837 @@ -973,6 +973,27 @@ + + + + + + Metatile Selector Width + + + + + + + <html><head/><body><p>The width (in metatiles) of the metatile selectors on the Map tab and in the Tileset Editor.</p></body></html> + + + 1 + + + + + @@ -1873,16 +1894,6 @@ QComboBox
noscrollcombobox.h
- - NoScrollSpinBox - QSpinBox -
noscrollspinbox.h
-
- - NoScrollTextEdit - QTextEdit -
noscrolltextedit.h
-
UIntSpinBox QAbstractSpinBox @@ -1893,6 +1904,16 @@ UIntSpinBox
uintspinbox.h
+ + NoScrollSpinBox + QSpinBox +
noscrollspinbox.h
+
+ + NoScrollTextEdit + QTextEdit +
noscrolltextedit.h
+
diff --git a/forms/tileseteditor.ui b/forms/tileseteditor.ui index 6c696bf3..895fae0f 100644 --- a/forms/tileseteditor.ui +++ b/forms/tileseteditor.ui @@ -65,7 +65,7 @@ 0 0 - 239 + 253 659
@@ -555,7 +555,7 @@ 0 0 - 499 + 446 241 @@ -625,18 +625,42 @@ File + + + Export Tiles Image + + + + + + + Import Metatiles from Advance Map 1.92 + + + + + + + Import Tiles Image + + + + + + + Export Porytiles Layer Images... + + + + - - + + - - - - - - - + + +
@@ -685,16 +709,6 @@ Ctrl+S - - - Import Primary Tiles Image... - - - - - Import Secondary Tiles Image... - - Change Number of Metatiles... @@ -745,36 +759,6 @@ Ctrl+Y - - - Export Primary Tiles Image... - - - - - Export Secondary Tiles Image... - - - - - Import Primary Metatiles from Advance Map 1.92... - - - - - Import Secondary Metatiles from Advance Map 1.92... - - - - - Export Primary Metatiles Image... - - - - - Export Secondary Metatiles Image... - - Cut @@ -834,6 +818,51 @@ Show Raw Metatile Attributes + + + Primary... + + + + + Secondary... + + + + + Primary... + + + + + Secondary... + + + + + Primary... + + + + + Secondary... + + + + + Export Metatiles Image... + + + + + Primary... + + + + + Secondary... + + diff --git a/include/config.h b/include/config.h index 6e9bbfca..e929d64c 100644 --- a/include/config.h +++ b/include/config.h @@ -60,7 +60,8 @@ protected: static bool getConfigBool(const QString &key, const QString &value); static int getConfigInteger(const QString &key, const QString &value, int min = INT_MIN, int max = INT_MAX, int defaultValue = 0); static uint32_t getConfigUint32(const QString &key, const QString &value, uint32_t min = 0, uint32_t max = UINT_MAX, uint32_t defaultValue = 0); - static QColor getConfigColor(const QString &key, const QString &value, const QColor &defaultValue = Qt::black); + static QColor getConfigColor(const QString &key, const QString &value, const QColor &defaultValue = QColor(Qt::black)); + static QString toConfigColor(const QColor &color); QString m_root; QString m_filename; @@ -71,57 +72,7 @@ class PorymapConfig: public KeyValueConfigBase { public: PorymapConfig(); - virtual void reset() override { - setRoot(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); - this->recentProjects.clear(); - this->projectManuallyClosed = false; - this->reopenOnLaunch = true; - this->mapListTab = 0; - this->mapListEditGroupsEnabled = false; - this->mapListHideEmptyEnabled.clear(); - this->prettyCursors = true; - this->mirrorConnectingMaps = true; - this->showDiveEmergeMaps = false; - this->diveEmergeMapOpacity = 30; - this->diveMapOpacity = 15; - this->emergeMapOpacity = 15; - this->collisionOpacity = 50; - this->collisionZoom = 30; - this->metatilesZoom = 30; - this->tilesetEditorMetatilesZoom = 45; - this->tilesetEditorTilesZoom = 30; - this->showPlayerView = false; - this->showCursorTile = true; - this->showBorder = true; - this->showGrid = false; - this->showTilesetEditorMetatileGrid = false; - this->showTilesetEditorLayerGrid = true; - this->showTilesetEditorDivider = false; - this->showTilesetEditorRawAttributes = false; - this->monitorFiles = true; - this->tilesetCheckerboardFill = true; - this->newMapHeaderSectionExpanded = false; - this->theme = "default"; - this->wildMonChartTheme = ""; - this->textEditorOpenFolder = ""; - this->textEditorGotoLine = ""; - this->paletteEditorBitDepth = 24; - this->projectSettingsTab = 0; - this->scriptAutocompleteMode = ScriptAutocompleteMode::MapOnly; - this->warpBehaviorWarningDisabled = false; - this->eventDeleteWarningDisabled = false; - this->eventOverlayEnabled = false; - this->checkForUpdates = true; - this->lastUpdateCheckTime = QDateTime(); - this->lastUpdateCheckVersion = porymapVersion; - this->rateLimitTimes.clear(); - this->eventSelectionShapeMode = QGraphicsPixmapItem::MaskShape; - this->shownInGameReloadMessage = false; - this->gridSettings = GridSettings(); - this->statusBarLogTypes = { LogType::LOG_ERROR, LogType::LOG_WARN }; - this->applicationFont = QFont(); - this->mapListFont = PorymapConfig::defaultMapListFont(); - } + virtual void reset() override; void addRecentProject(QString project); void setRecentProjects(QStringList projects); QString getRecentProject(); @@ -357,7 +308,7 @@ public: this->prefabImportPrompted = false; this->tilesetsHaveCallback = true; this->tilesetsHaveIsCompressed = true; - this->setTransparentPixelsBlack = true; + this->transparencyColor = QColor(Qt::black); this->preserveMatchingOnlyData = false; this->filePaths.clear(); this->eventIconPaths.clear(); @@ -374,6 +325,7 @@ public: this->unusedTileSplit = 0x0000; this->maxEventsPerGroup = 255; this->forcedMajorVersion = 0; + this->metatileSelectorWidth = 8; this->globalConstantsFilepaths.clear(); this->globalConstants.clear(); this->identifiers.clear(); @@ -432,7 +384,7 @@ public: bool prefabImportPrompted; bool tilesetsHaveCallback; bool tilesetsHaveIsCompressed; - bool setTransparentPixelsBlack; + QColor transparencyColor; bool preserveMatchingOnlyData; int metatileAttributesSize; uint32_t metatileBehaviorMask; @@ -453,6 +405,7 @@ public: QList warpBehaviors; int maxEventsPerGroup; int forcedMajorVersion; + int metatileSelectorWidth; QStringList globalConstantsFilepaths; QMap globalConstants; diff --git a/include/core/map.h b/include/core/map.h index 1d460cf1..734d5945 100644 --- a/include/core/map.h +++ b/include/core/map.h @@ -51,10 +51,12 @@ public: void setLayoutId(const QString &layoutId) { m_layoutId = layoutId; } QString layoutId() const { return layout() ? layout()->id : m_layoutId; } - int getWidth() const; - int getHeight() const; - int getBorderWidth() const; - int getBorderHeight() const; + int getWidth() const { return m_layout ? m_layout->getWidth() : 0; } + int getHeight() const { return m_layout ? m_layout->getHeight() : 0; } + int getBorderWidth() const { return m_layout ? m_layout->getBorderWidth() : 0; } + int getBorderHeight() const { return m_layout ? m_layout->getBorderHeight() : 0; } + int pixelWidth() const { return m_layout ? m_layout->pixelWidth() : 0; } + int pixelHeight() const { return m_layout ? m_layout->pixelHeight() : 0; } void setHeader(const MapHeader &header) { *m_header = header; } MapHeader* header() const { return m_header; } @@ -133,9 +135,6 @@ private: QMap> m_events; QSet m_ownedEvents; // for memory management - QList m_metatileLayerOrder; - QList m_metatileLayerOpacity; - void trackConnection(MapConnection*); // MapConnections in 'ownedConnections' but not 'connections' persist in the edit history. diff --git a/include/core/mapconnection.h b/include/core/mapconnection.h index 5df071bd..ba7c95a8 100644 --- a/include/core/mapconnection.h +++ b/include/core/mapconnection.h @@ -42,7 +42,7 @@ public: MapConnection* createMirror(); QPixmap render() const; - QPoint relativePos(bool clipped = false) const; + QPoint relativePixelPos(bool clipped = false) const; static QPointer project; static const QMap oppositeDirections; diff --git a/include/core/maplayout.h b/include/core/maplayout.h index c7790ad3..9f073d20 100644 --- a/include/core/maplayout.h +++ b/include/core/maplayout.h @@ -17,7 +17,7 @@ class BorderMetatilesPixmapItem; class Layout : public QObject { Q_OBJECT public: - Layout() {} + Layout() {}; Layout(const Layout &other); static QString layoutConstantFromName(const QString &name); @@ -32,6 +32,9 @@ public: int height; int border_width; int border_height; + int pixelWidth() const { return this->width * Metatile::pixelWidth(); } + int pixelHeight() const { return this->height * Metatile::pixelHeight(); } + QSize pixelSize() const { return QSize(pixelWidth(), pixelHeight()); } QString border_path; QString blockdata_path; @@ -64,8 +67,25 @@ public: QSize borderDimensions; } lastCommitBlocks; // to track map changes - QList metatileLayerOrder; - QList metatileLayerOpacity; + void setMetatileLayerOrder(const QList &layerOrder) { m_metatileLayerOrder = layerOrder; } + const QList &metatileLayerOrder() const { + return !m_metatileLayerOrder.isEmpty() ? m_metatileLayerOrder : Layout::globalMetatileLayerOrder(); + } + static void setGlobalMetatileLayerOrder(const QList &layerOrder) { s_globalMetatileLayerOrder = layerOrder; } + static const QList &globalMetatileLayerOrder() { + static const QList defaultLayerOrder = {0, 1, 2}; + return !s_globalMetatileLayerOrder.isEmpty() ? s_globalMetatileLayerOrder : defaultLayerOrder; + } + + void setMetatileLayerOpacity(const QList &layerOpacity) { m_metatileLayerOpacity = layerOpacity; } + const QList &metatileLayerOpacity() const { + return !m_metatileLayerOpacity.isEmpty() ? m_metatileLayerOpacity : Layout::globalMetatileLayerOpacity(); + } + static void setGlobalMetatileLayerOpacity(const QList &layerOpacity) { s_globalMetatileLayerOpacity = layerOpacity; } + static const QList &globalMetatileLayerOpacity() { + static const QList defaultLayerOpacity = {1.0, 1.0, 1.0}; + return !s_globalMetatileLayerOpacity.isEmpty() ? s_globalMetatileLayerOpacity : defaultLayerOpacity; + } LayoutPixmapItem *layoutItem = nullptr; CollisionPixmapItem *collisionItem = nullptr; @@ -136,9 +156,8 @@ public: void _floodFillCollisionElevation(int x, int y, uint16_t collision, uint16_t elevation); void magicFillCollisionElevation(int x, int y, uint16_t collision, uint16_t elevation); - QPixmap render(bool ignoreCache = false, Layout *fromLayout = nullptr, QRect bounds = QRect(0, 0, -1, -1)); + QPixmap render(bool ignoreCache = false, Layout *fromLayout = nullptr, const QRect &bounds = QRect(0, 0, -1, -1)); QPixmap renderCollision(bool ignoreCache); - // QPixmap renderConnection(MapConnection, Layout *); QPixmap renderBorder(bool ignoreCache = false); QPixmap getLayoutItemPixmap(); @@ -147,6 +166,8 @@ public: void setCollisionItem(CollisionPixmapItem *item) { collisionItem = item; } void setBorderItem(BorderMetatilesPixmapItem *item) { borderItem = item; } + bool metatileIsValid(uint16_t metatileId) { return Tileset::metatileIsValid(metatileId, this->tileset_primary, this->tileset_secondary); } + private: void setNewDimensionsBlockdata(int newWidth, int newHeight); void setNewBorderDimensionsBlockdata(int newWidth, int newHeight); @@ -155,6 +176,11 @@ private: static int getBorderDrawDistance(int dimension, qreal minimum); + QList m_metatileLayerOrder; + QList m_metatileLayerOpacity; + static QList s_globalMetatileLayerOrder; + static QList s_globalMetatileLayerOpacity; + signals: void dimensionsChanged(const QSize &size); void needsRedrawing(); diff --git a/include/core/metatile.h b/include/core/metatile.h index a1c805d1..e35e1fa3 100644 --- a/include/core/metatile.h +++ b/include/core/metatile.h @@ -62,6 +62,14 @@ public: static void setLayout(Project*); static QString getMetatileIdString(uint16_t metatileId); static QString getMetatileIdStrings(const QList metatileIds); + static QString getLayerName(int layerNum); + + static constexpr int tileWidth() { return 2; } + static constexpr int tileHeight() { return 2; } + static constexpr int tilesPerLayer() { return Metatile::tileWidth() * Metatile::tileHeight(); } + static constexpr int pixelWidth() { return Metatile::tileWidth() * Tile::pixelWidth(); } + static constexpr int pixelHeight() { return Metatile::tileHeight() * Tile::pixelHeight(); } + static constexpr QSize pixelSize() { return QSize(pixelWidth(), pixelHeight()); } inline bool operator==(const Metatile &other) { return this->tiles == other.tiles && this->attributes == other.attributes; diff --git a/include/core/tile.h b/include/core/tile.h index 1ae7e23d..a13e9fc7 100644 --- a/include/core/tile.h +++ b/include/core/tile.h @@ -3,6 +3,7 @@ #define TILE_H #include +#include class Tile { @@ -24,6 +25,11 @@ public: static int getIndexInTileset(int); static const uint16_t maxValue; + + static constexpr int pixelWidth() { return 8; } + static constexpr int pixelHeight() { return 8; } + static constexpr QSize pixelSize() { return QSize(Tile::pixelWidth(), Tile::pixelHeight()); } + static constexpr int sizeInBytes() { return sizeof(uint16_t); } }; inline bool operator==(const Tile &a, const Tile &b) { diff --git a/include/core/tileset.h b/include/core/tileset.h index abb31ab1..be5bdace 100644 --- a/include/core/tileset.h +++ b/include/core/tileset.h @@ -30,14 +30,13 @@ public: QString metatile_attrs_label; QString metatile_attrs_path; QString tilesImagePath; - QImage tilesImage; QStringList palettePaths; - QList tiles; QHash metatileLabels; QList> palettes; QList> palettePreviews; + static QString stripPrefix(const QString &fullName); static Tileset* getMetatileTileset(int, Tileset*, Tileset*); static Tileset* getTileTileset(int, Tileset*, Tileset*); static Metatile* getMetatile(int, Tileset*, Tileset*); @@ -76,15 +75,28 @@ public: void setMetatiles(const QList &metatiles); void addMetatile(Metatile* metatile); - QList metatiles() const { return m_metatiles; } - Metatile* metatileAt(unsigned int i) const { return m_metatiles.at(i); } + const QList &metatiles() const { return m_metatiles; } + const Metatile* metatileAt(unsigned int i) const { return m_metatiles.at(i); } void clearMetatiles(); void resizeMetatiles(int newNumMetatiles); int numMetatiles() const { return m_metatiles.length(); } + int maxMetatiles() const; + + uint16_t firstMetatileId() const; + uint16_t lastMetatileId() const; + bool contains(uint16_t metatileId) const { return metatileId >= firstMetatileId() && metatileId <= lastMetatileId(); } + + int numTiles() const { return m_tiles.length(); } + int maxTiles() const; + + QImage tileImage(uint16_t tileId) const { return m_tiles.value(Tile::getIndexInTileset(tileId)); } private: QList m_metatiles; + + QList m_tiles; + QImage m_tilesImage; bool m_hasUnsavedTilesImage = false; }; diff --git a/include/core/utility.h b/include/core/utility.h index f7f5da61..41c1dcde 100644 --- a/include/core/utility.h +++ b/include/core/utility.h @@ -7,7 +7,7 @@ namespace Util { void numericalModeSort(QStringList &list); - int roundUp(int numToRound, int multiple); + int roundUpToMultiple(int numToRound, int multiple); QString toDefineCase(QString input); QString toHexString(uint32_t value, int minLength = 0); QString toHtmlParagraph(const QString &text); diff --git a/include/editor.h b/include/editor.h index 9de1bc94..fd9a809e 100644 --- a/include/editor.h +++ b/include/editor.h @@ -285,7 +285,6 @@ signals: void wildMonTableEdited(); void currentMetatilesSelectionChanged(); void mapRulerStatusChanged(const QString &); - void tilesetUpdated(QString); void gridToggled(bool); void editActionSet(EditAction newEditAction); }; diff --git a/include/mainwindow.h b/include/mainwindow.h index c28e78f3..813370b4 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -145,6 +145,10 @@ public: Q_INVOKABLE void setMetatileTiles(int metatileId, QJSValue tilesObj, int tileStart = 0, int tileEnd = -1, bool forceRedraw = true); Q_INVOKABLE void setMetatileTiles(int metatileId, int tileId, bool xflip, bool yflip, int palette, int tileStart = 0, int tileEnd = -1, bool forceRedraw = true); Q_INVOKABLE QJSValue getTilePixels(int tileId); + Q_INVOKABLE QList getMetatileLayerOrder() const; + Q_INVOKABLE void setMetatileLayerOrder(const QList &order); + Q_INVOKABLE QList getMetatileLayerOpacity() const; + Q_INVOKABLE void setMetatileLayerOpacity(const QList &opacities); Q_INVOKABLE QString getSong(); Q_INVOKABLE void setSong(QString song); Q_INVOKABLE QString getLocation(); diff --git a/include/project.h b/include/project.h index 45df8143..dd66c2e5 100644 --- a/include/project.h +++ b/include/project.h @@ -255,13 +255,17 @@ public: static QString getDynamicMapDefineName(); static QString getDynamicMapName(); static QString getEmptySpeciesName(); + static QMargins getPixelViewDistance(); static QMargins getMetatileViewDistance(); static int getNumTilesPrimary() { return num_tiles_primary; } static int getNumTilesTotal() { return num_tiles_total; } + static int getNumTilesSecondary() { return getNumTilesTotal() - getNumTilesPrimary(); } static int getNumMetatilesPrimary() { return num_metatiles_primary; } static int getNumMetatilesTotal() { return Block::getMaxMetatileId() + 1; } + static int getNumMetatilesSecondary() { return getNumMetatilesTotal() - getNumMetatilesPrimary(); } static int getNumPalettesPrimary(){ return num_pals_primary; } static int getNumPalettesTotal() { return num_pals_total; } + static int getNumPalettesSecondary() { return getNumPalettesTotal() - getNumPalettesPrimary(); } static QString getEmptyMapsecName(); static QString getMapGroupPrefix(); diff --git a/include/scripting.h b/include/scripting.h index b85e9e26..b2c43ed2 100644 --- a/include/scripting.h +++ b/include/scripting.h @@ -51,7 +51,7 @@ public: static void cb_MapResized(int oldWidth, int oldHeight, const QMargins &delta); static void cb_BorderResized(int oldWidth, int oldHeight, int newWidth, int newHeight); static void cb_MapShifted(int xDelta, int yDelta); - static void cb_TilesetUpdated(QString tilesetName); + static void cb_TilesetUpdated(const QString &tilesetName); static void cb_MainTabChanged(int oldTab, int newTab); static void cb_MapViewTabChanged(int oldTab, int newTab); static void cb_BorderVisibilityToggled(bool visible); diff --git a/include/scriptutility.h b/include/scriptutility.h index 78272eec..09bfaae6 100644 --- a/include/scriptutility.h +++ b/include/scriptutility.h @@ -38,9 +38,9 @@ public: Q_INVOKABLE bool getSmartPathsEnabled(); Q_INVOKABLE QList getCustomScripts(); Q_INVOKABLE QList getMetatileLayerOrder(); - Q_INVOKABLE void setMetatileLayerOrder(QList order); + Q_INVOKABLE void setMetatileLayerOrder(const QList &order); Q_INVOKABLE QList getMetatileLayerOpacity(); - Q_INVOKABLE void setMetatileLayerOpacity(QList order); + Q_INVOKABLE void setMetatileLayerOpacity(const QList &order); Q_INVOKABLE QList getMapNames(); Q_INVOKABLE QList getMapConstants(); Q_INVOKABLE QList getLayoutNames(); @@ -57,6 +57,8 @@ public: Q_INVOKABLE bool isPrimaryTileset(QString tilesetName); Q_INVOKABLE bool isSecondaryTileset(QString tilesetName); + static bool validateMetatileLayerOrder(const QList &order); + private: void callTimeoutFunction(QJSValue callback); void runMessageBox(QString text, QString informativeText, QString detailedText, QMessageBox::Icon icon); diff --git a/include/ui/checkeredbgscene.h b/include/ui/checkeredbgscene.h new file mode 100644 index 00000000..bf0452a7 --- /dev/null +++ b/include/ui/checkeredbgscene.h @@ -0,0 +1,39 @@ +#ifndef CHECKEREDBGSCENE_H +#define CHECKEREDBGSCENE_H + +#include + +// Custom scene that paints its background a gray checkered pattern. +// Additionally there is a definable "valid" area which will paint the checkerboard green inside. +class CheckeredBgScene : public QGraphicsScene { + Q_OBJECT + +public: + CheckeredBgScene(const QSize &gridSize, QObject *parent = nullptr) + : QGraphicsScene(parent), + gridSize(gridSize) + {}; + CheckeredBgScene(int width, int height, QObject *parent = nullptr) + : CheckeredBgScene(QSize(width, height), parent) + {}; + + void setValidRect(int x, int y, int width, int height) { + this->validRect = QRect(x * this->gridSize.width(), + y * this->gridSize.height(), + width * this->gridSize.width(), + height * this->gridSize.height()); + } + void setValidRect(const QRect &rect) { + this->validRect = rect; + } + QRect getValidRect() { return this->validRect; } + +protected: + void drawBackground(QPainter *painter, const QRectF &rect) override; + +private: + QSize gridSize; + QRect validRect; +}; + +#endif // CHECKEREDBGSCENE_H diff --git a/include/ui/connectionpixmapitem.h b/include/ui/connectionpixmapitem.h index 32e309f9..5106ba8f 100644 --- a/include/ui/connectionpixmapitem.h +++ b/include/ui/connectionpixmapitem.h @@ -2,6 +2,7 @@ #define CONNECTIONPIXMAPITEM_H #include "mapconnection.h" +#include "metatile.h" #include #include #include @@ -31,8 +32,8 @@ private: bool selected = false; unsigned actionId = 0; - static const int mWidth = 16; - static const int mHeight = 16; + static const int mWidth = Metatile::pixelWidth(); + static const int mHeight = Metatile::pixelHeight(); void updatePos(); void updateOrigin(); diff --git a/include/ui/imageproviders.h b/include/ui/imageproviders.h index 806ffd5b..17b823ce 100644 --- a/include/ui/imageproviders.h +++ b/include/ui/imageproviders.h @@ -6,13 +6,40 @@ #include #include +class Layout; + QImage getCollisionMetatileImage(Block); QImage getCollisionMetatileImage(int, int); -QImage getMetatileImage(uint16_t, Tileset*, Tileset*, const QList&, const QList&, bool useTruePalettes = false); -QImage getMetatileImage(Metatile*, Tileset*, Tileset*, const QList&, const QList&, bool useTruePalettes = false); + +QImage getMetatileImage(uint16_t, Layout*, bool useTruePalettes = false); +QImage getMetatileImage(Metatile*, Layout*, bool useTruePalettes = false); +QImage getMetatileImage(uint16_t, Tileset*, Tileset*, const QList&, const QList& = {}, bool useTruePalettes = false); +QImage getMetatileImage(Metatile*, Tileset*, Tileset*, const QList&, const QList& = {}, bool useTruePalettes = false); + +QImage getMetatileSheetImage(Layout *layout, int numMetatilesWIde, bool useTruePalettes = false); +QImage getMetatileSheetImage(Tileset *primaryTileset, + Tileset *secondaryTileset, + uint16_t metatileIdStart, + uint16_t metatileIdEnd, + int numMetatilesWIde, + const QList &layerOrder, + const QList &layerOpacity = {}, + const QSize &metatileSize = Metatile::pixelSize(), + bool useTruePalettes = false); +QImage getMetatileSheetImage(Tileset *primaryTileset, + Tileset *secondaryTileset, + int numMetatilesWide, + const QList &layerOrder, + const QList &layerOpacity = {}, + const QSize &metatileSize = Metatile::pixelSize(), + bool useTruePalettes = false); + + QImage getTileImage(uint16_t, Tileset*, Tileset*); QImage getPalettedTileImage(uint16_t, Tileset*, Tileset*, int, bool useTruePalettes = false); -QImage getGreyscaleTileImage(uint16_t tile, Tileset *primaryTileset, Tileset *secondaryTileset); +QImage getColoredTileImage(uint16_t tileId, Tileset *primaryTileset, Tileset *secondaryTileset, const QList &palette); +QImage getGreyscaleTileImage(uint16_t tileId, Tileset *primaryTileset, Tileset *secondaryTileset); + void flattenTo4bppImage(QImage * image); static QList greyscalePalette({ diff --git a/include/ui/mapimageexporter.h b/include/ui/mapimageexporter.h index 51c1afe3..789cf9b9 100644 --- a/include/ui/mapimageexporter.h +++ b/include/ui/mapimageexporter.h @@ -2,6 +2,7 @@ #define MAPIMAGEEXPORTER_H #include "project.h" +#include "checkeredbgscene.h" class QGifImage; @@ -52,7 +53,7 @@ private: Project *m_project = nullptr; Map *m_map = nullptr; Layout *m_layout = nullptr; - QGraphicsScene *m_scene = nullptr; + CheckeredBgScene *m_scene = nullptr; QGifImage *m_timelapseGifImage = nullptr; QBuffer *m_timelapseBuffer = nullptr; QMovie *m_timelapseMovie = nullptr; diff --git a/include/ui/mapruler.h b/include/ui/mapruler.h index 151492e3..160978cc 100644 --- a/include/ui/mapruler.h +++ b/include/ui/mapruler.h @@ -3,6 +3,7 @@ #include #include +#include "metatile.h" class MapRuler : public QGraphicsObject, private QLine @@ -63,8 +64,8 @@ private: QPoint snapToWithinBounds(QPoint pos) const; void updateGeometry(); void updateStatus(Qt::Corner corner); - int pixWidth() const { return width() * 16; } - int pixHeight() const { return height() * 16; } + int pixWidth() const { return width() * Metatile::pixelWidth(); } + int pixHeight() const { return height() * Metatile::pixelHeight(); } }; #endif // MAPRULER_H diff --git a/include/ui/metatileimageexporter.h b/include/ui/metatileimageexporter.h new file mode 100644 index 00000000..1d486bd4 --- /dev/null +++ b/include/ui/metatileimageexporter.h @@ -0,0 +1,103 @@ +#ifndef METATILEIMAGEEXPORTER_H +#define METATILEIMAGEEXPORTER_H + +#include +#include +#include +#include +#include +#include + +#include "config.h" +#include "checkeredbgscene.h" + +class Tileset; + +namespace Ui { +class MetatileImageExporter; +} + +class ReorderableListWidget : public QListWidget +{ + Q_OBJECT +public: + explicit ReorderableListWidget(QWidget *parent = nullptr) : QListWidget(parent) { + setDragEnabled(true); + setDragDropMode(QAbstractItemView::InternalMove); + setDefaultDropAction(Qt::MoveAction); + }; + +signals: + void reordered(); + +protected: + virtual void dropEvent(QDropEvent *event) override { + QListWidget::dropEvent(event); + if (event->isAccepted()) { + emit reordered(); + } + } +}; + +class MetatileImageExporter : public QDialog +{ + Q_OBJECT + +public: + struct Settings { + OrderedMap layerOrder = { + {2, true}, + {1, true}, + {0, true}, + }; + uint16_t metatileStart = 0; + uint16_t metatileEnd = 0xFFFF; + uint16_t numMetatilesWide = projectConfig.metatileSelectorWidth; + bool usePrimaryTileset = true; + bool useSecondaryTileset = false; + bool renderPlaceholders = false; + int transparencyMode = 0; + }; + + explicit MetatileImageExporter(QWidget *parent, Tileset *primaryTileset, Tileset *secondaryTileset, Settings *savedSettings = nullptr); + ~MetatileImageExporter(); + + bool saveImage(QString filepath = QString()); + QImage getImage(); + QString getDefaultFileName() const; + void applySettings(const Settings &settings); + void reset(); + +protected: + virtual void showEvent(QShowEvent *) override; + virtual void closeEvent(QCloseEvent *) override; + +private: + Ui::MetatileImageExporter *ui; + + Tileset *m_primaryTileset; + Tileset *m_secondaryTileset; + Settings *m_savedSettings; + + CheckeredBgScene *m_scene = nullptr; + QGraphicsPixmapItem *m_preview = nullptr; + bool m_previewUpdateQueued = false; + QList m_layerOrder; + ProjectConfig m_savedConfig; + QList m_transparencyButtons; + + void populate(const Settings &settings); + void updatePreview(); + void tryUpdatePreview(); + void queuePreviewUpdate(); + void tryEnforceMetatileRange(); + void syncPixelWidth(); + void syncMetatileWidth(); + void validateMetatileStart(); + void validateMetatileEnd(); + void updateMetatileRange(); + void copyRenderSettings(); + void restoreRenderSettings(); +}; + +#endif // METATILEIMAGEEXPORTER_H diff --git a/include/ui/metatilelayersitem.h b/include/ui/metatilelayersitem.h index 1ca27c1c..ceea3ff8 100644 --- a/include/ui/metatilelayersitem.h +++ b/include/ui/metatilelayersitem.h @@ -26,7 +26,7 @@ private: signals: void tileChanged(int, int); void selectedTilesChanged(QPoint, int, int); - void hoveredTileChanged(uint16_t); + void hoveredTileChanged(const Tile &tile); void hoveredTileCleared(); protected: void mousePressEvent(QGraphicsSceneMouseEvent*); diff --git a/include/ui/metatileselector.h b/include/ui/metatileselector.h index 7bb3f131..edc3bf77 100644 --- a/include/ui/metatileselector.h +++ b/include/ui/metatileselector.h @@ -31,33 +31,34 @@ struct MetatileSelection class MetatileSelector: public SelectablePixmapItem { Q_OBJECT public: - MetatileSelector(int numMetatilesWide, Layout *layout): SelectablePixmapItem(16, 16) { + MetatileSelector(int numMetatilesWide, Layout *layout) + : SelectablePixmapItem(Metatile::pixelSize()), + numMetatilesWide(qMax(numMetatilesWide, 1)) + { this->externalSelection = false; this->prefabSelection = false; - this->numMetatilesWide = numMetatilesWide; this->layout = layout; - this->primaryTileset = layout->tileset_primary; - this->secondaryTileset = layout->tileset_secondary; this->selection = MetatileSelection{}; this->cellPos = QPoint(-1, -1); setAcceptHoverEvents(true); } - QPoint getSelectionDimensions() override; + QPoint getSelectionDimensions() const override; void draw() override; + void refresh(); bool select(uint16_t metatile); void selectFromMap(uint16_t metatileId, uint16_t collision, uint16_t elevation); - void setTilesets(Tileset*, Tileset*); - MetatileSelection getMetatileSelection(); + MetatileSelection getMetatileSelection() const { return this->selection; } void setPrefabSelection(MetatileSelection selection); - void setExternalSelection(int, int, QList, QList>); - QPoint getMetatileIdCoordsOnWidget(uint16_t); + void setExternalSelection(int, int, const QList&, const QList>&); + QPoint getMetatileIdCoordsOnWidget(uint16_t metatileId) const; void setLayout(Layout *layout); bool isInternalSelection() const { return (!this->externalSelection && !this->prefabSelection); } - Tileset *primaryTileset; - Tileset *secondaryTileset; + Tileset *primaryTileset() const { return this->layout->tileset_primary; } + Tileset *secondaryTileset() const { return this->layout->tileset_secondary; } + protected: void mousePressEvent(QGraphicsSceneMouseEvent*) override; void mouseMoveEvent(QGraphicsSceneMouseEvent*) override; @@ -66,10 +67,10 @@ protected: void hoverLeaveEvent(QGraphicsSceneHoverEvent*) override; void drawSelection() override; private: + const int numMetatilesWide; QPixmap basePixmap; bool externalSelection; bool prefabSelection; - int numMetatilesWide; Layout *layout; int externalSelectionWidth; int externalSelectionHeight; @@ -80,8 +81,9 @@ private: void updateBasePixmap(); void updateSelectedMetatiles(); void updateExternalSelectedMetatiles(); - uint16_t getMetatileId(int x, int y) const; - QPoint getMetatileIdCoords(uint16_t); + uint16_t posToMetatileId(int x, int y, bool *ok = nullptr) const; + uint16_t posToMetatileId(const QPoint &pos, bool *ok = nullptr) const; + QPoint metatileIdToPos(uint16_t metatileId, bool *ok = nullptr) const; bool positionIsValid(const QPoint &pos) const; bool selectionIsValid(); void hoverChanged(); diff --git a/include/ui/movablerect.h b/include/ui/movablerect.h index 552ad6f5..7940e592 100644 --- a/include/ui/movablerect.h +++ b/include/ui/movablerect.h @@ -10,7 +10,7 @@ class MovableRect : public QGraphicsRectItem { public: - MovableRect(const QRectF &rect, const QRgb &color); + MovableRect(const QRectF &rect, const QSize &cellSize, const QRgb &color); QRectF boundingRect() const override { qreal penWidth = 4; return QRectF(-penWidth, @@ -31,6 +31,7 @@ public: protected: QRectF baseRect; + QSize cellSize; QRgb color; void updateVisibility(); @@ -43,7 +44,7 @@ class ResizableRect : public QObject, public MovableRect { Q_OBJECT public: - ResizableRect(QObject *parent, int width, int height, QRgb color); + ResizableRect(QObject *parent, const QSize &cellSize, const QSize &size, const QRgb &color); QRectF boundingRect() const override { return QRectF(this->rect() + QMargins(lineWidth, lineWidth, lineWidth, lineWidth)); diff --git a/include/ui/resizelayoutpopup.h b/include/ui/resizelayoutpopup.h index 89ce2e99..b187044f 100644 --- a/include/ui/resizelayoutpopup.h +++ b/include/ui/resizelayoutpopup.h @@ -3,10 +3,10 @@ #include "maplayout.h" #include "project.h" +#include "checkeredbgscene.h" #include #include -#include #include #include #include @@ -16,39 +16,12 @@ namespace Ui { class ResizeLayoutPopup; } - - -/// Custom scene that paints its background a gray checkered pattern. -/// Additionally there is a definable "valid" area which will paint the checkerboard green inside. -class CheckeredBgScene : public QGraphicsScene { - Q_OBJECT - -public: - CheckeredBgScene(QObject *parent = nullptr); - void setValidRect(int x, int y, int width, int height) { - this->validRect = QRect(x * this->gridSize, y * this->gridSize, width * this->gridSize, height * this->gridSize); - } - void setValidRect(QRect rect) { - this->validRect = rect; - } - QRect getValidRect() { return this->validRect; } - -protected: - void drawBackground(QPainter *painter, const QRectF &rect) override; - -private: - int gridSize = 16; // virtual pixels - QRect validRect = QRect(); -}; - - - /// PixmapItem subclass which allows for creating a boundary which determine whether /// the pixmap paints normally or with a black tint. -/// This item is movable and snaps on a 16x16 grid. +/// This item is movable and snaps on a 'cellSize' grid. class BoundedPixmapItem : public QGraphicsPixmapItem { public: - BoundedPixmapItem(const QPixmap &pixmap, QGraphicsItem *parent = nullptr); + BoundedPixmapItem(const QPixmap &pixmap, const QSize &cellSize, QGraphicsItem *parent = nullptr); void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override; void setBoundary(ResizableRect *rect) { this->boundary = rect; } @@ -59,6 +32,7 @@ protected: private: ResizableRect *boundary = nullptr; QPointF clickedPos = QPointF(); + QSize cellSize; }; diff --git a/include/ui/selectablepixmapitem.h b/include/ui/selectablepixmapitem.h index 2681e485..8323f7e1 100644 --- a/include/ui/selectablepixmapitem.h +++ b/include/ui/selectablepixmapitem.h @@ -7,14 +7,19 @@ class SelectablePixmapItem : public QObject, public QGraphicsPixmapItem { Q_OBJECT public: - SelectablePixmapItem(int cellWidth, int cellHeight): SelectablePixmapItem(cellWidth, cellHeight, INT_MAX, INT_MAX) {} - SelectablePixmapItem(int cellWidth, int cellHeight, int maxSelectionWidth, int maxSelectionHeight) { - this->cellWidth = cellWidth; - this->cellHeight = cellHeight; - this->maxSelectionWidth = maxSelectionWidth; - this->maxSelectionHeight = maxSelectionHeight; - } - virtual QPoint getSelectionDimensions(); + SelectablePixmapItem(const QSize &size, const QSize &maxSelectionSize = QSize(INT_MAX, INT_MAX)) + : SelectablePixmapItem(size.width(), size.height(), maxSelectionSize.width(), maxSelectionSize.height()) {} + SelectablePixmapItem(int cellWidth, int cellHeight, int maxSelectionWidth = INT_MAX, int maxSelectionHeight = INT_MAX) + : cellWidth(cellWidth), + cellHeight(cellHeight), + maxSelectionWidth(maxSelectionWidth), + maxSelectionHeight(maxSelectionHeight), + selectionInitialX(0), + selectionInitialY(0), + selectionOffsetX(0), + selectionOffsetY(0) + {} + virtual QPoint getSelectionDimensions() const; virtual void draw() = 0; protected: @@ -28,7 +33,8 @@ protected: int selectionOffsetY; QPoint getSelectionStart(); - void select(int, int, int, int); + void select(int x, int y, int width = 0, int height = 0); + void select(const QPoint &pos, const QSize &size = QSize(0,0)) { select(pos.x(), pos.y(), size.width(), size.height()); } void updateSelection(int, int); QPoint getCellPos(QPointF); virtual void mousePressEvent(QGraphicsSceneMouseEvent*) override; diff --git a/include/ui/tileseteditor.h b/include/ui/tileseteditor.h index b6a60a61..dc5b935a 100644 --- a/include/ui/tileseteditor.h +++ b/include/ui/tileseteditor.h @@ -8,6 +8,7 @@ #include "tileseteditormetatileselector.h" #include "tileseteditortileselector.h" #include "metatilelayersitem.h" +#include "metatileimageexporter.h" class NoScrollComboBox; class Layout; @@ -61,6 +62,7 @@ private slots: void onWindowActivated(); void onHoveredMetatileChanged(uint16_t); void onHoveredMetatileCleared(); + void onHoveredTileChanged(const Tile&); void onHoveredTileChanged(uint16_t); void onHoveredTileCleared(); void onSelectedTilesChanged(); @@ -71,10 +73,6 @@ private slots: void on_spinBox_paletteSelector_valueChanged(int arg1); - void on_actionImport_Primary_Tiles_triggered(); - - void on_actionImport_Secondary_Tiles_triggered(); - void on_actionChange_Metatiles_Count_triggered(); void on_actionChange_Palettes_triggered(); @@ -91,14 +89,6 @@ private slots: void on_lineEdit_metatileLabel_editingFinished(); - void on_actionExport_Primary_Tiles_Image_triggered(); - void on_actionExport_Secondary_Tiles_Image_triggered(); - void on_actionExport_Primary_Metatiles_Image_triggered(); - void on_actionExport_Secondary_Metatiles_Image_triggered(); - - void on_actionImport_Primary_Metatiles_triggered(); - void on_actionImport_Secondary_Metatiles_triggered(); - void on_copyButton_metatileLabel_clicked(); void on_actionCut_triggered(); @@ -122,8 +112,11 @@ private: void drawSelectedTiles(); void redrawTileSelector(); void redrawMetatileSelector(); - void importTilesetTiles(Tileset*, bool); - void importTilesetMetatiles(Tileset*, bool); + void importTilesetTiles(Tileset*); + void importAdvanceMapMetatiles(Tileset*); + void exportTilesImage(Tileset*); + void exportPorytilesLayerImages(Tileset*); + void exportMetatilesImage(); void refresh(); void commitMetatileLabel(); void closeEvent(QCloseEvent*); @@ -170,6 +163,7 @@ private: QGraphicsScene *metatileLayersScene = nullptr; bool lockSelection = false; QSet metatileReloadQueue; + MetatileImageExporter::Settings *metatileImageExportSettings = nullptr; bool save(); diff --git a/include/ui/tileseteditormetatileselector.h b/include/ui/tileseteditormetatileselector.h index 8a3fdfa7..6ca9088f 100644 --- a/include/ui/tileseteditormetatileselector.h +++ b/include/ui/tileseteditormetatileselector.h @@ -9,7 +9,7 @@ class Layout; class TilesetEditorMetatileSelector: public SelectablePixmapItem { Q_OBJECT public: - TilesetEditorMetatileSelector(Tileset *primaryTileset, Tileset *secondaryTileset, Layout *layout); + TilesetEditorMetatileSelector(int numMetatilesWide, Tileset *primaryTileset, Tileset *secondaryTileset, Layout *layout); Layout *layout = nullptr; void draw() override; @@ -18,11 +18,9 @@ public: bool select(uint16_t metatileId); void setTilesets(Tileset*, Tileset*); - uint16_t getSelectedMetatileId(); + uint16_t getSelectedMetatileId() const { return this->selectedMetatileId; } void updateSelectedMetatile(); - QPoint getMetatileIdCoordsOnWidget(uint16_t metatileId); - QImage buildPrimaryMetatilesImage(); - QImage buildSecondaryMetatilesImage(); + QPoint getMetatileIdCoordsOnWidget(uint16_t metatileId) const; QVector usedMetatiles; bool selectorShowUnused = false; @@ -38,26 +36,24 @@ protected: void hoverLeaveEvent(QGraphicsSceneHoverEvent*) override; private: + const int numMetatilesWide; QImage baseImage; QPixmap basePixmap; Tileset *primaryTileset = nullptr; Tileset *secondaryTileset = nullptr; uint16_t selectedMetatileId; - int numMetatilesWide; - int numMetatilesHigh; void updateBasePixmap(); - uint16_t getMetatileId(int x, int y); - QPoint getMetatileIdCoords(uint16_t); + uint16_t posToMetatileId(int x, int y, bool *ok = nullptr) const; + uint16_t posToMetatileId(const QPoint &pos, bool *ok = nullptr) const; + QPoint metatileIdToPos(uint16_t metatileId, bool *ok = nullptr) const; bool shouldAcceptEvent(QGraphicsSceneMouseEvent*); - int numRows(int numMetatiles); - int numRows(); + int numRows(int numMetatiles) const; + int numRows() const; void drawGrid(); void drawDivider(); void drawFilters(); void drawUnused(); void drawCounts(); - QImage buildAllMetatilesImage(); - QImage buildImage(int metatileIdStart, int numMetatiles); int numPrimaryMetatilesRounded() const; signals: diff --git a/include/ui/tileseteditortileselector.h b/include/ui/tileseteditortileselector.h index aa2a1923..0ad22b97 100644 --- a/include/ui/tileseteditortileselector.h +++ b/include/ui/tileseteditortileselector.h @@ -8,7 +8,7 @@ class TilesetEditorTileSelector: public SelectablePixmapItem { Q_OBJECT public: TilesetEditorTileSelector(Tileset *primaryTileset, Tileset *secondaryTileset, int numLayers) - : SelectablePixmapItem(16, 16, numLayers * 2, 2) { + : SelectablePixmapItem(16, 16, numLayers * Metatile::tileWidth(), Metatile::tileHeight()) { this->primaryTileset = primaryTileset; this->secondaryTileset = secondaryTileset; this->numTilesWide = 16; @@ -18,8 +18,8 @@ public: this->paletteChanged = false; setAcceptHoverEvents(true); } - QPoint getSelectionDimensions(); - void draw(); + QPoint getSelectionDimensions() const override; + void draw() override; void select(uint16_t metatileId); void highlight(uint16_t metatileId); void setTilesets(Tileset*, Tileset*); @@ -36,13 +36,14 @@ public: bool showDivider = false; protected: - void mousePressEvent(QGraphicsSceneMouseEvent*); - void mouseMoveEvent(QGraphicsSceneMouseEvent*); - void mouseReleaseEvent(QGraphicsSceneMouseEvent*); - void hoverMoveEvent(QGraphicsSceneHoverEvent*); - void hoverLeaveEvent(QGraphicsSceneHoverEvent*); + void mousePressEvent(QGraphicsSceneMouseEvent*) override; + void mouseMoveEvent(QGraphicsSceneMouseEvent*) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent*) override; + void hoverMoveEvent(QGraphicsSceneHoverEvent*) override; + void hoverLeaveEvent(QGraphicsSceneHoverEvent*) override; private: + QPixmap basePixmap; bool externalSelection; int externalSelectionWidth; int externalSelectionHeight; @@ -63,7 +64,7 @@ private: QList getCurPaletteTable(); QList buildSelectedTiles(int, int, QList); QImage buildImage(int tileIdStart, int numTiles); - + void updateBasePixmap(); void drawUnused(); signals: diff --git a/include/ui/uintspinbox.h b/include/ui/uintspinbox.h index bc217ec2..aa8bcb77 100644 --- a/include/ui/uintspinbox.h +++ b/include/ui/uintspinbox.h @@ -21,6 +21,7 @@ public: uint32_t value() const { return m_value; } uint32_t minimum() const { return m_minimum; } uint32_t maximum() const { return m_maximum; } + uint32_t singleStep() const { return m_singleStep; } QString prefix() const { return m_prefix; } int displayIntegerBase() const { return m_displayIntegerBase; } bool hasPadding() const { return m_hasPadding; } @@ -28,6 +29,7 @@ public: void setMinimum(uint32_t min); void setMaximum(uint32_t max); void setRange(uint32_t min, uint32_t max); + void setSingleStep(uint32_t val); void setPrefix(const QString &prefix); void setDisplayIntegerBase(int base); void setHasPadding(bool enabled); @@ -36,6 +38,7 @@ private: uint32_t m_minimum; uint32_t m_maximum; uint32_t m_value; + uint32_t m_singleStep; QString m_prefix; int m_displayIntegerBase; bool m_hasPadding; diff --git a/porymap.pro b/porymap.pro index 4045e230..9c6327b7 100644 --- a/porymap.pro +++ b/porymap.pro @@ -66,6 +66,7 @@ SOURCES += src/core/advancemapparser.cpp \ src/scriptapi/apiutility.cpp \ src/scriptapi/scripting.cpp \ src/ui/aboutporymap.cpp \ + src/ui/checkeredbgscene.cpp \ src/ui/colorinputwidget.cpp \ src/ui/connectionslistitem.cpp \ src/ui/customattributesdialog.cpp \ @@ -124,6 +125,7 @@ SOURCES += src/core/advancemapparser.cpp \ src/ui/regionmapeditor.cpp \ src/ui/newmapdialog.cpp \ src/ui/mapimageexporter.cpp \ + src/ui/metatileimageexporter.cpp \ src/ui/newtilesetdialog.cpp \ src/ui/flowlayout.cpp \ src/ui/mapruler.cpp \ @@ -180,6 +182,7 @@ HEADERS += include/core/advancemapparser.h \ include/lib/orderedmap.h \ include/lib/orderedjson.h \ include/ui/aboutporymap.h \ + include/ui/checkeredbgscene.h \ include/ui/connectionslistitem.h \ include/ui/customattributesdialog.h \ include/ui/customattributestable.h \ @@ -240,6 +243,7 @@ HEADERS += include/core/advancemapparser.h \ include/ui/regionmapeditor.h \ include/ui/newmapdialog.h \ include/ui/mapimageexporter.h \ + include/ui/metatileimageexporter.h \ include/ui/newtilesetdialog.h \ include/ui/overlay.h \ include/ui/flowlayout.h \ @@ -289,6 +293,7 @@ FORMS += forms/mainwindow.ui \ forms/aboutporymap.ui \ forms/newtilesetdialog.ui \ forms/mapimageexporter.ui \ + forms/metatileimageexporter.ui \ forms/shortcutseditor.ui \ forms/preferenceeditor.ui \ forms/regionmappropertiesdialog.ui \ diff --git a/src/config.cpp b/src/config.cpp index 666ec12c..d5c5b5c9 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -4,6 +4,7 @@ #include "map.h" #include "validator.h" #include "utility.h" +#include "metatile.h" #include #include #include @@ -283,7 +284,7 @@ int KeyValueConfigBase::getConfigInteger(const QString &key, const QString &valu logWarn(QString("Invalid config value for %1: '%2'. Must be an integer. Using default value '%3'.").arg(key).arg(value).arg(defaultValue)); result = defaultValue; } - return qMin(max, qMax(min, result)); + return qBound(min, result, max); } uint32_t KeyValueConfigBase::getConfigUint32(const QString &key, const QString &value, uint32_t min, uint32_t max, uint32_t defaultValue) { @@ -293,10 +294,13 @@ uint32_t KeyValueConfigBase::getConfigUint32(const QString &key, const QString & logWarn(QString("Invalid config value for %1: '%2'. Must be an integer. Using default value '%3'.").arg(key).arg(value).arg(defaultValue)); result = defaultValue; } - return qMin(max, qMax(min, result)); + return qBound(min, result, max); } QColor KeyValueConfigBase::getConfigColor(const QString &key, const QString &value, const QColor &defaultValue) { + if (value.isEmpty()) + return QColor(); + QColor color = QColor("#" + value); if (!color.isValid()) { logWarn(QString("Invalid config value for %1: '%2'. Must be a color in the format 'RRGGBB'. Using default value '%3'.").arg(key).arg(value).arg(defaultValue.name())); @@ -305,12 +309,70 @@ QColor KeyValueConfigBase::getConfigColor(const QString &key, const QString &val return color; } +QString KeyValueConfigBase::toConfigColor(const QColor &color) { + return color.isValid() ? color.name().remove("#") : QString(); // Our text config treats '#' as the start of a comment. +} + PorymapConfig porymapConfig; PorymapConfig::PorymapConfig() : KeyValueConfigBase(QStringLiteral("porymap.cfg")) { reset(); } +void PorymapConfig::reset() { + setRoot(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); + this->recentProjects.clear(); + this->projectManuallyClosed = false; + this->reopenOnLaunch = true; + this->mapListTab = 0; + this->mapListEditGroupsEnabled = false; + this->mapListHideEmptyEnabled.clear(); + this->prettyCursors = true; + this->mirrorConnectingMaps = true; + this->showDiveEmergeMaps = false; + this->diveEmergeMapOpacity = 30; + this->diveMapOpacity = 15; + this->emergeMapOpacity = 15; + this->collisionOpacity = 50; + this->collisionZoom = 30; + this->metatilesZoom = 30; + this->tilesetEditorMetatilesZoom = 30; + this->tilesetEditorTilesZoom = 30; + this->showPlayerView = false; + this->showCursorTile = true; + this->showBorder = true; + this->showGrid = false; + this->showTilesetEditorMetatileGrid = false; + this->showTilesetEditorLayerGrid = true; + this->showTilesetEditorDivider = false; + this->showTilesetEditorRawAttributes = false; + this->monitorFiles = true; + this->tilesetCheckerboardFill = true; + this->newMapHeaderSectionExpanded = false; + this->theme = "default"; + this->wildMonChartTheme = ""; + this->textEditorOpenFolder = ""; + this->textEditorGotoLine = ""; + this->paletteEditorBitDepth = 24; + this->projectSettingsTab = 0; + this->scriptAutocompleteMode = ScriptAutocompleteMode::MapOnly; + this->warpBehaviorWarningDisabled = false; + this->eventDeleteWarningDisabled = false; + this->eventOverlayEnabled = false; + this->checkForUpdates = true; + this->lastUpdateCheckTime = QDateTime(); + this->lastUpdateCheckVersion = porymapVersion; + this->rateLimitTimes.clear(); + this->eventSelectionShapeMode = QGraphicsPixmapItem::MaskShape; + this->shownInGameReloadMessage = false; + this->gridSettings = GridSettings(); + this->gridSettings.width = Metatile::pixelWidth(); + this->gridSettings.height = Metatile::pixelHeight(); + this->statusBarLogTypes = { LogType::LOG_ERROR, LogType::LOG_WARN }; + this->applicationFont = QFont(); + this->mapListFont = PorymapConfig::defaultMapListFont(); +} + void PorymapConfig::parseConfigKeyValue(QString key, QString value) { if (key == "recent_project") { this->recentProjects = value.split(",", Qt::SkipEmptyParts); @@ -576,7 +638,7 @@ QMap PorymapConfig::getKeyValueMap() { map.insert("grid_x", QString::number(this->gridSettings.offsetX)); map.insert("grid_y", QString::number(this->gridSettings.offsetY)); map.insert("grid_style", GridSettings::getStyleName(this->gridSettings.style)); - map.insert("grid_color", this->gridSettings.color.name().remove("#")); // Our text config treats '#' as the start of a comment. + map.insert("grid_color", toConfigColor(this->gridSettings.color)); QStringList logTypesStrings; for (const auto &type : this->statusBarLogTypes) { @@ -903,8 +965,8 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) { this->tilesetsHaveCallback = getConfigBool(key, value); } else if (key == "tilesets_have_is_compressed") { this->tilesetsHaveIsCompressed = getConfigBool(key, value); - } else if (key == "set_transparent_pixels_black") { - this->setTransparentPixelsBlack = getConfigBool(key, value); + } else if (key == "transparency_color") { + this->transparencyColor = getConfigColor(key, value); } else if (key == "preserve_matching_only_data") { this->preserveMatchingOnlyData = getConfigBool(key, value); } else if (key == "event_icon_path_object") { @@ -945,6 +1007,8 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) { this->maxEventsPerGroup = getConfigInteger(key, value, 1, INT_MAX, 255); } else if (key == "forced_major_version") { this->forcedMajorVersion = getConfigInteger(key, value); + } else if (key == "metatile_selector_width") { + this->metatileSelectorWidth = getConfigInteger(key, value, 1, INT_MAX, 8); } else { logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->filepath()).arg(key)); } @@ -1010,7 +1074,7 @@ QMap ProjectConfig::getKeyValueMap() { } map.insert("tilesets_have_callback", QString::number(this->tilesetsHaveCallback)); map.insert("tilesets_have_is_compressed", QString::number(this->tilesetsHaveIsCompressed)); - map.insert("set_transparent_pixels_black", QString::number(this->setTransparentPixelsBlack)); + map.insert("transparency_color", toConfigColor(this->transparencyColor)); map.insert("preserve_matching_only_data", QString::number(this->preserveMatchingOnlyData)); map.insert("metatile_attributes_size", QString::number(this->metatileAttributesSize)); map.insert("metatile_behavior_mask", Util::toHexString(this->metatileBehaviorMask)); @@ -1054,6 +1118,7 @@ QMap ProjectConfig::getKeyValueMap() { map.insert("warp_behaviors", warpBehaviorStrs.join(",")); map.insert("max_events_per_group", QString::number(this->maxEventsPerGroup)); map.insert("forced_major_version", QString::number(this->forcedMajorVersion)); + map.insert("metatile_selector_width", QString::number(this->metatileSelectorWidth)); return map; } @@ -1187,7 +1252,7 @@ int ProjectConfig::getNumLayersInMetatile() { } int ProjectConfig::getNumTilesInMetatile() { - return this->tripleLayerMetatilesEnabled ? 12 : 8; + return getNumLayersInMetatile() * Metatile::tilesPerLayer(); } void ProjectConfig::setEventIconPath(Event::Group group, const QString &path) { diff --git a/src/core/advancemapparser.cpp b/src/core/advancemapparser.cpp index 6a8428ec..2e4ef7cd 100644 --- a/src/core/advancemapparser.cpp +++ b/src/core/advancemapparser.cpp @@ -131,7 +131,7 @@ QList AdvanceMapParser::parseMetatiles(const QString &filepath, bool } int attrSize = Metatile::getDefaultAttributesSize(version); - int maxMetatiles = primaryTileset ? Project::getNumMetatilesPrimary() : Project::getNumMetatilesTotal() - Project::getNumMetatilesPrimary(); + int maxMetatiles = primaryTileset ? Project::getNumMetatilesPrimary() : Project::getNumMetatilesSecondary(); int numMetatiles = static_cast(in.at(0)) | (static_cast(in.at(1)) << 8) | (static_cast(in.at(2)) << 16) | diff --git a/src/core/map.cpp b/src/core/map.cpp index 6110a26f..079f336a 100644 --- a/src/core/map.cpp +++ b/src/core/map.cpp @@ -30,8 +30,6 @@ Map::Map(const Map &other, QObject *parent) : Map(parent) { *m_header = *other.m_header; m_layout = other.m_layout; m_isPersistedToFile = false; - m_metatileLayerOrder = other.m_metatileLayerOrder; - m_metatileLayerOpacity = other.m_metatileLayerOpacity; // Copy events for (auto i = other.m_events.constBegin(); i != other.m_events.constEnd(); i++) { @@ -64,44 +62,28 @@ QString Map::mapConstantFromName(const QString &name) { return projectConfig.getIdentifier(ProjectIdentifier::define_map_prefix) + Util::toDefineCase(name); } -int Map::getWidth() const { - return m_layout ? m_layout->getWidth() : 0; -} - -int Map::getHeight() const { - return m_layout ? m_layout->getHeight() : 0; -} - -int Map::getBorderWidth() const { - return m_layout ? m_layout->getBorderWidth() : 0; -} - -int Map::getBorderHeight() const { - return m_layout ? m_layout->getBorderHeight() : 0; -} - // Get the portion of the map that can be rendered when rendered as a map connection. // Cardinal connections render the nearest segment of their map and within the bounds of the border draw distance, // Dive/Emerge connections are rendered normally within the bounds of their parent map. QRect Map::getConnectionRect(const QString &direction, Layout * fromLayout) const { int x = 0, y = 0; - int w = getWidth(), h = getHeight(); + int w = pixelWidth(), h = pixelHeight(); - QMargins viewDistance = Project::getMetatileViewDistance(); + QMargins viewDistance = Project::getPixelViewDistance(); if (direction == "up") { h = qMin(h, viewDistance.top()); - y = getHeight() - h; + y = pixelHeight() - h; } else if (direction == "down") { h = qMin(h, viewDistance.bottom()); } else if (direction == "left") { w = qMin(w, viewDistance.left()); - x = getWidth() - w; + x = pixelWidth() - w; } else if (direction == "right") { w = qMin(w, viewDistance.right()); } else if (MapConnection::isDiving(direction)) { if (fromLayout) { - w = qMin(w, fromLayout->getWidth()); - h = qMin(h, fromLayout->getHeight()); + w = qMin(w, fromLayout->pixelWidth()); + h = qMin(h, fromLayout->pixelHeight()); } } else { // Unknown direction @@ -124,7 +106,7 @@ QPixmap Map::renderConnection(const QString &direction, Layout * fromLayout) { fromLayout = nullptr; QPixmap connectionPixmap = m_layout->render(true, fromLayout, bounds); - return connectionPixmap.copy(bounds.x() * 16, bounds.y() * 16, bounds.width() * 16, bounds.height() * 16); + return connectionPixmap.copy(bounds); } void Map::openScript(const QString &label) { diff --git a/src/core/mapconnection.cpp b/src/core/mapconnection.cpp index d37a8c74..e1478fad 100644 --- a/src/core/mapconnection.cpp +++ b/src/core/mapconnection.cpp @@ -72,20 +72,20 @@ QPixmap MapConnection::render() const { // For right/down connections this is offset by the dimensions of the parent map. // For left/up connections this is offset by the dimensions of the target map. // If 'clipped' is true, only the rendered dimensions of the target map will be used, rather than its full dimensions. -QPoint MapConnection::relativePos(bool clipped) const { +QPoint MapConnection::relativePixelPos(bool clipped) const { int x = 0, y = 0; if (m_direction == "right") { - if (m_parentMap) x = m_parentMap->getWidth(); - y = m_offset; + if (m_parentMap) x = m_parentMap->pixelWidth(); + y = m_offset * Metatile::pixelHeight(); } else if (m_direction == "down") { - x = m_offset; - if (m_parentMap) y = m_parentMap->getHeight(); + x = m_offset * Metatile::pixelWidth(); + if (m_parentMap) y = m_parentMap->pixelHeight(); } else if (m_direction == "left") { - if (targetMap()) x = !clipped ? -targetMap()->getWidth() : -targetMap()->getConnectionRect(m_direction).width(); - y = m_offset; + if (targetMap()) x = !clipped ? -targetMap()->pixelWidth() : -targetMap()->getConnectionRect(m_direction).width(); + y = m_offset * Metatile::pixelHeight(); } else if (m_direction == "up") { - x = m_offset; - if (targetMap()) y = !clipped ? -targetMap()->getHeight() : -targetMap()->getConnectionRect(m_direction).height(); + x = m_offset * Metatile::pixelWidth(); + if (targetMap()) y = !clipped ? -targetMap()->pixelHeight() : -targetMap()->getConnectionRect(m_direction).height(); } return QPoint(x, y); } diff --git a/src/core/maplayout.cpp b/src/core/maplayout.cpp index 4ddb3b75..a965ddb8 100644 --- a/src/core/maplayout.cpp +++ b/src/core/maplayout.cpp @@ -6,6 +6,9 @@ #include "imageproviders.h" #include "utility.h" +QList Layout::s_globalMetatileLayerOrder; +QList Layout::s_globalMetatileLayerOpacity; + Layout::Layout(const Layout &other) : Layout() { copyFrom(&other); } @@ -89,12 +92,11 @@ QMargins Layout::getBorderMargins() const { return distance; } -// Get a rectangle that represents (in pixels) the layout's map area and the visible area of its border. -// At maximum, this is equal to the map size plus the border margins. -// If the border is large (and so beyond player the view) it may be smaller than that. +// Get a rectangle that represents (in pixels) the layout's map area + the distance the player can see. +// Note that this may be smaller than the map area + the size of the border for layouts with large border dimensions. QRect Layout::getVisibleRect() const { - QRect area = QRect(0, 0, this->width * 16, this->height * 16); - return area += (Project::getMetatileViewDistance() * 16); + QRect area = QRect(0, 0, this->pixelWidth(), this->pixelHeight()); + return area += Project::getPixelViewDistance(); } bool Layout::getBlock(int x, int y, Block *out) { @@ -341,15 +343,13 @@ void Layout::magicFillCollisionElevation(int initialX, int initialY, uint16_t co } } -QPixmap Layout::render(bool ignoreCache, Layout *fromLayout, QRect bounds) { +QPixmap Layout::render(bool ignoreCache, Layout *fromLayout, const QRect &bounds) { bool changed_any = false; - int width_ = getWidth(); - int height_ = getHeight(); - if (this->image.isNull() || this->image.width() != width_ * 16 || this->image.height() != height_ * 16) { - this->image = QImage(width_ * 16, height_ * 16, QImage::Format_RGBA8888); + if (this->image.isNull() || this->image.width() != pixelWidth() || this->image.height() != pixelHeight()) { + this->image = QImage(pixelWidth(), pixelHeight(), QImage::Format_RGBA8888); changed_any = true; } - if (this->blockdata.isEmpty() || !width_ || !height_) { + if (this->blockdata.isEmpty() || this->width == 0 || this->height == 0) { this->pixmap = this->pixmap.fromImage(this->image); return this->pixmap; } @@ -365,9 +365,9 @@ QPixmap Layout::render(bool ignoreCache, Layout *fromLayout, QRect bounds) { if (!ignoreCache && !layoutBlockChanged(i, this->blockdata, this->cached_blockdata)) { continue; } - int map_y = width_ ? i / width_ : 0; - int map_x = width_ ? i % width_ : 0; - if (bounds.isValid() && !bounds.contains(map_x, map_y)) { + int x = this->width ? ((i % this->width) * Metatile::pixelWidth()) : 0; + int y = this->width ? ((i / this->width) * Metatile::pixelHeight()) : 0; + if (bounds.isValid() && !bounds.contains(x, y)) { continue; } @@ -380,14 +380,12 @@ QPixmap Layout::render(bool ignoreCache, Layout *fromLayout, QRect bounds) { metatileId, fromLayout ? fromLayout->tileset_primary : this->tileset_primary, fromLayout ? fromLayout->tileset_secondary : this->tileset_secondary, - metatileLayerOrder, - metatileLayerOpacity + metatileLayerOrder(), + metatileLayerOpacity() ); imageCache.insert(metatileId, metatileImage); } - - QPoint metatileOrigin = QPoint(map_x * 16, map_y * 16); - painter.drawImage(metatileOrigin, metatileImage); + painter.drawImage(x, y, metatileImage); changed_any = true; } painter.end(); @@ -401,13 +399,11 @@ QPixmap Layout::render(bool ignoreCache, Layout *fromLayout, QRect bounds) { QPixmap Layout::renderCollision(bool ignoreCache) { bool changed_any = false; - int width_ = getWidth(); - int height_ = getHeight(); - if (collision_image.isNull() || collision_image.width() != width_ * 16 || collision_image.height() != height_ * 16) { - collision_image = QImage(width_ * 16, height_ * 16, QImage::Format_RGBA8888); + if (collision_image.isNull() || collision_image.width() != pixelWidth() || collision_image.height() != pixelHeight()) { + collision_image = QImage(pixelWidth(), pixelHeight(), QImage::Format_RGBA8888); changed_any = true; } - if (this->blockdata.isEmpty() || !width_ || !height_) { + if (this->blockdata.isEmpty() || this->width == 0 || this->height == 0) { collision_pixmap = collision_pixmap.fromImage(collision_image); return collision_pixmap; } @@ -419,10 +415,9 @@ QPixmap Layout::renderCollision(bool ignoreCache) { changed_any = true; Block block = this->blockdata.at(i); QImage collision_metatile_image = getCollisionMetatileImage(block); - int map_y = width_ ? i / width_ : 0; - int map_x = width_ ? i % width_ : 0; - QPoint metatile_origin = QPoint(map_x * 16, map_y * 16); - painter.drawImage(metatile_origin, collision_metatile_image); + int x = this->width ? ((i % this->width) * Metatile::pixelWidth()) : 0; + int y = this->width ? ((i / this->width) * Metatile::pixelHeight()) : 0; + painter.drawImage(x, y, collision_metatile_image); } painter.end(); cacheCollision(); @@ -434,14 +429,14 @@ QPixmap Layout::renderCollision(bool ignoreCache) { QPixmap Layout::renderBorder(bool ignoreCache) { bool changed_any = false, border_resized = false; - int width_ = getBorderWidth(); - int height_ = getBorderHeight(); + int pixelWidth = this->border_width * Metatile::pixelWidth(); + int pixelHeight = this->border_height * Metatile::pixelHeight(); if (this->border_image.isNull()) { - this->border_image = QImage(width_ * 16, height_ * 16, QImage::Format_RGBA8888); + this->border_image = QImage(pixelWidth, pixelHeight, QImage::Format_RGBA8888); changed_any = true; } - if (this->border_image.width() != width_ * 16 || this->border_image.height() != height_ * 16) { - this->border_image = QImage(width_ * 16, height_ * 16, QImage::Format_RGBA8888); + if (this->border_image.width() != pixelWidth || this->border_image.height() != pixelHeight) { + this->border_image = QImage(pixelWidth, pixelHeight, QImage::Format_RGBA8888); border_resized = true; } if (this->border.isEmpty()) { @@ -457,10 +452,10 @@ QPixmap Layout::renderBorder(bool ignoreCache) { changed_any = true; Block block = this->border.at(i); uint16_t metatileId = block.metatileId(); - QImage metatile_image = getMetatileImage(metatileId, this->tileset_primary, this->tileset_secondary, metatileLayerOrder, metatileLayerOpacity); - int map_y = width_ ? i / width_ : 0; - int map_x = width_ ? i % width_ : 0; - painter.drawImage(QPoint(map_x * 16, map_y * 16), metatile_image); + QImage metatile_image = getMetatileImage(metatileId, this); + int x = this->border_width ? ((i % this->border_width) * Metatile::pixelWidth()) : 0; + int y = this->border_width ? ((i / this->border_width) * Metatile::pixelHeight()) : 0; + painter.drawImage(x, y, metatile_image); } painter.end(); if (changed_any) { @@ -535,6 +530,7 @@ bool Layout::loadBorder(const QString &root) { logError(QString("Failed to load border for %1 from '%2': %3").arg(this->name).arg(path).arg(error)); return false; } + this->border = blockdata; // 0 is an expected border width/height that should be handled, GF used it for the RS layouts in FRLG if (this->border_width <= 0) { @@ -544,10 +540,6 @@ bool Layout::loadBorder(const QString &root) { this->border_height = DEFAULT_BORDER_HEIGHT; } - this->border = blockdata; - this->lastCommitBlocks.border = blockdata; - this->lastCommitBlocks.borderDimensions = QSize(this->border_width, this->border_height); - int expectedSize = this->border_width * this->border_height; if (this->border.count() != expectedSize) { logWarn(QString("%1 border blockdata length %2 does not match dimensions %3x%4 (should be %5). Resizing border blockdata.") @@ -558,6 +550,10 @@ bool Layout::loadBorder(const QString &root) { .arg(expectedSize)); this->border.resize(expectedSize); } + + this->lastCommitBlocks.border = this->border; + this->lastCommitBlocks.borderDimensions = QSize(this->border_width, this->border_height); + return true; } @@ -574,10 +570,7 @@ bool Layout::loadBlockdata(const QString &root) { logError(QString("Failed to load blockdata for %1 from '%2': %3").arg(this->name).arg(path).arg(error)); return false; } - this->blockdata = blockdata; - this->lastCommitBlocks.blocks = blockdata; - this->lastCommitBlocks.layoutDimensions = QSize(this->width, this->height); int expectedSize = this->width * this->height; if (expectedSize <= 0) { @@ -593,6 +586,10 @@ bool Layout::loadBlockdata(const QString &root) { .arg(expectedSize)); this->blockdata.resize(expectedSize); } + + this->lastCommitBlocks.blocks = this->blockdata; + this->lastCommitBlocks.layoutDimensions = QSize(this->width, this->height); + return true; } diff --git a/src/core/metatile.cpp b/src/core/metatile.cpp index 73ff9c86..f6c0d382 100644 --- a/src/core/metatile.cpp +++ b/src/core/metatile.cpp @@ -36,8 +36,8 @@ int Metatile::getIndexInTileset(int metatileId) { } QPoint Metatile::coordFromPixmapCoord(const QPointF &pixelCoord) { - int x = static_cast(pixelCoord.x()) / 16; - int y = static_cast(pixelCoord.y()) / 16; + int x = static_cast(pixelCoord.x()) / pixelWidth(); + int y = static_cast(pixelCoord.y()) / pixelHeight(); if (pixelCoord.x() < 0) x--; if (pixelCoord.y() < 0) y--; return QPoint(x, y); @@ -55,6 +55,11 @@ QString Metatile::getMetatileIdStrings(const QList metatileIds) { return metatiles.join(","); }; +QString Metatile::getLayerName(int layerNum) { + static const QStringList layerTitles = { "Bottom", "Middle", "Top"}; + return layerTitles.value(layerNum); +} + // Read and pack together this metatile's attributes. uint32_t Metatile::getAttributes() const { uint32_t data = 0; diff --git a/src/core/tileset.cpp b/src/core/tileset.cpp index cc7efb55..5f7ac20b 100644 --- a/src/core/tileset.cpp +++ b/src/core/tileset.cpp @@ -20,15 +20,15 @@ Tileset::Tileset(const Tileset &other) metatile_attrs_label(other.metatile_attrs_label), metatile_attrs_path(other.metatile_attrs_path), tilesImagePath(other.tilesImagePath), - tilesImage(other.tilesImage.copy()), palettePaths(other.palettePaths), metatileLabels(other.metatileLabels), palettes(other.palettes), palettePreviews(other.palettePreviews), + m_tilesImage(other.m_tilesImage.copy()), m_hasUnsavedTilesImage(other.m_hasUnsavedTilesImage) { - for (auto tile : other.tiles) { - tiles.append(tile.copy()); + for (auto tile : other.m_tiles) { + m_tiles.append(tile.copy()); } for (auto *metatile : other.m_metatiles) { @@ -46,15 +46,15 @@ Tileset &Tileset::operator=(const Tileset &other) { metatile_attrs_label = other.metatile_attrs_label; metatile_attrs_path = other.metatile_attrs_path; tilesImagePath = other.tilesImagePath; - tilesImage = other.tilesImage.copy(); + m_tilesImage = other.m_tilesImage.copy(); palettePaths = other.palettePaths; metatileLabels = other.metatileLabels; palettes = other.palettes; palettePreviews = other.palettePreviews; - tiles.clear(); - for (auto tile : other.tiles) { - tiles.append(tile.copy()); + m_tiles.clear(); + for (auto tile : other.m_tiles) { + m_tiles.append(tile.copy()); } clearMetatiles(); @@ -94,6 +94,22 @@ void Tileset::resizeMetatiles(int newNumMetatiles) { } } +uint16_t Tileset::firstMetatileId() const { + return this->is_secondary ? Project::getNumMetatilesPrimary() : 0; +} + +uint16_t Tileset::lastMetatileId() const { + return qMax(1, firstMetatileId() + m_metatiles.length()) - 1; +} + +int Tileset::maxMetatiles() const { + return this->is_secondary ? Project::getNumMetatilesSecondary() : Project::getNumMetatilesPrimary(); +} + +int Tileset::maxTiles() const { + return this->is_secondary ? Project::getNumTilesSecondary() : Project::getNumTilesPrimary(); +} + Tileset* Tileset::getTileTileset(int tileId, Tileset *primaryTileset, Tileset *secondaryTileset) { if (tileId < Project::getNumTilesPrimary()) { return primaryTileset; @@ -200,34 +216,34 @@ QString Tileset::getMetatileLabelPrefix() QString Tileset::getMetatileLabelPrefix(const QString &name) { // Default is "gTileset_Name" --> "METATILE_Name_" - const QString tilesetPrefix = projectConfig.getIdentifier(ProjectIdentifier::symbol_tilesets_prefix); const QString labelPrefix = projectConfig.getIdentifier(ProjectIdentifier::define_metatile_label_prefix); - return QString("%1%2_").arg(labelPrefix).arg(QString(name).replace(tilesetPrefix, "")); + return QString("%1%2_").arg(labelPrefix).arg(Tileset::stripPrefix(name)); } bool Tileset::metatileIsValid(uint16_t metatileId, Tileset *primaryTileset, Tileset *secondaryTileset) { - if (metatileId >= Project::getNumMetatilesTotal()) - return false; - - if (metatileId < Project::getNumMetatilesPrimary() && metatileId >= primaryTileset->numMetatiles()) - return false; - - if (metatileId >= Project::getNumMetatilesPrimary() + secondaryTileset->numMetatiles()) - return false; - - return true; + return (primaryTileset && primaryTileset->contains(metatileId)) + || (secondaryTileset && secondaryTileset->contains(metatileId)); } QList> Tileset::getBlockPalettes(Tileset *primaryTileset, Tileset *secondaryTileset, bool useTruePalettes) { QList> palettes; - auto primaryPalettes = useTruePalettes ? primaryTileset->palettes : primaryTileset->palettePreviews; + + QList> primaryPalettes; + if (primaryTileset) { + primaryPalettes = useTruePalettes ? primaryTileset->palettes : primaryTileset->palettePreviews; + } for (int i = 0; i < Project::getNumPalettesPrimary(); i++) { - palettes.append(primaryPalettes.at(i)); + palettes.append(primaryPalettes.value(i)); + } + + QList> secondaryPalettes; + if (secondaryTileset) { + secondaryPalettes = useTruePalettes ? secondaryTileset->palettes : secondaryTileset->palettePreviews; } - auto secondaryPalettes = useTruePalettes ? secondaryTileset->palettes : secondaryTileset->palettePreviews; for (int i = Project::getNumPalettesPrimary(); i < Project::getNumPalettesTotal(); i++) { - palettes.append(secondaryPalettes.at(i)); + palettes.append(secondaryPalettes.value(i)); } + return palettes; } @@ -236,10 +252,12 @@ QList Tileset::getPalette(int paletteId, Tileset *primaryTileset, Tileset Tileset *tileset = paletteId < Project::getNumPalettesPrimary() ? primaryTileset : secondaryTileset; - auto palettes = useTruePalettes ? tileset->palettes : tileset->palettePreviews; + if (!tileset) { + return paletteTable; + } - if (paletteId < 0 || paletteId >= palettes.length()){ - logError(QString("Invalid tileset palette id '%1' requested.").arg(paletteId)); + auto palettes = useTruePalettes ? tileset->palettes : tileset->palettePreviews; + if (paletteId < 0 || paletteId >= palettes.length()) { return paletteTable; } @@ -373,8 +391,7 @@ QString Tileset::getExpectedDir(QString tilesetName, bool isSecondary) : projectConfig.getFilePath(ProjectFilePath::data_primary_tilesets_folders); static const QRegularExpression re("([a-z])([A-Z0-9])"); - const QString prefix = projectConfig.getIdentifier(ProjectIdentifier::symbol_tilesets_prefix); - return basePath + tilesetName.replace(prefix, "").replace(re, "\\1_\\2").toLower(); + return basePath + Tileset::stripPrefix(tilesetName).replace(re, "\\1_\\2").toLower(); } // Get the expected positions of the members in struct Tileset. @@ -400,17 +417,25 @@ QHash Tileset::getHeaderMemberMap(bool usingAsm) bool Tileset::loadMetatiles() { clearMetatiles(); - QFile metatiles_file(this->metatiles_path); - if (!metatiles_file.open(QIODevice::ReadOnly)) { - logError(QString("Could not open '%1' for reading: %2").arg(this->metatiles_path).arg(metatiles_file.errorString())); + QFile file(this->metatiles_path); + if (!file.open(QIODevice::ReadOnly)) { + logError(QString("Could not open '%1' for reading: %2").arg(this->metatiles_path).arg(file.errorString())); return false; } - QByteArray data = metatiles_file.readAll(); + QByteArray data = file.readAll(); int tilesPerMetatile = projectConfig.getNumTilesInMetatile(); - int bytesPerMetatile = 2 * tilesPerMetatile; - int num_metatiles = data.length() / bytesPerMetatile; - for (int i = 0; i < num_metatiles; i++) { + int bytesPerMetatile = Tile::sizeInBytes() * tilesPerMetatile; + int numMetatiles = data.length() / bytesPerMetatile; + if (numMetatiles > maxMetatiles()) { + logWarn(QString("%1 metatile count %2 exceeds limit of %3. Additional metatiles will be ignored.") + .arg(this->name) + .arg(numMetatiles) + .arg(maxMetatiles())); + numMetatiles = maxMetatiles(); + } + + for (int i = 0; i < numMetatiles; i++) { auto metatile = new Metatile; int index = i * bytesPerMetatile; for (int j = 0; j < tilesPerMetatile; j++) { @@ -424,9 +449,9 @@ bool Tileset::loadMetatiles() { } bool Tileset::saveMetatiles() { - QFile metatiles_file(this->metatiles_path); - if (!metatiles_file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { - logError(QString("Could not open '%1' for writing: %2").arg(this->metatiles_path).arg(metatiles_file.errorString())); + QFile file(this->metatiles_path); + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + logError(QString("Could not open '%1' for writing: %2").arg(this->metatiles_path).arg(file.errorString())); return false; } @@ -434,31 +459,40 @@ bool Tileset::saveMetatiles() { int numTiles = projectConfig.getNumTilesInMetatile(); for (const auto &metatile : m_metatiles) { for (int i = 0; i < numTiles; i++) { - uint16_t tile = metatile->tiles.at(i).rawValue(); + uint16_t tile = metatile->tiles.value(i).rawValue(); data.append(static_cast(tile)); data.append(static_cast(tile >> 8)); } } - metatiles_file.write(data); + file.write(data); return true; } bool Tileset::loadMetatileAttributes() { - QFile attrs_file(this->metatile_attrs_path); - if (!attrs_file.open(QIODevice::ReadOnly)) { - logError(QString("Could not open '%1' for reading: %2").arg(this->metatile_attrs_path).arg(attrs_file.errorString())); + QFile file(this->metatile_attrs_path); + if (!file.open(QIODevice::ReadOnly)) { + logError(QString("Could not open '%1' for reading: %2").arg(this->metatile_attrs_path).arg(file.errorString())); return false; } - QByteArray data = attrs_file.readAll(); + QByteArray data = file.readAll(); int attrSize = projectConfig.metatileAttributesSize; int numMetatiles = m_metatiles.length(); int numMetatileAttrs = data.length() / attrSize; - if (numMetatiles != numMetatileAttrs) { - logWarn(QString("Metatile count %1 does not match metatile attribute count %2 in %3").arg(numMetatiles).arg(numMetatileAttrs).arg(this->name)); + if (numMetatileAttrs > numMetatiles) { + logWarn(QString("%1 metatile attributes count %2 exceeds metatile count of %3. Additional attributes will be ignored.") + .arg(this->name) + .arg(numMetatileAttrs) + .arg(numMetatiles)); + numMetatileAttrs = numMetatiles; + } else if (numMetatileAttrs < numMetatiles) { + logWarn(QString("%1 metatile attributes count %2 is fewer than the metatile count of %3. Missing attributes will default to 0.") + .arg(this->name) + .arg(numMetatileAttrs) + .arg(numMetatiles)); } - for (int i = 0; i < qMin(numMetatiles, numMetatileAttrs); i++) { + for (int i = 0; i < numMetatileAttrs; i++) { uint32_t attributes = 0; for (int j = 0; j < attrSize; j++) attributes |= static_cast(data.at(i * attrSize + j)) << (8 * j); @@ -468,9 +502,9 @@ bool Tileset::loadMetatileAttributes() { } bool Tileset::saveMetatileAttributes() { - QFile attrs_file(this->metatile_attrs_path); - if (!attrs_file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { - logError(QString("Could not open '%1' for writing: %2").arg(this->metatile_attrs_path).arg(attrs_file.errorString())); + QFile file(this->metatile_attrs_path); + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + logError(QString("Could not open '%1' for writing: %2").arg(this->metatile_attrs_path).arg(file.errorString())); return false; } @@ -480,21 +514,36 @@ bool Tileset::saveMetatileAttributes() { for (int i = 0; i < projectConfig.metatileAttributesSize; i++) data.append(static_cast(attributes >> (8 * i))); } - attrs_file.write(data); + file.write(data); return true; } bool Tileset::loadTilesImage(QImage *importedImage) { QImage image; + bool imported = false; if (importedImage) { image = *importedImage; - m_hasUnsavedTilesImage = true; + imported = true; } else if (QFile::exists(this->tilesImagePath)) { // No image provided, load from file path. image = QImage(this->tilesImagePath).convertToFormat(QImage::Format_Indexed8, Qt::ThresholdDither); - } else { - // Use default image - image = QImage(8, 8, QImage::Format_Indexed8); + } + + if (image.isNull()) { + logWarn(QString("Failed to load tiles image for %1. Using default tiles image.").arg(this->name)); + image = QImage(Tile::pixelWidth(), Tile::pixelHeight(), QImage::Format_Indexed8); + image.fill(0); + } + + // Validate image dimensions + if (image.width() % Tile::pixelWidth() || image.height() % Tile::pixelHeight()) { + logError(QString("%1 tiles image has invalid dimensions %2x%3. Dimensions must be a multiple of %4x%5.") + .arg(this->name) + .arg(image.width()) + .arg(image.height()) + .arg(Tile::pixelWidth()) + .arg(Tile::pixelHeight())); + return false; } // Validate image contains 16 colors. @@ -508,17 +557,31 @@ bool Tileset::loadTilesImage(QImage *importedImage) { } image.setColorTable(colorTable); } + m_tilesImage = image; - QList tiles; - int w = 8; - int h = 8; - for (int y = 0; y < image.height(); y += h) - for (int x = 0; x < image.width(); x += w) { - QImage tile = image.copy(x, y, w, h); - tiles.append(tile); + // Cut up the full tiles image into individual tile images. + m_tiles.clear(); + for (int y = 0; y < image.height(); y += Tile::pixelHeight()) + for (int x = 0; x < image.width(); x += Tile::pixelWidth()) { + m_tiles.append(image.copy(x, y, Tile::pixelWidth(), Tile::pixelHeight())); } - this->tilesImage = image; - this->tiles = tiles; + + if (m_tiles.length() > maxTiles()) { + logWarn(QString("%1 tile count of %2 exceeds limit of %3. Additional tiles will not be displayed.") + .arg(this->name) + .arg(m_tiles.length()) + .arg(maxTiles())); + + // Just resize m_tiles so that numTiles() reports the correct tile count. + // We'll leave m_tilesImage alone (it doesn't get displayed, and we don't want to delete the user's image data). + m_tiles = m_tiles.mid(0, maxTiles()); + } + + if (imported) { + // Only set this flag once we've successfully loaded the tiles image. + m_hasUnsavedTilesImage = true; + } + return true; } @@ -528,7 +591,7 @@ bool Tileset::saveTilesImage() { if (!m_hasUnsavedTilesImage) return true; - if (!this->tilesImage.save(this->tilesImagePath, "PNG")) { + if (!m_tilesImage.save(this->tilesImagePath, "PNG")) { logError(QString("Failed to save tiles image '%1'").arg(this->tilesImagePath)); return false; } @@ -575,19 +638,23 @@ bool Tileset::savePalettes() { bool Tileset::load() { bool success = true; + if (!loadPalettes()) success = false; + if (!loadTilesImage()) success = false; if (!loadMetatiles()) success = false; if (!loadMetatileAttributes()) success = false; - if (!loadTilesImage()) success = false; - if (!loadPalettes()) success = false; return success; } // Because metatile labels are global (and handled by the project) we don't save them here. bool Tileset::save() { bool success = true; + if (!savePalettes()) success = false; + if (!saveTilesImage()) success = false; if (!saveMetatiles()) success = false; if (!saveMetatileAttributes()) success = false; - if (!saveTilesImage()) success = false; - if (!savePalettes()) success = false; return success; } + +QString Tileset::stripPrefix(const QString &fullName) { + return QString(fullName).replace(projectConfig.getIdentifier(ProjectIdentifier::symbol_tilesets_prefix), ""); +} diff --git a/src/core/utility.cpp b/src/core/utility.cpp index 0ecfd13e..ff06838f 100644 --- a/src/core/utility.cpp +++ b/src/core/utility.cpp @@ -14,7 +14,7 @@ void Util::numericalModeSort(QStringList &list) { std::sort(list.begin(), list.end(), collator); } -int Util::roundUp(int numToRound, int multiple) { +int Util::roundUpToMultiple(int numToRound, int multiple) { if (multiple <= 0) return numToRound; diff --git a/src/editor.cpp b/src/editor.cpp index a0635d38..5f1dfe0f 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -30,7 +30,7 @@ Editor::Editor(Ui::MainWindow* ui) { this->ui = ui; this->settings = new Settings(); - this->cursorMapTileRect = new CursorTileRect(QSize(16,16), qRgb(255, 255, 255)); + this->cursorMapTileRect = new CursorTileRect(Metatile::pixelSize(), qRgb(255, 255, 255)); this->map_ruler = new MapRuler(4); connect(this->map_ruler, &MapRuler::statusChanged, this, &Editor::mapRulerStatusChanged); @@ -1171,13 +1171,13 @@ bool Editor::isMouseInMap() const { void Editor::setPlayerViewRect(const QRectF &rect) { delete this->playerViewRect; - this->playerViewRect = new MovableRect(rect, qRgb(255, 255, 255)); + this->playerViewRect = new MovableRect(rect, Metatile::pixelSize(), qRgb(255, 255, 255)); updateCursorRectVisibility(); } void Editor::setCursorRectPos(const QPoint &pos) { - int x = qMax(0, qMin(pos.x(), this->layout ? this->layout->getWidth() - 1 : 0)); - int y = qMax(0, qMin(pos.y(), this->layout ? this->layout->getHeight() - 1 : 0)); + int x = qBound(0, pos.x(), this->layout ? this->layout->getWidth() - 1 : 0); + int y = qBound(0, pos.y(), this->layout ? this->layout->getHeight() - 1 : 0); if (this->playerViewRect) this->playerViewRect->updateLocation(x, y); @@ -1357,12 +1357,13 @@ bool Editor::setLayout(QString layoutId) { map_ruler->setMapDimensions(QSize(this->layout->getWidth(), this->layout->getHeight())); connect(this->layout, &Layout::dimensionsChanged, map_ruler, &MapRuler::setMapDimensions); - ui->comboBox_PrimaryTileset->blockSignals(true); - ui->comboBox_SecondaryTileset->blockSignals(true); + QString prevPrimaryTileset = ui->comboBox_PrimaryTileset->currentText(); + QString prevSecondaryTileset = ui->comboBox_SecondaryTileset->currentText(); + + const QSignalBlocker b_PrimaryTilest(ui->comboBox_PrimaryTileset); + const QSignalBlocker b_SecondaryTilest(ui->comboBox_SecondaryTileset); ui->comboBox_PrimaryTileset->setTextItem(this->layout->tileset_primary_label); ui->comboBox_SecondaryTileset->setTextItem(this->layout->tileset_secondary_label); - ui->comboBox_PrimaryTileset->blockSignals(false); - ui->comboBox_SecondaryTileset->blockSignals(false); const QSignalBlocker b0(this->ui->comboBox_LayoutSelector); int index = this->ui->comboBox_LayoutSelector->findText(layoutId); @@ -1371,6 +1372,10 @@ bool Editor::setLayout(QString layoutId) { if (this->layout->name != prevLayoutName) Scripting::cb_LayoutOpened(this->layout->name); + if (this->layout->tileset_primary_label != prevPrimaryTileset) + Scripting::cb_TilesetUpdated(this->layout->tileset_primary_label); + if (this->layout->tileset_secondary_label != prevSecondaryTileset) + Scripting::cb_TilesetUpdated(this->layout->tileset_secondary_label); return true; } @@ -1641,7 +1646,7 @@ void Editor::displayMetatileSelector() { scene_metatiles = new QGraphicsScene; if (!metatile_selector_item) { - metatile_selector_item = new MetatileSelector(8, this->layout); + metatile_selector_item = new MetatileSelector(projectConfig.metatileSelectorWidth, this->layout); connect(metatile_selector_item, &MetatileSelector::hoveredMetatileSelectionChanged, this, &Editor::onHoveredMetatileSelectionChanged); connect(metatile_selector_item, &MetatileSelector::hoveredMetatileSelectionCleared, @@ -1651,13 +1656,6 @@ void Editor::displayMetatileSelector() { metatile_selector_item->select(0); } else { metatile_selector_item->setLayout(this->layout); - if (metatile_selector_item->primaryTileset - && metatile_selector_item->primaryTileset != this->layout->tileset_primary) - emit tilesetUpdated(this->layout->tileset_primary->name); - if (metatile_selector_item->secondaryTileset - && metatile_selector_item->secondaryTileset != this->layout->tileset_secondary) - emit tilesetUpdated(this->layout->tileset_secondary->name); - metatile_selector_item->setTilesets(this->layout->tileset_primary, this->layout->tileset_secondary); } scene_metatiles->addItem(metatile_selector_item); @@ -1917,8 +1915,8 @@ void Editor::displayMapBorder() { for (int y = -borderMargins.top(); y < this->layout->getHeight() + borderMargins.bottom(); y += this->layout->getBorderHeight()) for (int x = -borderMargins.left(); x < this->layout->getWidth() + borderMargins.right(); x += this->layout->getBorderWidth()) { QGraphicsPixmapItem *item = new QGraphicsPixmapItem(pixmap); - item->setX(x * 16); - item->setY(y * 16); + item->setX(x * Metatile::pixelWidth()); + item->setY(y * Metatile::pixelHeight()); item->setZValue(ZValue::MapBorder); scene->addItem(item); borderItems.append(item); @@ -1966,8 +1964,8 @@ void Editor::displayMapGrid() { // elements of the scripting API, so they're painted manually in MapView::drawForeground. this->mapGrid = new QGraphicsItemGroup(); - const int pixelMapWidth = this->layout->getWidth() * 16; - const int pixelMapHeight = this->layout->getHeight() * 16; + const int pixelMapWidth = this->layout->pixelWidth(); + const int pixelMapHeight = this->layout->pixelHeight(); // The grid can be moved with a user-specified x/y offset. The grid's dash patterns will only wrap in full pattern increments, // so we draw an additional row/column outside the map that can be revealed as the offset changes. @@ -2488,7 +2486,7 @@ void Editor::setCollisionGraphics() { // Use the image sheet to create an icon for each collision/elevation combination. // Any icons for combinations that aren't provided by the image sheet are also created now using default graphics. - const int w = 16, h = 16; + const int w = Metatile::pixelWidth(), h = Metatile::pixelHeight(); imgSheet = imgSheet.scaled(w * imgColumns, h * imgRows); for (int collision = 0; collision <= Block::getMaxCollision(); collision++) { // If (collision >= imgColumns) here, it's a valid collision value, but it is not represented with an icon on the image sheet. diff --git a/src/main.cpp b/src/main.cpp index d85c1b38..dfd3bff2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,6 +6,8 @@ int main(int argc, char *argv[]) { QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::Round); + QCoreApplication::setAttribute(Qt::AA_UseStyleSheetPropagationInWidgetStyles, true); + QApplication a(argc, argv); a.setStyle("fusion"); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 7a316459..c191ea07 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -367,7 +367,6 @@ void MainWindow::initEditor() { connect(this->editor, &Editor::currentMetatilesSelectionChanged, this, &MainWindow::currentMetatilesSelectionChanged); connect(this->editor, &Editor::wildMonTableEdited, [this] { markMapEdited(this->editor->map); }); connect(this->editor, &Editor::mapRulerStatusChanged, this, &MainWindow::onMapRulerStatusChanged); - connect(this->editor, &Editor::tilesetUpdated, this, &Scripting::cb_TilesetUpdated); connect(this->editor, &Editor::editActionSet, this, &MainWindow::setEditActionUi); connect(ui->newEventToolButton, &NewEventToolButton::newEventAdded, this->editor, &Editor::addNewEvent); connect(ui->toolButton_deleteEvent, &QAbstractButton::clicked, this->editor, &Editor::deleteSelectedEvents); @@ -1409,7 +1408,7 @@ bool MainWindow::setProjectUI() { ui->newEventToolButton->setEventTypeVisible(Event::Type::SecretBase, projectConfig.eventSecretBaseEnabled); ui->newEventToolButton->setEventTypeVisible(Event::Type::CloneObject, projectConfig.eventCloneObjectEnabled); - this->editor->setPlayerViewRect(QRectF(0, 0, 16, 16).marginsAdded(projectConfig.playerViewDistance)); + this->editor->setPlayerViewRect(QRectF(QPoint(0,0), Metatile::pixelSize()).marginsAdded(projectConfig.playerViewDistance)); editor->setCollisionGraphics(); ui->spinBox_SelectedElevation->setMaximum(Block::getMaxElevation()); @@ -1810,7 +1809,7 @@ void MainWindow::scrollMetatileSelectorToSelection() { QPoint pos = editor->metatile_selector_item->getMetatileIdCoordsOnWidget(selection.metatileItems.first().metatileId); QPoint size = editor->metatile_selector_item->getSelectionDimensions(); - pos += QPoint(size.x() - 1, size.y() - 1) * 16 / 2; // We want to focus on the center of the whole selection + pos += QPoint((size.x() - 1) * Metatile::pixelWidth(), (size.y() - 1) * Metatile::pixelHeight()) / 2; // We want to focus on the center of the whole selection pos *= getMetatilesZoomScale(); auto viewport = ui->scrollArea_MetatileSelector->viewport(); @@ -1821,7 +1820,9 @@ void MainWindow::currentMetatilesSelectionChanged() { redrawMetatileSelection(); if (this->tilesetEditor) { MetatileSelection selection = editor->metatile_selector_item->getMetatileSelection(); - this->tilesetEditor->selectMetatile(selection.metatileItems.first().metatileId); + if (!selection.metatileItems.isEmpty()) { + this->tilesetEditor->selectMetatile(selection.metatileItems.first().metatileId); + } } // Don't scroll to internal selections here, it will disrupt the user while they make their selection. @@ -2845,7 +2846,9 @@ void MainWindow::on_actionTileset_Editor_triggered() openSubWindow(this->tilesetEditor); MetatileSelection selection = this->editor->metatile_selector_item->getMetatileSelection(); - this->tilesetEditor->selectMetatile(selection.metatileItems.first().metatileId); + if (!selection.metatileItems.isEmpty()) { + this->tilesetEditor->selectMetatile(selection.metatileItems.first().metatileId); + } } void MainWindow::initTilesetEditor() { diff --git a/src/project.cpp b/src/project.cpp index 7188b5ef..b76821f3 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -1558,9 +1558,8 @@ Tileset *Project::createNewTileset(QString name, bool secondary, bool checkerboa tileset->loadTilesImage(&tilesImage); // Create default metatiles - const int numMetatiles = tileset->is_secondary ? (Project::getNumMetatilesTotal() - Project::getNumMetatilesPrimary()) : Project::getNumMetatilesPrimary(); const int tilesPerMetatile = projectConfig.getNumTilesInMetatile(); - for (int i = 0; i < numMetatiles; ++i) { + for (int i = 0; i < tileset->maxMetatiles(); ++i) { auto metatile = new Metatile(); for(int j = 0; j < tilesPerMetatile; ++j){ Tile tile = Tile(); @@ -1618,7 +1617,7 @@ Tileset *Project::createNewTileset(QString name, bool secondary, bool checkerboa metatilesFilepath.append(projectConfig.getFilePath(ProjectFilePath::tilesets_metatiles)); } ignoreWatchedFilesTemporarily({headersFilepath, graphicsFilepath, metatilesFilepath}); - name.remove(0, prefix.length()); // Strip prefix from name to get base name for use in other symbols. + name = Tileset::stripPrefix(name); tileset->appendToHeaders(headersFilepath, name, this->usingAsmTilesets); tileset->appendToGraphics(graphicsFilepath, name, this->usingAsmTilesets); tileset->appendToMetatiles(metatilesFilepath, name, this->usingAsmTilesets); @@ -2400,12 +2399,11 @@ bool Project::readFieldmapProperties() { // We can determine whether triple-layer metatiles are in-use by reading this constant. // If the constant is missing (or is using a value other than 8 or 12) the user must tell // us whether they're using triple-layer metatiles under Project Settings. - static const int numTilesPerLayer = 4; int numTilesPerMetatile = it.value(); - if (numTilesPerMetatile == 2 * numTilesPerLayer) { + if (numTilesPerMetatile == 2 * Metatile::tilesPerLayer()) { projectConfig.tripleLayerMetatilesEnabled = false; this->disabledSettingsNames.insert(numTilesPerMetatileName); - } else if (numTilesPerMetatile == 3 * numTilesPerLayer) { + } else if (numTilesPerMetatile == 3 * Metatile::tilesPerLayer()) { projectConfig.tripleLayerMetatilesEnabled = true; this->disabledSettingsNames.insert(numTilesPerMetatileName); } @@ -2462,6 +2460,7 @@ bool Project::readFieldmapMasks() { projectConfig.blockCollisionMask = blockMask; if (readBlockMask(elevationMaskName, &blockMask)) projectConfig.blockElevationMask = blockMask; + Block::setLayout(); // Read RSE metatile attribute masks auto it = defines.find(behaviorMaskName); @@ -3444,14 +3443,25 @@ QString Project::getEmptySpeciesName() { return projectConfig.getIdentifier(ProjectIdentifier::define_species_prefix) + projectConfig.getIdentifier(ProjectIdentifier::define_species_empty); } -// Get the distance in metatiles (rounded up) that the player is able to see in each direction in-game. +// Get the distance in pixels that the player is able to see in each direction in-game, +// rounded up to a multiple of a metatile's pixel size. +QMargins Project::getPixelViewDistance() { + QMargins viewDistance = projectConfig.playerViewDistance; + viewDistance.setTop(Util::roundUpToMultiple(viewDistance.top(), Metatile::pixelHeight())); + viewDistance.setBottom(Util::roundUpToMultiple(viewDistance.bottom(), Metatile::pixelHeight())); + viewDistance.setLeft(Util::roundUpToMultiple(viewDistance.left(), Metatile::pixelWidth())); + viewDistance.setRight(Util::roundUpToMultiple(viewDistance.right(), Metatile::pixelWidth())); + return viewDistance; +} + +// Get the distance in metatiles that the player is able to see in each direction in-game. // For the default view distance (i.e. assuming the player is centered in a 240x160 pixel GBA screen) this is 7x5 metatiles. QMargins Project::getMetatileViewDistance() { - QMargins viewDistance = projectConfig.playerViewDistance; - viewDistance.setTop(qCeil(viewDistance.top() / 16.0)); - viewDistance.setBottom(qCeil(viewDistance.bottom() / 16.0)); - viewDistance.setLeft(qCeil(viewDistance.left() / 16.0)); - viewDistance.setRight(qCeil(viewDistance.right() / 16.0)); + QMargins viewDistance = getPixelViewDistance(); + viewDistance.setTop(viewDistance.top() / Metatile::pixelHeight()); + viewDistance.setBottom(viewDistance.bottom() / Metatile::pixelHeight()); + viewDistance.setLeft(viewDistance.left() / Metatile::pixelWidth()); + viewDistance.setRight(viewDistance.right() / Metatile::pixelWidth()); return viewDistance; } @@ -3481,15 +3491,14 @@ void Project::applyParsedLimits() { projectConfig.metatileEncounterTypeMask &= maxMask; projectConfig.metatileLayerTypeMask &= maxMask; - Block::setLayout(); Metatile::setLayout(this); - Project::num_metatiles_primary = qMin(qMax(Project::num_metatiles_primary, 1), Block::getMaxMetatileId() + 1); + Project::num_metatiles_primary = qBound(1, Project::num_metatiles_primary, Block::getMaxMetatileId() + 1); projectConfig.defaultMetatileId = qMin(projectConfig.defaultMetatileId, Block::getMaxMetatileId()); projectConfig.defaultElevation = qMin(projectConfig.defaultElevation, Block::getMaxElevation()); projectConfig.defaultCollision = qMin(projectConfig.defaultCollision, Block::getMaxCollision()); - projectConfig.collisionSheetSize.setHeight(qMin(qMax(projectConfig.collisionSheetSize.height(), 1), Block::getMaxElevation() + 1)); - projectConfig.collisionSheetSize.setWidth(qMin(qMax(projectConfig.collisionSheetSize.width(), 1), Block::getMaxCollision() + 1)); + projectConfig.collisionSheetSize.setHeight(qBound(1, projectConfig.collisionSheetSize.height(), Block::getMaxElevation() + 1)); + projectConfig.collisionSheetSize.setWidth(qBound(1, projectConfig.collisionSheetSize.width(), Block::getMaxCollision() + 1)); } bool Project::hasUnsavedChanges() { diff --git a/src/scriptapi/apimap.cpp b/src/scriptapi/apimap.cpp index 08bfc9c5..a1730791 100644 --- a/src/scriptapi/apimap.cpp +++ b/src/scriptapi/apimap.cpp @@ -4,6 +4,7 @@ #include "editcommands.h" #include "config.h" #include "imageproviders.h" +#include "scriptutility.h" // TODO: "tilesetNeedsRedraw" is used when redrawing the map after // changing a metatile's tiles via script. It is unnecessarily @@ -26,7 +27,7 @@ void MainWindow::tryRedrawMapArea(bool forceRedraw) { if (this->tilesetEditor) this->tilesetEditor->updateTilesets(this->editor->layout->tileset_primary_label, this->editor->layout->tileset_secondary_label); if (this->editor->metatile_selector_item) - this->editor->metatile_selector_item->draw(); + this->editor->metatile_selector_item->refresh(); if (this->editor->selected_border_metatiles_item) this->editor->selected_border_metatiles_item->draw(); if (this->editor->current_metatile_selection_item) @@ -332,7 +333,7 @@ void MainWindow::refreshAfterPaletteChange(Tileset *tileset) { if (this->tilesetEditor) { this->tilesetEditor->updateTilesets(this->editor->layout->tileset_primary_label, this->editor->layout->tileset_secondary_label); } - this->editor->metatile_selector_item->draw(); + this->editor->metatile_selector_item->refresh(); this->editor->selected_border_metatiles_item->draw(); this->editor->map_item->draw(true); this->editor->updateMapBorder(); @@ -444,7 +445,7 @@ QJSValue MainWindow::getSecondaryTilesetPalettes() { } void MainWindow::refreshAfterPalettePreviewChange() { - this->editor->metatile_selector_item->draw(); + this->editor->metatile_selector_item->refresh(); this->editor->selected_border_metatiles_item->draw(); this->editor->map_item->draw(true); this->editor->updateMapBorder(); @@ -545,13 +546,13 @@ int MainWindow::getNumSecondaryTilesetMetatiles() { int MainWindow::getNumPrimaryTilesetTiles() { if (!this->editor || !this->editor->layout || !this->editor->layout->tileset_primary) return 0; - return this->editor->layout->tileset_primary->tiles.length(); + return this->editor->layout->tileset_primary->numTiles(); } int MainWindow::getNumSecondaryTilesetTiles() { if (!this->editor || !this->editor->layout || !this->editor->layout->tileset_secondary) return 0; - return this->editor->layout->tileset_secondary->tiles.length(); + return this->editor->layout->tileset_secondary->numTiles(); } QString MainWindow::getPrimaryTileset() { @@ -796,17 +797,46 @@ void MainWindow::setMetatileTile(int metatileId, int tileIndex, QJSValue tileObj QJSValue MainWindow::getTilePixels(int tileId) { if (tileId < 0 || !this->editor || !this->editor->layout) return QJSValue(); + + const int numPixels = Tile::pixelWidth() * Tile::pixelHeight(); QImage tileImage = getTileImage(tileId, this->editor->layout->tileset_primary, this->editor->layout->tileset_secondary); - if (tileImage.isNull() || tileImage.sizeInBytes() < 64) + if (tileImage.isNull() || tileImage.sizeInBytes() < numPixels) return QJSValue(); + const uchar * pixels = tileImage.constBits(); - QJSValue pixelArray = Scripting::getEngine()->newArray(64); - for (int i = 0; i < 64; i++) { + QJSValue pixelArray = Scripting::getEngine()->newArray(numPixels); + for (int i = 0; i < numPixels; i++) { pixelArray.setProperty(i, pixels[i]); } return pixelArray; } +QList MainWindow::getMetatileLayerOrder() const { + if (!this->editor || !this->editor->layout) + return QList(); + return this->editor->layout->metatileLayerOrder(); +} + +void MainWindow::setMetatileLayerOrder(const QList &order) { + if (!this->editor || !this->editor->layout || !ScriptUtility::validateMetatileLayerOrder(order)) + return; + this->editor->layout->setMetatileLayerOrder(order); + this->refreshAfterPalettePreviewChange(); +} + +QList MainWindow::getMetatileLayerOpacity() const { + if (!this->editor || !this->editor->layout) + return QList(); + return this->editor->layout->metatileLayerOpacity(); +} + +void MainWindow::setMetatileLayerOpacity(const QList &opacities) { + if (!this->editor || !this->editor->layout) + return; + this->editor->layout->setMetatileLayerOpacity(opacities); + this->refreshAfterPalettePreviewChange(); +} + //===================== // Editing map header //===================== diff --git a/src/scriptapi/apioverlay.cpp b/src/scriptapi/apioverlay.cpp index 4e183659..ffe7743a 100644 --- a/src/scriptapi/apioverlay.cpp +++ b/src/scriptapi/apioverlay.cpp @@ -294,11 +294,7 @@ void MapView::addTileImage(int x, int y, const Tile &tile, bool setTransparency, void MapView::addMetatileImage(int x, int y, int metatileId, bool setTransparency, int layer) { if (!this->editor || !this->editor->layout || !this->editor->layout->tileset_primary || !this->editor->layout->tileset_secondary) return; - QImage image = getMetatileImage(static_cast(metatileId), - this->editor->layout->tileset_primary, - this->editor->layout->tileset_secondary, - this->editor->layout->metatileLayerOrder, - this->editor->layout->metatileLayerOpacity); + QImage image = getMetatileImage(static_cast(metatileId), this->editor->layout); if (setTransparency) image.setColor(0, qRgba(0, 0, 0, 0)); if (this->getOverlay(layer)->addImage(x, y, image)) diff --git a/src/scriptapi/apiutility.cpp b/src/scriptapi/apiutility.cpp index dfc65617..5058e2de 100644 --- a/src/scriptapi/apiutility.cpp +++ b/src/scriptapi/apiutility.cpp @@ -201,46 +201,36 @@ QList ScriptUtility::getCustomScripts() { } QList ScriptUtility::getMetatileLayerOrder() { - if (!window || !window->editor || !window->editor->layout) - return QList(); - return window->editor->layout->metatileLayerOrder; + return Layout::globalMetatileLayerOrder(); } -void ScriptUtility::setMetatileLayerOrder(QList order) { - if (!window || !window->editor || !window->editor->layout) - return; - +bool ScriptUtility::validateMetatileLayerOrder(const QList &order) { const int numLayers = 3; - int size = order.size(); - if (size < numLayers) { - logError(QString("Metatile layer order has insufficient elements (%1), needs at least %2.").arg(size).arg(numLayers)); - return; - } - bool invalid = false; - for (int i = 0; i < numLayers; i++) { + bool valid = true; + for (int i = 0; i < qMin(order.length(), numLayers); i++) { int layer = order.at(i); if (layer < 0 || layer >= numLayers) { logError(QString("'%1' is not a valid metatile layer order value, must be in range 0-%2.").arg(layer).arg(numLayers - 1)); - invalid = true; + valid = false; } } - if (invalid) return; + return valid; +} - window->editor->layout->metatileLayerOrder = order; - window->refreshAfterPalettePreviewChange(); +void ScriptUtility::setMetatileLayerOrder(const QList &order) { + if (!validateMetatileLayerOrder(order)) + return; + Layout::setGlobalMetatileLayerOrder(order); + if (window) window->refreshAfterPalettePreviewChange(); } QList ScriptUtility::getMetatileLayerOpacity() { - if (!window || !window->editor || !window->editor->layout) - return QList(); - return window->editor->layout->metatileLayerOpacity; + return Layout::globalMetatileLayerOpacity(); } -void ScriptUtility::setMetatileLayerOpacity(QList order) { - if (!window || !window->editor || !window->editor->layout) - return; - window->editor->layout->metatileLayerOpacity = order; - window->refreshAfterPalettePreviewChange(); +void ScriptUtility::setMetatileLayerOpacity(const QList &opacities) { + Layout::setGlobalMetatileLayerOpacity(opacities); + if (window) window->refreshAfterPalettePreviewChange(); } QList ScriptUtility::getMapNames() { diff --git a/src/scriptapi/scripting.cpp b/src/scriptapi/scripting.cpp index 93823de2..148a75fe 100644 --- a/src/scriptapi/scripting.cpp +++ b/src/scriptapi/scripting.cpp @@ -99,15 +99,12 @@ void Scripting::populateGlobalObject(MainWindow *mainWindow) { constants.setProperty("version", version); // Get basic tileset information - int numTilesPrimary = Project::getNumTilesPrimary(); - int numMetatilesPrimary = Project::getNumMetatilesPrimary(); - int numPalettesPrimary = Project::getNumPalettesPrimary(); - constants.setProperty("max_primary_tiles", numTilesPrimary); - constants.setProperty("max_secondary_tiles", Project::getNumTilesTotal() - numTilesPrimary); - constants.setProperty("max_primary_metatiles", numMetatilesPrimary); - constants.setProperty("max_secondary_metatiles", Project::getNumMetatilesTotal() - numMetatilesPrimary); - constants.setProperty("num_primary_palettes", numPalettesPrimary); - constants.setProperty("num_secondary_palettes", Project::getNumPalettesTotal() - numPalettesPrimary); + constants.setProperty("max_primary_tiles", Project::getNumTilesPrimary()); + constants.setProperty("max_secondary_tiles", Project::getNumTilesSecondary()); + constants.setProperty("max_primary_metatiles", Project::getNumMetatilesPrimary()); + constants.setProperty("max_secondary_metatiles", Project::getNumMetatilesSecondary()); + constants.setProperty("num_primary_palettes", Project::getNumPalettesPrimary()); + constants.setProperty("num_secondary_palettes", Project::getNumPalettesSecondary()); constants.setProperty("layers_per_metatile", projectConfig.getNumLayersInMetatile()); constants.setProperty("tiles_per_metatile", projectConfig.getNumTilesInMetatile()); @@ -301,7 +298,7 @@ void Scripting::cb_MapShifted(int xDelta, int yDelta) { instance->invokeCallback(OnMapShifted, args); } -void Scripting::cb_TilesetUpdated(QString tilesetName) { +void Scripting::cb_TilesetUpdated(const QString &tilesetName) { if (!instance) return; QJSValueList args { diff --git a/src/ui/bordermetatilespixmapitem.cpp b/src/ui/bordermetatilespixmapitem.cpp index e793e466..ef3c33db 100644 --- a/src/ui/bordermetatilespixmapitem.cpp +++ b/src/ui/bordermetatilespixmapitem.cpp @@ -39,21 +39,15 @@ void BorderMetatilesPixmapItem::draw() { int width = layout->getBorderWidth(); int height = layout->getBorderHeight(); - QImage image(16 * width, 16 * height, QImage::Format_RGBA8888); + QImage image(width * Metatile::pixelWidth(), height * Metatile::pixelHeight(), QImage::Format_RGBA8888); QPainter painter(&image); for (int i = 0; i < width; i++) { for (int j = 0; j < height; j++) { - int x = i * 16; - int y = j * 16; - QImage metatile_image = getMetatileImage( - layout->getBorderMetatileId(i, j), - layout->tileset_primary, - layout->tileset_secondary, - layout->metatileLayerOrder, - layout->metatileLayerOpacity); - QPoint metatile_origin = QPoint(x, y); - painter.drawImage(metatile_origin, metatile_image); + QImage metatile_image = getMetatileImage(layout->getBorderMetatileId(i, j), layout); + int x = i * Metatile::pixelWidth(); + int y = j * Metatile::pixelHeight(); + painter.drawImage(x, y, metatile_image); } } diff --git a/src/ui/checkeredbgscene.cpp b/src/ui/checkeredbgscene.cpp new file mode 100644 index 00000000..9a32dff7 --- /dev/null +++ b/src/ui/checkeredbgscene.cpp @@ -0,0 +1,32 @@ +#include "checkeredbgscene.h" + +#include + +void CheckeredBgScene::drawBackground(QPainter *painter, const QRectF &rect) { + QRect r = rect.toRect(); + int w = this->gridSize.width(); + int h = this->gridSize.height(); + int xMin = r.left() - (r.left() % w) - w; + int yMin = r.top() - (r.top() % h) - h; + int xMax = r.right() - (r.right() % w) + w; + int yMax = r.bottom() - (r.bottom() % h) + h; + + // draw grid from top to bottom of scene + QColor paintColor; + for (int x = xMin; x <= xMax; x += w) { + for (int y = yMin; y <= yMax; y += h) { + if ((x/w + y/h) % 2) { + if (this->validRect.contains(x, y)) + paintColor = QColor(132, 217, 165); // green light color + else + paintColor = 0xbcbcbc; // normal light color + } else { + if (this->validRect.contains(x, y)) + paintColor = QColor(76, 178, 121); // green dark color + else + paintColor = 0x969696; // normal dark color + } + painter->fillRect(QRect(x, y, w, h), paintColor); + } + } +} diff --git a/src/ui/connectionpixmapitem.cpp b/src/ui/connectionpixmapitem.cpp index b4fbfa12..db05ffab 100644 --- a/src/ui/connectionpixmapitem.cpp +++ b/src/ui/connectionpixmapitem.cpp @@ -100,9 +100,9 @@ void ConnectionPixmapItem::updatePos() { void ConnectionPixmapItem::updateOrigin() { if (this->connection->isVertical()) { this->originX = 0; - this->originY = this->connection->relativePos(true).y() * this->mHeight; + this->originY = this->connection->relativePixelPos(true).y(); } else if (this->connection->isHorizontal()) { - this->originX = this->connection->relativePos(true).x() * this->mWidth; + this->originX = this->connection->relativePixelPos(true).x(); this->originY = 0; } updatePos(); diff --git a/src/ui/currentselectedmetatilespixmapitem.cpp b/src/ui/currentselectedmetatilespixmapitem.cpp index e8b16f49..33425131 100644 --- a/src/ui/currentselectedmetatilespixmapitem.cpp +++ b/src/ui/currentselectedmetatilespixmapitem.cpp @@ -3,26 +3,21 @@ #include QPixmap drawMetatileSelection(MetatileSelection selection, Layout *layout) { - int width = selection.dimensions.x() * 16; - int height = selection.dimensions.y() * 16; + int width = selection.dimensions.x() * Metatile::pixelWidth(); + int height = selection.dimensions.y() * Metatile::pixelHeight(); QImage image(width, height, QImage::Format_RGBA8888); image.fill(QColor(0, 0, 0, 0)); QPainter painter(&image); for (int i = 0; i < selection.dimensions.x(); i++) { for (int j = 0; j < selection.dimensions.y(); j++) { - int x = i * 16; - int y = j * 16; + int x = i * Metatile::pixelWidth(); + int y = j * Metatile::pixelHeight(); QPoint metatile_origin = QPoint(x, y); int index = j * selection.dimensions.x() + i; MetatileSelectionItem item = selection.metatileItems.at(index); if (item.enabled) { - QImage metatile_image = getMetatileImage( - item.metatileId, - layout->tileset_primary, - layout->tileset_secondary, - layout->metatileLayerOrder, - layout->metatileLayerOpacity); + QImage metatile_image = getMetatileImage(item.metatileId, layout); painter.drawImage(metatile_origin, metatile_image); } } diff --git a/src/ui/cursortilerect.cpp b/src/ui/cursortilerect.cpp index ef1de285..e62bcdc8 100644 --- a/src/ui/cursortilerect.cpp +++ b/src/ui/cursortilerect.cpp @@ -65,6 +65,6 @@ void CursorTileRect::updateLocation(int coordX, int coordY) { } } - this->setX(coordX * 16); - this->setY(coordY * 16); + this->setX(coordX * m_tileSize.width()); + this->setY(coordY * m_tileSize.height()); } diff --git a/src/ui/graphicsview.cpp b/src/ui/graphicsview.cpp index c8fc4495..667e0aab 100644 --- a/src/ui/graphicsview.cpp +++ b/src/ui/graphicsview.cpp @@ -33,7 +33,7 @@ void MapView::drawForeground(QPainter *painter, const QRectF&) { painter->save(); if (editor->layout) { // We're clipping here to hide parts of the grid that are outside the map. - const QRectF mapRect(-0.5, -0.5, editor->layout->getWidth() * 16 + 1.5, editor->layout->getHeight() * 16 + 1.5); + const QRectF mapRect(-0.5, -0.5, editor->layout->pixelWidth() + 1.5, editor->layout->pixelHeight() + 1.5); painter->setClipping(true); painter->setClipRect(mapRect); } diff --git a/src/ui/imageproviders.cpp b/src/ui/imageproviders.cpp index 3f7b8fb8..ce233066 100644 --- a/src/ui/imageproviders.cpp +++ b/src/ui/imageproviders.cpp @@ -1,6 +1,5 @@ #include "config.h" #include "imageproviders.h" -#include "log.h" #include "editor.h" #include @@ -9,10 +8,29 @@ QImage getCollisionMetatileImage(Block block) { } QImage getCollisionMetatileImage(int collision, int elevation) { - const QImage * image = Editor::collisionIcons.at(collision).at(elevation); + const QImage * image = Editor::collisionIcons.value(collision).value(elevation); return image ? *image : QImage(); } +QImage getMetatileImage(uint16_t metatileId, Layout *layout, bool useTruePalettes) { + Metatile* metatile = Tileset::getMetatile(metatileId, + layout ? layout->tileset_primary : nullptr, + layout ? layout->tileset_secondary : nullptr); + return getMetatileImage(metatile, layout, useTruePalettes); +} + +QImage getMetatileImage(Metatile *metatile, Layout *layout, bool useTruePalettes) { + if (!layout) { + return getMetatileImage(metatile, nullptr, nullptr, {}, {}, useTruePalettes); + } + return getMetatileImage(metatile, + layout->tileset_primary, + layout->tileset_secondary, + layout->metatileLayerOrder(), + layout->metatileLayerOpacity(), + useTruePalettes); +} + QImage getMetatileImage( uint16_t metatileId, Tileset *primaryTileset, @@ -21,13 +39,18 @@ QImage getMetatileImage( const QList &layerOpacity, bool useTruePalettes) { - Metatile* metatile = Tileset::getMetatile(metatileId, primaryTileset, secondaryTileset); - if (!metatile) { - QImage metatile_image(16, 16, QImage::Format_RGBA8888); - metatile_image.fill(Qt::magenta); - return metatile_image; - } - return getMetatileImage(metatile, primaryTileset, secondaryTileset, layerOrder, layerOpacity, useTruePalettes); + return getMetatileImage(Tileset::getMetatile(metatileId, primaryTileset, secondaryTileset), + primaryTileset, + secondaryTileset, + layerOrder, + layerOpacity, + useTruePalettes); +} + +// The color to use when we want to show some portion of the image request was invalid. +// Normally this is Qt::magenta, but we'll use Qt::transparent if we think the image allows it. +QColor getInvalidImageColor() { + return (projectConfig.transparencyColor == QColor(Qt::transparent)) ? QColor(Qt::transparent) : QColor(Qt::magenta); } QImage getMetatileImage( @@ -38,10 +61,10 @@ QImage getMetatileImage( const QList &layerOpacity, bool useTruePalettes) { - QImage metatile_image(16, 16, QImage::Format_RGBA8888); + QImage metatileImage(Metatile::pixelSize(), QImage::Format_RGBA8888); if (!metatile) { - metatile_image.fill(Qt::magenta); - return metatile_image; + metatileImage.fill(getInvalidImageColor()); + return metatileImage; } QList> palettes = Tileset::getBlockPalettes(primaryTileset, secondaryTileset, useTruePalettes); @@ -50,22 +73,20 @@ QImage getMetatileImage( // tile pixels line up across layers we will still have something to render. // The GBA renders transparent pixels using palette 0 color 0. We have this color, // but all 3 games actually overwrite it with black when loading the tileset palettes, - // so we have a setting to choose between these two behaviors. - metatile_image.fill(projectConfig.setTransparentPixelsBlack ? QColor("black") : QColor(palettes.value(0).value(0))); + // so we have a setting to specify an override transparency color. + metatileImage.fill(projectConfig.transparencyColor.isValid() ? projectConfig.transparencyColor : QColor(palettes.value(0).value(0))); + + QPainter painter(&metatileImage); - QPainter metatile_painter(&metatile_image); - const int numLayers = 3; // When rendering, metatiles always have 3 layers uint32_t layerType = metatile->layerType(); - for (int layer = 0; layer < numLayers; layer++) - for (int y = 0; y < 2; y++) - for (int x = 0; x < 2; x++) { - int l = layerOrder.size() >= numLayers ? layerOrder[layer] : layer; - + for (const auto &layer : layerOrder) + for (int y = 0; y < Metatile::tileHeight(); y++) + for (int x = 0; x < Metatile::tileWidth(); x++) { // Get the tile to render next Tile tile; - int tileOffset = (y * 2) + x; + int tileOffset = (y * Metatile::tileWidth()) + x; if (projectConfig.tripleLayerMetatilesEnabled) { - tile = metatile->tiles.value(tileOffset + (l * 4)); + tile = metatile->tiles.value(tileOffset + (layer * Metatile::tilesPerLayer())); } else { // "Vanilla" metatiles only have 8 tiles, but render 12. // The remaining 4 tiles are rendered using user-specified tiles depending on layer type. @@ -73,88 +94,71 @@ QImage getMetatileImage( { default: case Metatile::LayerType::Normal: - if (l == 0) + if (layer == 0) tile = Tile(projectConfig.unusedTileNormal); else // Tiles are on layers 1 and 2 - tile = metatile->tiles.value(tileOffset + ((l - 1) * 4)); + tile = metatile->tiles.value(tileOffset + ((layer - 1) * Metatile::tilesPerLayer())); break; case Metatile::LayerType::Covered: - if (l == 2) + if (layer == 2) tile = Tile(projectConfig.unusedTileCovered); else // Tiles are on layers 0 and 1 - tile = metatile->tiles.value(tileOffset + (l * 4)); + tile = metatile->tiles.value(tileOffset + (layer * Metatile::tilesPerLayer())); break; case Metatile::LayerType::Split: - if (l == 1) + if (layer == 1) tile = Tile(projectConfig.unusedTileSplit); else // Tiles are on layers 0 and 2 - tile = metatile->tiles.value(tileOffset + ((l == 0 ? 0 : 1) * 4)); + tile = metatile->tiles.value(tileOffset + ((layer == 0 ? 0 : 1) * Metatile::tilesPerLayer())); break; } } - QImage tile_image = getTileImage(tile.tileId, primaryTileset, secondaryTileset); - if (tile_image.isNull()) { - // Some metatiles specify tiles that are outside the valid range. - // The way the GBA will render these depends on what's in memory (which Porymap can't know) - // so we treat them as if they were transparent. - continue; - } + QImage tileImage = getColoredTileImage(tile.tileId, primaryTileset, secondaryTileset, palettes.value(tile.palette)); - // Colorize the metatile tiles with its palette. - if (tile.palette < palettes.length()) { - const QList palette = palettes.value(tile.palette); - for (int j = 0; j < palette.length(); j++) { - tile_image.setColor(j, palette.value(j)); - } - } else { - logWarn(QString("Tile '%1' is referring to invalid palette number: '%2'").arg(tile.tileId).arg(tile.palette)); - } - - QPoint origin = QPoint(x*8, y*8); - float opacity = layerOpacity.size() >= numLayers ? layerOpacity[l] : 1.0; + float opacity = layerOpacity.value(layer, 1.0); if (opacity < 1.0) { int alpha = 255 * opacity; - for (int c = 0; c < tile_image.colorCount(); c++) { - QColor color(tile_image.color(c)); + for (int c = 0; c < tileImage.colorCount(); c++) { + QColor color(tileImage.color(c)); color.setAlpha(alpha); - tile_image.setColor(c, color.rgba()); + tileImage.setColor(c, color.rgba()); } } // Color 0 is displayed as transparent. - QColor color(tile_image.color(0)); - color.setAlpha(0); - tile_image.setColor(0, color.rgba()); - tile.flip(&tile_image); - metatile_painter.drawImage(origin, tile_image); - } - metatile_painter.end(); + if (tileImage.colorCount()) { + QColor color(tileImage.color(0)); + color.setAlpha(0); + tileImage.setColor(0, color.rgba()); + } - return metatile_image; + tile.flip(&tileImage); + painter.drawImage(x * Tile::pixelWidth(), y * Tile::pixelHeight(), tileImage); + } + painter.end(); + + return metatileImage; } QImage getTileImage(uint16_t tileId, Tileset *primaryTileset, Tileset *secondaryTileset) { Tileset *tileset = Tileset::getTileTileset(tileId, primaryTileset, secondaryTileset); - int index = Tile::getIndexInTileset(tileId); - if (!tileset) { - return QImage(); - } - return tileset->tiles.value(index, QImage()); + return tileset ? tileset->tileImage(tileId) : QImage(); } QImage getColoredTileImage(uint16_t tileId, Tileset *primaryTileset, Tileset *secondaryTileset, const QList &palette) { QImage tileImage = getTileImage(tileId, primaryTileset, secondaryTileset); if (tileImage.isNull()) { - tileImage = QImage(8, 8, QImage::Format_RGBA8888); - QPainter painter(&tileImage); - painter.fillRect(0, 0, 8, 8, palette.at(0)); + // Some tiles specify tile IDs or palette IDs that are outside the valid range. + // The way the GBA will render these depends on what's in memory (which Porymap can't know) + // so we render them using the invalid color + tileImage = QImage(Tile::pixelSize(), QImage::Format_RGBA8888); + tileImage.fill(getInvalidImageColor()); } else { for (int i = 0; i < 16; i++) { - tileImage.setColor(i, palette.at(i)); + tileImage.setColor(i, palette.value(i, getInvalidImageColor().rgb())); } } - return tileImage; } @@ -175,3 +179,94 @@ void flattenTo4bppImage(QImage * image) { for (int i = 0; i < image->sizeInBytes(); i++, pixel++) *pixel %= 16; } + +// Constructs a grid image of the metatiles in the specified ID range. +QImage getMetatileSheetImage(Tileset *primaryTileset, + Tileset *secondaryTileset, + uint16_t metatileIdStart, + uint16_t metatileIdEnd, + int numMetatilesWide, + const QList &layerOrder, + const QList &layerOpacity, + const QSize &metatileSize, + bool useTruePalettes) +{ + if (metatileIdEnd < metatileIdStart || numMetatilesWide <= 0) + return QImage(); + + int numMetatilesToDraw = metatileIdEnd - metatileIdStart + 1; + + // Round up image height for incomplete last row. + int numMetatilesTall = Util::roundUpToMultiple(numMetatilesToDraw, numMetatilesWide) / numMetatilesWide; + + QImage image(numMetatilesWide * metatileSize.width(), numMetatilesTall * metatileSize.height(), QImage::Format_RGBA8888); + image.fill(getInvalidImageColor()); + + QPainter painter(&image); + for (int i = 0; i < numMetatilesToDraw; i++) { + uint16_t metatileId = i + metatileIdStart; + QImage metatileImage = getMetatileImage(metatileId, primaryTileset, secondaryTileset, layerOrder, layerOpacity, useTruePalettes) + .scaled(metatileSize); + + int x = (i % numMetatilesWide) * metatileSize.width(); + int y = (i / numMetatilesWide) * metatileSize.height(); + painter.drawImage(x, y, metatileImage); + } + painter.end(); + return image; +} + +// Constructs a grid image of the metatiles in the primary and secondary tileset, +// rounding as necessary to keep the two tilesets on separate rows. +// The unused metatiles (if any) between the primary and secondary tilesets are skipped. +QImage getMetatileSheetImage(Tileset *primaryTileset, + Tileset *secondaryTileset, + int numMetatilesWide, + const QList &layerOrder, + const QList &layerOpacity, + const QSize &metatileSize, + bool useTruePalettes) +{ + auto createSheetImage = [=](uint16_t start, Tileset *tileset) { + uint16_t end = start; + if (tileset) { + if (tileset->numMetatiles() == 0) + return QImage(); + end += tileset->numMetatiles() - 1; + } + return getMetatileSheetImage(primaryTileset, + secondaryTileset, + start, + end, + numMetatilesWide, + layerOrder, + layerOpacity, + metatileSize, + useTruePalettes); + }; + + QImage primaryImage = createSheetImage(0, primaryTileset); + QImage secondaryImage = createSheetImage(Project::getNumMetatilesPrimary(), secondaryTileset); + + QImage image(qMax(primaryImage.width(), secondaryImage.width()), primaryImage.height() + secondaryImage.height(), QImage::Format_RGBA8888); + image.fill(getInvalidImageColor()); + + QPainter painter(&image); + painter.drawImage(0, 0, primaryImage); + painter.drawImage(0, primaryImage.height(), secondaryImage); + painter.end(); + + return image; +} + +QImage getMetatileSheetImage(Layout *layout, int numMetatilesWide, bool useTruePalettes) { + if (!layout) + return QImage(); + return getMetatileSheetImage(layout->tileset_primary, + layout->tileset_secondary, + numMetatilesWide, + layout->metatileLayerOrder(), + layout->metatileLayerOpacity(), + Metatile::pixelSize(), + useTruePalettes); +} diff --git a/src/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index 59efbe4b..30c6ff72 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -46,7 +46,7 @@ MapImageExporter::MapImageExporter(QWidget *parent, Project *project, Map *map, setAttribute(Qt::WA_DeleteOnClose); ui->setupUi(this); - m_scene = new QGraphicsScene(this); + m_scene = new CheckeredBgScene(QSize(8,8), this); m_preview = m_scene->addPixmap(QPixmap()); ui->graphicsView_Preview->setScene(m_scene); @@ -333,8 +333,8 @@ QGifImage* MapImageExporter::createTimelapseGifImage(QProgressDialog *progress) if (currentHistoryAppliesToFrame(step.historyStack) || step.historyStack->index() == step.initialStackIndex) { // Either this is relevant edit history, or it's the final frame (which is always rendered). Record the size of the map at this point. QMargins margins = getMargins(m_map); - canvasSize = canvasSize.expandedTo(QSize(m_layout->getWidth() * 16 + margins.left() + margins.right(), - m_layout->getHeight() * 16 + margins.top() + margins.bottom())); + canvasSize = canvasSize.expandedTo(QSize(m_layout->pixelWidth() + margins.left() + margins.right(), + m_layout->pixelHeight() + margins.top() + margins.bottom())); } if (step.historyStack->canUndo()){ step.historyStack->undo(); @@ -434,8 +434,8 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) { if (!connection->isCardinal()) continue; Map *connectedMap = connection->targetMap(); if (!connectedMap) continue; - QPoint pos = connection->relativePos(); - unvisited.append(StitchedMap{cur.x + (pos.x() * 16), cur.y + (pos.y() * 16), connectedMap}); + QPoint pos = connection->relativePixelPos(); + unvisited.append(StitchedMap{cur.x + pos.x(), cur.y + pos.y(), connectedMap}); } } if (stitchedMaps.isEmpty()) @@ -447,7 +447,7 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) { // Determine the overall dimensions of the stitched maps. QRect dimensions = QRect(0, 0, m_map->getWidth(), m_map->getHeight()) + getMargins(m_map); for (const StitchedMap &map : stitchedMaps) { - dimensions |= (QRect(map.x, map.y, map.map->getWidth() * 16, map.map->getHeight() * 16) + getMargins(map.map)); + dimensions |= (QRect(map.x, map.y, map.map->pixelWidth(), map.map->pixelHeight()) + getMargins(map.map)); } QPixmap stitchedPixmap(dimensions.width(), dimensions.height()); @@ -602,7 +602,7 @@ QPixmap MapImageExporter::getFormattedMapPixmap() { QMargins MapImageExporter::getMargins(const Map *map) { QMargins margins; if (m_settings.showBorder) { - margins = m_project->getMetatileViewDistance() * 16; + margins = m_project->getPixelViewDistance(); } else if (map && connectionsEnabled()) { for (const auto &connection : map->getConnections()) { const QString dir = connection->direction(); @@ -612,10 +612,10 @@ QMargins MapImageExporter::getMargins(const Map *map) { if (!targetMap) continue; QRect rect = targetMap->getConnectionRect(dir); - if (dir == "up") margins.setTop(rect.height() * 16); - else if (dir == "down") margins.setBottom(rect.height() * 16); - else if (dir == "left") margins.setLeft(rect.width() * 16); - else if (dir == "right") margins.setRight(rect.width() * 16); + if (dir == "up") margins.setTop(rect.height()); + else if (dir == "down") margins.setBottom(rect.height()); + else if (dir == "left") margins.setLeft(rect.width()); + else if (dir == "right") margins.setRight(rect.width()); } } if (m_settings.showGrid) { @@ -652,7 +652,7 @@ void MapImageExporter::paintBorder(QPainter *painter, Layout *layout) { // Skip border painting if it would be fully covered by the rest of the map if (layout->isWithinBounds(QRect(x, y, layout->getBorderWidth(), layout->getBorderHeight()))) continue; - painter->drawPixmap(x * 16, y * 16, layout->border_pixmap); + painter->drawPixmap(x * Metatile::pixelWidth(), y * Metatile::pixelHeight(), layout->border_pixmap); } painter->restore(); @@ -665,7 +665,7 @@ void MapImageExporter::paintConnections(QPainter *painter, const Map *map) { for (const auto &connection : map->getConnections()) { if (!m_settings.showConnections.contains(connection->direction())) continue; - painter->drawImage(connection->relativePos(true) * 16, connection->render().toImage()); + painter->drawImage(connection->relativePixelPos(true), connection->render().toImage()); } } @@ -693,12 +693,12 @@ void MapImageExporter::paintGrid(QPainter *painter, const Layout *layout) { if (!m_settings.showGrid) return; - int w = layout->getWidth() * 16; - int h = layout->getHeight() * 16; - for (int x = 0; x <= w; x += 16) { + int w = layout->pixelWidth(); + int h = layout->pixelHeight(); + for (int x = 0; x <= w; x += Metatile::pixelWidth()) { painter->drawLine(x, 0, x, h); } - for (int y = 0; y <= h; y += 16) { + for (int y = 0; y <= h; y += Metatile::pixelHeight()) { painter->drawLine(0, y, w, y); } } diff --git a/src/ui/mapruler.cpp b/src/ui/mapruler.cpp index 908e0a46..da83821c 100644 --- a/src/ui/mapruler.cpp +++ b/src/ui/mapruler.cpp @@ -37,9 +37,11 @@ QPainterPath MapRuler::shape() const { ruler.addRect(xRuler); ruler.addRect(yRuler); ruler = ruler.simplified(); - for (int x = 16; x < pixWidth(); x += 16) + int w = Metatile::pixelWidth(); + int h = Metatile::pixelHeight(); + for (int x = w; x < pixWidth(); x += w) ruler.addRect(x, xRuler.y(), 0, thickness); - for (int y = 16; y < pixHeight(); y += 16) + for (int y = h; y < pixHeight(); y += h) ruler.addRect(yRuler.x(), y, thickness, 0); if (deltaX() && deltaY()) ruler.addPolygon(QVector({ cornerTick.p1(), cornerTick.p2() })); @@ -131,7 +133,9 @@ QPoint MapRuler::snapToWithinBounds(QPoint pos) const { void MapRuler::updateGeometry() { prepareGeometryChange(); - setPos(QPoint(left() * 16 + 8, top() * 16 + 8)); + int w = Metatile::pixelWidth(); + int h = Metatile::pixelHeight(); + setPos(QPoint(left() * w + w/2, top() * h + h/2)); /* Determine what quadrant the end point is in relative to the anchor point. The anchor * point is the top-left corner of the metatile the ruler starts in, so a zero-length * ruler is considered to be in the bottom-right quadrant from the anchor point. */ diff --git a/src/ui/metatileimageexporter.cpp b/src/ui/metatileimageexporter.cpp new file mode 100644 index 00000000..92dc5079 --- /dev/null +++ b/src/ui/metatileimageexporter.cpp @@ -0,0 +1,364 @@ +#include "metatileimageexporter.h" +#include "ui_metatileimageexporter.h" +#include "filedialog.h" +#include "imageproviders.h" +#include "utility.h" +#include "project.h" +#include "metatile.h" + +#include + +MetatileImageExporter::MetatileImageExporter(QWidget *parent, Tileset *primaryTileset, Tileset *secondaryTileset, Settings *savedSettings) : + QDialog(parent), + ui(new Ui::MetatileImageExporter), + m_primaryTileset(primaryTileset), + m_secondaryTileset(secondaryTileset), + m_savedSettings(savedSettings) +{ + ui->setupUi(this); + m_transparencyButtons = { + ui->radioButton_TransparencyNormal, + ui->radioButton_TransparencyBlack, + ui->radioButton_TransparencyFirst, + }; + + m_scene = new CheckeredBgScene(QSize(8,8), this); + m_preview = m_scene->addPixmap(QPixmap()); + ui->graphicsView_Preview->setScene(m_scene); + + if (projectConfig.tripleLayerMetatilesEnabled) { + // When triple-layer metatiles are enabled there is no unused layer, + // so this setting becomes pointless. Disable it. + ui->checkBox_Placeholders->setVisible(false); + } + + uint16_t maxMetatileId = Block::getMaxMetatileId(); + ui->spinBox_MetatileStart->setMaximum(maxMetatileId); + ui->spinBox_MetatileEnd->setMaximum(maxMetatileId); + ui->spinBox_WidthMetatiles->setRange(1, maxMetatileId); + ui->spinBox_WidthPixels->setRange(1 * Metatile::pixelWidth(), maxMetatileId * Metatile::pixelWidth()); + + if (m_primaryTileset) { + ui->comboBox_PrimaryTileset->setTextItem(m_primaryTileset->name); + } + if (m_secondaryTileset) { + ui->comboBox_SecondaryTileset->setTextItem(m_secondaryTileset->name); + } + + if (m_savedSettings) { + populate(*m_savedSettings); + } else { + populate({}); + } + + connect(ui->listWidget_Layers, &ReorderableListWidget::itemChanged, this, &MetatileImageExporter::updatePreview); + connect(ui->listWidget_Layers, &ReorderableListWidget::reordered, this, &MetatileImageExporter::updatePreview); + + connect(ui->pushButton_Save, &QPushButton::pressed, [this] { saveImage(); }); + connect(ui->pushButton_Close, &QPushButton::pressed, this, &MetatileImageExporter::close); + connect(ui->pushButton_Reset, &QPushButton::pressed, this, &MetatileImageExporter::reset); + + connect(ui->spinBox_WidthMetatiles, &UIntSpinBox::valueChanged, this, &MetatileImageExporter::syncPixelWidth); + connect(ui->spinBox_WidthMetatiles, &UIntSpinBox::valueChanged, this, &MetatileImageExporter::queuePreviewUpdate); + connect(ui->spinBox_WidthMetatiles, &UIntSpinBox::editingFinished, this, &MetatileImageExporter::tryUpdatePreview); + + connect(ui->spinBox_WidthPixels, &UIntSpinBox::valueChanged, this, &MetatileImageExporter::syncMetatileWidth); + connect(ui->spinBox_WidthPixels, &UIntSpinBox::valueChanged, this, &MetatileImageExporter::queuePreviewUpdate); + connect(ui->spinBox_WidthPixels, &UIntSpinBox::editingFinished, this, &MetatileImageExporter::syncPixelWidth); // Round pixel width to multiple of 16 + connect(ui->spinBox_WidthPixels, &UIntSpinBox::editingFinished, this, &MetatileImageExporter::tryUpdatePreview); + + connect(ui->spinBox_MetatileStart, &UIntHexSpinBox::valueChanged, this, &MetatileImageExporter::validateMetatileEnd); + connect(ui->spinBox_MetatileStart, &UIntHexSpinBox::valueChanged, this, &MetatileImageExporter::queuePreviewUpdate); + connect(ui->spinBox_MetatileStart, &UIntHexSpinBox::editingFinished, this, &MetatileImageExporter::tryUpdatePreview); + + connect(ui->spinBox_MetatileEnd, &UIntHexSpinBox::valueChanged, this, &MetatileImageExporter::validateMetatileStart); + connect(ui->spinBox_MetatileEnd, &UIntHexSpinBox::valueChanged, this, &MetatileImageExporter::queuePreviewUpdate); + connect(ui->spinBox_MetatileEnd, &UIntHexSpinBox::editingFinished, this, &MetatileImageExporter::tryUpdatePreview); + + // If we used toggled instead of clicked we'd get two preview updates instead of one when the setting changes. + connect(ui->radioButton_TransparencyNormal, &QRadioButton::clicked, this, &MetatileImageExporter::updatePreview); + connect(ui->radioButton_TransparencyBlack, &QRadioButton::clicked, this, &MetatileImageExporter::updatePreview); + connect(ui->radioButton_TransparencyFirst, &QRadioButton::clicked, this, &MetatileImageExporter::updatePreview); + + connect(ui->checkBox_Placeholders, &QCheckBox::toggled, this, &MetatileImageExporter::updatePreview); + connect(ui->checkBox_PrimaryTileset, &QCheckBox::toggled, this, &MetatileImageExporter::tryEnforceMetatileRange); + connect(ui->checkBox_PrimaryTileset, &QCheckBox::toggled, this, &MetatileImageExporter::updatePreview); + connect(ui->checkBox_SecondaryTileset, &QCheckBox::toggled, this, &MetatileImageExporter::tryEnforceMetatileRange); + connect(ui->checkBox_SecondaryTileset, &QCheckBox::toggled, this, &MetatileImageExporter::updatePreview); + + ui->graphicsView_Preview->setFocus(); +} + +MetatileImageExporter::~MetatileImageExporter() { + delete ui; +} + +// Allow the window to open before displaying the preview. +// Metatile sheet image creation is generally quick, so this only +// really matters so that the graphics view can adjust the scale properly. +void MetatileImageExporter::showEvent(QShowEvent *event) { + QDialog::showEvent(event); + if (!event->spontaneous()) { + QTimer::singleShot(0, this, &MetatileImageExporter::updatePreview); + } +} + +void MetatileImageExporter::closeEvent(QCloseEvent *event) { + if (m_savedSettings) { + m_savedSettings->metatileStart = ui->spinBox_MetatileStart->value(); + m_savedSettings->metatileEnd = ui->spinBox_MetatileEnd->value(); + m_savedSettings->numMetatilesWide = ui->spinBox_WidthMetatiles->value(); + m_savedSettings->usePrimaryTileset = ui->checkBox_PrimaryTileset->isChecked(); + m_savedSettings->useSecondaryTileset = ui->checkBox_SecondaryTileset->isChecked(); + m_savedSettings->renderPlaceholders = ui->checkBox_Placeholders->isChecked(); + for (int i = 0; i < m_transparencyButtons.length(); i++) { + if (m_transparencyButtons.at(i)->isChecked()) { + m_savedSettings->transparencyMode = i; + break; + } + } + m_savedSettings->layerOrder.clear(); + for (int i = 0; i < ui->listWidget_Layers->count(); i++) { + auto item = ui->listWidget_Layers->item(i); + int layerNum = item->data(Qt::UserRole).toInt(); + m_savedSettings->layerOrder[layerNum] = (item->checkState() == Qt::Checked); + } + } + QDialog::closeEvent(event); +} + +void MetatileImageExporter::populate(const Settings &settings) { + const QSignalBlocker b_MetatileStart(ui->spinBox_MetatileStart); + ui->spinBox_MetatileStart->setValue(settings.metatileStart); + + const QSignalBlocker b_MetatileEnd(ui->spinBox_MetatileStart); + ui->spinBox_MetatileEnd->setValue(settings.metatileEnd); + + const QSignalBlocker b_WidthMetatiles(ui->spinBox_MetatileStart); + ui->spinBox_WidthMetatiles->setValue(settings.numMetatilesWide); + + const QSignalBlocker b_WidthPixels(ui->spinBox_MetatileStart); + ui->spinBox_WidthPixels->setValue(settings.numMetatilesWide * Metatile::pixelWidth()); + + const QSignalBlocker b_PrimaryTileset(ui->spinBox_MetatileStart); + ui->checkBox_PrimaryTileset->setChecked(settings.usePrimaryTileset); + + const QSignalBlocker b_SecondaryTileset(ui->spinBox_MetatileStart); + ui->checkBox_SecondaryTileset->setChecked(settings.useSecondaryTileset); + + const QSignalBlocker b_Placeholders(ui->spinBox_MetatileStart); + ui->checkBox_Placeholders->setChecked(settings.renderPlaceholders); + + if (m_transparencyButtons.value(settings.transparencyMode)) { + auto button = m_transparencyButtons[settings.transparencyMode]; + const QSignalBlocker b_Transparency(button); + button->setChecked(true); + } + + // Build layer list from settings + const QSignalBlocker b_Layers(ui->listWidget_Layers); + ui->listWidget_Layers->clear(); + for (auto it = settings.layerOrder.cbegin(); it != settings.layerOrder.cend(); it++) { + int layerNum = it.key(); + bool enabled = it.value(); + + auto item = new QListWidgetItem(Metatile::getLayerName(layerNum)); + item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsUserCheckable | Qt::ItemNeverHasChildren); + item->setCheckState(enabled ? Qt::Checked : Qt::Unchecked); + item->setData(Qt::UserRole, layerNum); // Save the original index to identify the layer + ui->listWidget_Layers->addItem(item); + } + // Don't give extra unnecessary space to the list + ui->listWidget_Layers->setFixedHeight(ui->listWidget_Layers->sizeHintForRow(0) * ui->listWidget_Layers->count() + 4); + + tryEnforceMetatileRange(); +} + +void MetatileImageExporter::applySettings(const Settings &settings) { + populate(settings); + updatePreview(); +} + +void MetatileImageExporter::reset() { + applySettings({}); +} + +QImage MetatileImageExporter::getImage() { + tryUpdatePreview(); + return m_preview->pixmap().toImage(); +} + +bool MetatileImageExporter::saveImage(QString filepath) { + tryUpdatePreview(); + if (filepath.isEmpty()) { + QString defaultFilepath = QString("%1/%2").arg(FileDialog::getDirectory()).arg(getDefaultFileName()); + filepath = FileDialog::getSaveFileName(this, windowTitle(), defaultFilepath, QStringLiteral("Image Files (*.png *.jpg *.bmp)")); + if (filepath.isEmpty()) { + return false; + } + } + return m_preview->pixmap().save(filepath); +} + +QString MetatileImageExporter::getDefaultFileName() const { + if (m_layerOrder.length() == 1) { + // Exporting a metatile layer image is an expected use case for Porytiles, which expects certain file names. + // We can make the process a little easier by setting the default file name to those expected file names. + static const QStringList layerFilenames = { "bottom", "middle", "top" }; + return layerFilenames.at(m_layerOrder.constFirst()) + ".png"; + } + + QString defaultFilename; + if (ui->checkBox_PrimaryTileset->isChecked() && m_primaryTileset) { + defaultFilename.append(QString("%1_").arg(Tileset::stripPrefix(m_primaryTileset->name))); + } + if (ui->checkBox_SecondaryTileset->isChecked() && m_secondaryTileset) { + defaultFilename.append(QString("%1_").arg(Tileset::stripPrefix(m_secondaryTileset->name))); + } + if (!m_layerOrder.isEmpty() && m_layerOrder != QList({0,1,2})) { + for (int i = m_layerOrder.length() - 1; i >= 0; i--) { + defaultFilename.append(Metatile::getLayerName(m_layerOrder.at(i))); + } + defaultFilename.append("_"); + } + defaultFilename.append("Metatiles.png"); + return defaultFilename; +} + +void MetatileImageExporter::queuePreviewUpdate() { + m_previewUpdateQueued = true; +} + +// For updating only when a change has been recorded. +// Useful for something that might happen often, like an input widget losing focus. +void MetatileImageExporter::tryUpdatePreview() { + if (m_preview->pixmap().isNull() || m_previewUpdateQueued) { + updatePreview(); + } +} + +void MetatileImageExporter::updatePreview() { + copyRenderSettings(); + + m_layerOrder.clear(); + for (int i = 0; i < ui->listWidget_Layers->count(); i++) { + auto item = ui->listWidget_Layers->item(i); + if (item->checkState() == Qt::Checked) { + int layerNum = item->data(Qt::UserRole).toInt(); + m_layerOrder.prepend(qBound(0, layerNum, 2)); + } + } + + QImage previewImage; + if (ui->checkBox_PrimaryTileset->isChecked() && ui->checkBox_SecondaryTileset->isChecked()) { + // Special behavior to combine the two tilesets while skipping the unused region between tilesets. + previewImage = getMetatileSheetImage(m_primaryTileset, + m_secondaryTileset, + ui->spinBox_WidthMetatiles->value(), + m_layerOrder); + } else { + previewImage = getMetatileSheetImage(m_primaryTileset, + m_secondaryTileset, + ui->spinBox_MetatileStart->value(), + ui->spinBox_MetatileEnd->value(), + ui->spinBox_WidthMetatiles->value(), + m_layerOrder); + } + + m_preview->setPixmap(QPixmap::fromImage(previewImage)); + m_scene->setSceneRect(m_scene->itemsBoundingRect()); + m_previewUpdateQueued = false; + + restoreRenderSettings(); +} + +void MetatileImageExporter::validateMetatileStart() { + const QSignalBlocker b(ui->spinBox_MetatileStart); + ui->spinBox_MetatileStart->setValue(qMin(ui->spinBox_MetatileStart->value(), + ui->spinBox_MetatileEnd->value())); +} + +void MetatileImageExporter::validateMetatileEnd() { + const QSignalBlocker b(ui->spinBox_MetatileEnd); + ui->spinBox_MetatileEnd->setValue(qMax(ui->spinBox_MetatileStart->value(), + ui->spinBox_MetatileEnd->value())); +} + +void MetatileImageExporter::updateMetatileRange() { + uint16_t min; + uint16_t max; + if (ui->checkBox_PrimaryTileset->isChecked() && m_primaryTileset) { + if (ui->checkBox_SecondaryTileset->isChecked() && m_secondaryTileset) { + // Both tilesets enforced + min = qMin(m_primaryTileset->firstMetatileId(), m_secondaryTileset->firstMetatileId()); + max = qMax(m_primaryTileset->lastMetatileId(), m_secondaryTileset->lastMetatileId()); + } else { + // Primary enforced + min = m_primaryTileset->firstMetatileId(); + max = m_primaryTileset->lastMetatileId(); + } + } else if (ui->checkBox_SecondaryTileset->isChecked() && m_secondaryTileset) { + // Secondary enforced + min = m_secondaryTileset->firstMetatileId(); + max = m_secondaryTileset->lastMetatileId(); + } else { + // No tilesets enforced + return; + } + + const QSignalBlocker b_MetatileStart(ui->spinBox_MetatileStart); + const QSignalBlocker b_MetatileEnd(ui->spinBox_MetatileEnd); + ui->spinBox_MetatileStart->setValue(min); + ui->spinBox_MetatileEnd->setValue(max); +} + +void MetatileImageExporter::tryEnforceMetatileRange() { + // Users can either specify which tileset(s) to render, or specify a range of metatiles, but not both. + if (ui->checkBox_PrimaryTileset->isChecked() || ui->checkBox_SecondaryTileset->isChecked()) { + updateMetatileRange(); + ui->groupBox_MetatileRange->setDisabled(true); + } else { + ui->groupBox_MetatileRange->setDisabled(false); + } +} + +void MetatileImageExporter::syncPixelWidth() { + const QSignalBlocker b(ui->spinBox_WidthPixels); + ui->spinBox_WidthPixels->setValue(ui->spinBox_WidthMetatiles->value() * Metatile::pixelWidth()); +} + +void MetatileImageExporter::syncMetatileWidth() { + const QSignalBlocker b(ui->spinBox_WidthMetatiles); + ui->spinBox_WidthMetatiles->setValue(Util::roundUpToMultiple(ui->spinBox_WidthPixels->value(), Metatile::pixelWidth()) / Metatile::pixelWidth()); +} + +// These settings control some rendering behavior that make metatiles render accurately to their in-game appearance, +// which may be undesirable when exporting metatile images for editing. +// The settings are buried in getMetatileImage at the moment, to change them we'll temporarily overwrite them. +void MetatileImageExporter::copyRenderSettings() { + m_savedConfig.transparencyColor = projectConfig.transparencyColor; + m_savedConfig.unusedTileNormal = projectConfig.unusedTileNormal; + m_savedConfig.unusedTileCovered = projectConfig.unusedTileCovered; + m_savedConfig.unusedTileSplit = projectConfig.unusedTileSplit; + + if (ui->radioButton_TransparencyNormal->isChecked()) { + projectConfig.transparencyColor = QColor(Qt::transparent); + } else if (ui->radioButton_TransparencyBlack->isChecked()) { + projectConfig.transparencyColor = QColor(Qt::black); + } else { + projectConfig.transparencyColor = QColor(); + } + + if (!ui->checkBox_Placeholders->isChecked()) { + projectConfig.unusedTileNormal = 0; + projectConfig.unusedTileCovered = 0; + projectConfig.unusedTileSplit = 0; + } +} + +void MetatileImageExporter::restoreRenderSettings() { + projectConfig.transparencyColor = m_savedConfig.transparencyColor; + projectConfig.unusedTileNormal = m_savedConfig.unusedTileNormal; + projectConfig.unusedTileCovered = m_savedConfig.unusedTileCovered; + projectConfig.unusedTileSplit = m_savedConfig.unusedTileSplit; +} diff --git a/src/ui/metatilelayersitem.cpp b/src/ui/metatilelayersitem.cpp index a26ed14b..bae09ee6 100644 --- a/src/ui/metatilelayersitem.cpp +++ b/src/ui/metatilelayersitem.cpp @@ -4,7 +4,7 @@ #include MetatileLayersItem::MetatileLayersItem(Metatile *metatile, Tileset *primaryTileset, Tileset *secondaryTileset) - : SelectablePixmapItem(16, 16, 2 * projectConfig.getNumLayersInMetatile(), 2), + : SelectablePixmapItem(16, 16, Metatile::tileWidth() * projectConfig.getNumLayersInMetatile(), Metatile::tileHeight()), metatile(metatile), primaryTileset(primaryTileset), secondaryTileset(secondaryTileset) @@ -31,8 +31,8 @@ static const QList tilePositions = { void MetatileLayersItem::draw() { const int numLayers = projectConfig.getNumLayersInMetatile(); - const int layerWidth = this->cellWidth * 2; - const int layerHeight = this->cellHeight * 2; + const int layerWidth = this->cellWidth * Metatile::tileWidth(); + const int layerHeight = this->cellHeight * Metatile::tileHeight(); QPixmap pixmap(numLayers * layerWidth, layerHeight); QPainter painter(&pixmap); @@ -129,7 +129,7 @@ void MetatileLayersItem::hoverMoveEvent(QGraphicsSceneHoverEvent * event) { if (tileIndex < 0 || tileIndex >= this->metatile->tiles.length()) return; - emit this->hoveredTileChanged(this->metatile->tiles.at(tileIndex).tileId); + emit this->hoveredTileChanged(this->metatile->tiles.at(tileIndex)); } void MetatileLayersItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *) { @@ -148,6 +148,6 @@ void MetatileLayersItem::clearLastHoveredCoords() { QPoint MetatileLayersItem::getBoundedPos(const QPointF &pos) { int x = static_cast(pos.x()) / this->cellWidth; int y = static_cast(pos.y()) / this->cellHeight; - return QPoint( qMax(0, qMin(x, this->maxSelectionWidth - 1)), - qMax(0, qMin(y, this->maxSelectionHeight - 1)) ); + return QPoint(qBound(0, x, this->maxSelectionWidth - 1), + qBound(0, y, this->maxSelectionHeight - 1)); } diff --git a/src/ui/metatileselector.cpp b/src/ui/metatileselector.cpp index 828c73cd..38cc50be 100644 --- a/src/ui/metatileselector.cpp +++ b/src/ui/metatileselector.cpp @@ -3,40 +3,20 @@ #include "project.h" #include -QPoint MetatileSelector::getSelectionDimensions() { +QPoint MetatileSelector::getSelectionDimensions() const { if (this->prefabSelection || this->externalSelection) return selection.dimensions; return SelectablePixmapItem::getSelectionDimensions(); } int MetatileSelector::numPrimaryMetatilesRounded() const { - // We round up the number of primary metatiles to keep the tilesets on separate rows. - return ceil((double)this->primaryTileset->numMetatiles() / this->numMetatilesWide) * this->numMetatilesWide; + if (!primaryTileset()) + return 0; + return Util::roundUpToMultiple(primaryTileset()->numMetatiles(), this->numMetatilesWide); } void MetatileSelector::updateBasePixmap() { - int primaryLength = this->numPrimaryMetatilesRounded(); - int length_ = primaryLength + this->secondaryTileset->numMetatiles(); - int height_ = length_ / this->numMetatilesWide; - if (length_ % this->numMetatilesWide != 0) { - height_++; - } - QImage image(this->numMetatilesWide * this->cellWidth, height_ * this->cellHeight, QImage::Format_RGBA8888); - image.fill(Qt::magenta); - QPainter painter(&image); - for (int i = 0; i < length_; i++) { - int tile = i; - if (i >= primaryLength) { - tile += Project::getNumMetatilesPrimary() - primaryLength; - } - QImage metatile_image = getMetatileImage(tile, this->primaryTileset, this->secondaryTileset, layout->metatileLayerOrder, layout->metatileLayerOpacity); - int map_y = i / this->numMetatilesWide; - int map_x = i % this->numMetatilesWide; - QPoint metatile_origin = QPoint(map_x * this->cellWidth, map_y * this->cellHeight); - painter.drawImage(metatile_origin, metatile_image); - } - painter.end(); - this->basePixmap = QPixmap::fromImage(image); + this->basePixmap = QPixmap::fromImage(getMetatileSheetImage(this->layout, this->numMetatilesWide)); } void MetatileSelector::draw() { @@ -53,7 +33,11 @@ void MetatileSelector::drawSelection() { } bool MetatileSelector::select(uint16_t metatileId) { - if (!Tileset::metatileIsValid(metatileId, this->primaryTileset, this->secondaryTileset)) return false; + bool ok; + QPoint pos = metatileIdToPos(metatileId, &ok); + if (!ok) { + return false; + } this->externalSelection = false; this->prefabSelection = false; this->selection = MetatileSelection{ @@ -62,8 +46,7 @@ bool MetatileSelector::select(uint16_t metatileId) { QList({MetatileSelectionItem{true, metatileId}}), QList(), }; - QPoint coords = this->getMetatileIdCoords(metatileId); - SelectablePixmapItem::select(coords.x(), coords.y(), 0, 0); + SelectablePixmapItem::select(pos); this->updateSelectedMetatiles(); return true; } @@ -73,9 +56,8 @@ void MetatileSelector::selectFromMap(uint16_t metatileId, uint16_t collision, ui this->setExternalSelection(1, 1, {metatileId}, {movePermissions}); } -void MetatileSelector::setTilesets(Tileset *primaryTileset, Tileset *secondaryTileset) { - this->primaryTileset = primaryTileset; - this->secondaryTileset = secondaryTileset; +void MetatileSelector::setLayout(Layout *layout) { + this->layout = layout; if (this->externalSelection) this->updateExternalSelectedMetatiles(); else @@ -85,11 +67,11 @@ void MetatileSelector::setTilesets(Tileset *primaryTileset, Tileset *secondaryTi draw(); } -MetatileSelection MetatileSelector::getMetatileSelection() { - return selection; +void MetatileSelector::refresh() { + setLayout(this->layout); } -void MetatileSelector::setExternalSelection(int width, int height, QList metatiles, QList> collisions) { +void MetatileSelector::setExternalSelection(int width, int height, const QList &metatiles, const QList> &collisions) { this->prefabSelection = false; this->externalSelection = true; this->externalSelectionWidth = width; @@ -99,18 +81,18 @@ void MetatileSelector::setExternalSelection(int width, int height, QListselection.collisionItems.clear(); this->selection.hasCollision = true; this->selection.dimensions = QPoint(width, height); - for (int i = 0; i < metatiles.length(); i++) { - auto collision = collisions.at(i); - this->selection.collisionItems.append(CollisionSelectionItem{true, collision.first, collision.second}); + for (int i = 0; i < qMin(metatiles.length(), collisions.length()); i++) { uint16_t metatileId = metatiles.at(i); + uint16_t collision = collisions.at(i).first; + uint16_t elevation = collisions.at(i).second; + this->selection.collisionItems.append(CollisionSelectionItem{true, collision, elevation}); this->externalSelectedMetatiles.append(metatileId); - if (!Tileset::metatileIsValid(metatileId, this->primaryTileset, this->secondaryTileset)) + if (!this->layout->metatileIsValid(metatileId)) metatileId = 0; this->selection.metatileItems.append(MetatileSelectionItem{true, metatileId}); } if (this->selection.metatileItems.length() == 1) { - QPoint coords = this->getMetatileIdCoords(this->selection.metatileItems.first().metatileId); - SelectablePixmapItem::select(coords.x(), coords.y(), 0, 0); + SelectablePixmapItem::select(metatileIdToPos(this->selection.metatileItems.first().metatileId)); } this->draw(); @@ -127,7 +109,9 @@ void MetatileSelector::setPrefabSelection(MetatileSelection selection) { } bool MetatileSelector::positionIsValid(const QPoint &pos) const { - return Tileset::metatileIsValid(getMetatileId(pos.x(), pos.y()), this->primaryTileset, this->secondaryTileset); + bool ok; + posToMetatileId(pos, &ok); + return ok; } void MetatileSelector::mousePressEvent(QGraphicsSceneMouseEvent *event) { @@ -168,8 +152,14 @@ void MetatileSelector::hoverMoveEvent(QGraphicsSceneHoverEvent *event) { } void MetatileSelector::hoverChanged() { - uint16_t metatileId = this->getMetatileId(this->cellPos.x(), this->cellPos.y()); - emit this->hoveredMetatileSelectionChanged(metatileId); + bool ok; + uint16_t metatileId = posToMetatileId(this->cellPos, &ok); + if (ok) { + emit this->hoveredMetatileSelectionChanged(metatileId); + } else { + emit this->hoveredMetatileSelectionCleared(); + this->cellPos = QPoint(-1, -1); + } } void MetatileSelector::hoverLeaveEvent(QGraphicsSceneHoverEvent*) { @@ -187,9 +177,7 @@ void MetatileSelector::updateSelectedMetatiles() { QPoint origin = this->getSelectionStart(); for (int j = 0; j < this->selection.dimensions.y(); j++) { for (int i = 0; i < this->selection.dimensions.x(); i++) { - uint16_t metatileId = this->getMetatileId(origin.x() + i, origin.y() + j); - if (!Tileset::metatileIsValid(metatileId, this->primaryTileset, this->secondaryTileset)) - metatileId = 0; + uint16_t metatileId = posToMetatileId(origin.x() + i, origin.y() + j); this->selection.metatileItems.append(MetatileSelectionItem{true, metatileId}); } } @@ -201,43 +189,59 @@ void MetatileSelector::updateExternalSelectedMetatiles() { this->selection.dimensions = QPoint(this->externalSelectionWidth, this->externalSelectionHeight); for (int i = 0; i < this->externalSelectedMetatiles.count(); ++i) { uint16_t metatileId = this->externalSelectedMetatiles.at(i); - if (!Tileset::metatileIsValid(metatileId, this->primaryTileset, this->secondaryTileset)) + if (!this->layout->metatileIsValid(metatileId)) metatileId = 0; this->selection.metatileItems.append(MetatileSelectionItem{true, metatileId}); } emit selectedMetatilesChanged(); } -uint16_t MetatileSelector::getMetatileId(int x, int y) const { +uint16_t MetatileSelector::posToMetatileId(const QPoint &pos, bool *ok) const { + return posToMetatileId(pos.x(), pos.y(), ok); +} + +uint16_t MetatileSelector::posToMetatileId(int x, int y, bool *ok) const { + if (ok) *ok = true; int index = y * this->numMetatilesWide + x; - int numPrimary = this->numPrimaryMetatilesRounded(); - if (index < numPrimary) { - return static_cast(index); - } else { - return static_cast(Project::getNumMetatilesPrimary() + index - numPrimary); - } -} - -QPoint MetatileSelector::getMetatileIdCoords(uint16_t metatileId) { - if (!Tileset::metatileIsValid(metatileId, this->primaryTileset, this->secondaryTileset)) - { - // Invalid metatile id. - return QPoint(0, 0); + uint16_t metatileId = static_cast(index); + if (primaryTileset() && primaryTileset()->contains(metatileId)) { + return metatileId; } - int index = metatileId < Project::getNumMetatilesPrimary() - ? metatileId - : metatileId - Project::getNumMetatilesPrimary() + this->numPrimaryMetatilesRounded(); - return QPoint(index % this->numMetatilesWide, index / this->numMetatilesWide); + // There's some extra handling here because we round the tilesets to keep them on separate rows. + // This means if the maximum number of primary metatiles is not divisible by the metatile width + // then the metatiles we used to round the primary tileset would have the index of valid secondary metatiles. + // These need to be ignored, or they'll appear to be duplicates of the subseqeunt secondary metatiles. + int numPrimaryRounded = numPrimaryMetatilesRounded(); + int firstSecondaryRow = numPrimaryRounded / this->numMetatilesWide; + metatileId = static_cast(Project::getNumMetatilesPrimary() + index - numPrimaryRounded); + if (secondaryTileset() && secondaryTileset()->contains(metatileId) && y >= firstSecondaryRow) { + return metatileId; + } + + if (ok) *ok = false; + return 0; } -QPoint MetatileSelector::getMetatileIdCoordsOnWidget(uint16_t metatileId) { - QPoint pos = getMetatileIdCoords(metatileId); +QPoint MetatileSelector::metatileIdToPos(uint16_t metatileId, bool *ok) const { + if (primaryTileset() && primaryTileset()->contains(metatileId)) { + if (ok) *ok = true; + int index = metatileId; + return QPoint(index % this->numMetatilesWide, index / this->numMetatilesWide); + } + if (secondaryTileset() && secondaryTileset()->contains(metatileId)) { + if (ok) *ok = true; + int index = metatileId - Project::getNumMetatilesPrimary() + numPrimaryMetatilesRounded(); + return QPoint(index % this->numMetatilesWide, index / this->numMetatilesWide); + } + + if (ok) *ok = false; + return QPoint(0,0); +} + +QPoint MetatileSelector::getMetatileIdCoordsOnWidget(uint16_t metatileId) const { + QPoint pos = metatileIdToPos(metatileId); pos.rx() = (pos.x() * this->cellWidth) + (this->cellWidth / 2); pos.ry() = (pos.y() * this->cellHeight) + (this->cellHeight / 2); return pos; } - -void MetatileSelector::setLayout(Layout *layout) { - this->layout = layout; -} diff --git a/src/ui/movablerect.cpp b/src/ui/movablerect.cpp index 3fc73934..1e751aa9 100644 --- a/src/ui/movablerect.cpp +++ b/src/ui/movablerect.cpp @@ -5,16 +5,17 @@ #include "movablerect.h" #include "utility.h" -MovableRect::MovableRect(const QRectF &rect, const QRgb &color) +MovableRect::MovableRect(const QRectF &rect, const QSize &cellSize, const QRgb &color) : QGraphicsRectItem(rect), baseRect(rect), + cellSize(cellSize), color(color) { } /// Center rect on grid position (x, y) void MovableRect::updateLocation(int x, int y) { - setRect(this->baseRect.x() + (x * 16), - this->baseRect.y() + (y * 16), + setRect(this->baseRect.x() + (x * this->cellSize.width()), + this->baseRect.y() + (y * this->cellSize.height()), this->baseRect.width(), this->baseRect.height()); } @@ -24,9 +25,9 @@ void MovableRect::updateLocation(int x, int y) { ************************************************************************ ******************************************************************************/ -ResizableRect::ResizableRect(QObject *parent, int width, int height, QRgb color) +ResizableRect::ResizableRect(QObject *parent, const QSize &cellSize, const QSize &size, const QRgb &color) : QObject(parent), - MovableRect(QRect(0, 0, width * 16, height * 16), color) + MovableRect(QRect(0, 0, size.width(), size.height()), cellSize, color) { setAcceptHoverEvents(true); setFlags(this->flags() | QGraphicsItem::ItemIsMovable); @@ -114,8 +115,8 @@ void ResizableRect::mousePressEvent(QGraphicsSceneMouseEvent *event) { } void ResizableRect::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { - int dx = Util::roundUp(event->scenePos().x() - this->clickedPos.x(), 16); - int dy = Util::roundUp(event->scenePos().y() - this->clickedPos.y(), 16); + int dx = Util::roundUpToMultiple(event->scenePos().x() - this->clickedPos.x(), this->cellSize.width()); + int dy = Util::roundUpToMultiple(event->scenePos().y() - this->clickedPos.y(), this->cellSize.height()); QRect resizedRect = this->clickedRect; @@ -149,20 +150,20 @@ void ResizableRect::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { break; } - // Lower limits: smallest possible size is 16x16 square - if (resizedRect.width() < 16) { + // Lower limits: smallest possible size is 1 cell + if (resizedRect.width() < this->cellSize.width()) { if (dx < 0) { // right sided adjustment made - resizedRect.setWidth(16); + resizedRect.setWidth(this->cellSize.width()); } else { // left sided adjustment slightly more complicated - int dxMax = this->clickedRect.right() - this->clickedRect.left() - 16; + int dxMax = this->clickedRect.right() - this->clickedRect.left() - this->cellSize.width(); resizedRect.adjust(dxMax - dx, 0, 0, 0); } } - if (resizedRect.height() < 16) { + if (resizedRect.height() < this->cellSize.height()) { if (dy < 0) { // bottom - resizedRect.setHeight(16); + resizedRect.setHeight(this->cellSize.height()); } else { // top - int dyMax = this->clickedRect.bottom() - this->clickedRect.top() - 16; + int dyMax = this->clickedRect.bottom() - this->clickedRect.top() - this->cellSize.height(); resizedRect.adjust(0, dyMax - dy, 0, 0); } } diff --git a/src/ui/prefabcreationdialog.cpp b/src/ui/prefabcreationdialog.cpp index ef6e6a0f..76c71f39 100644 --- a/src/ui/prefabcreationdialog.cpp +++ b/src/ui/prefabcreationdialog.cpp @@ -23,12 +23,12 @@ PrefabCreationDialog::PrefabCreationDialog(QWidget *parent, MetatileSelector *me QObject::connect(this->ui->graphicsView_Prefab, &ClickableGraphicsView::clicked, [=](QMouseEvent *event){ auto pos = event->pos(); - int selectionWidth = this->selection.dimensions.x() * 16; - int selectionHeight = this->selection.dimensions.y() * 16; + int selectionWidth = this->selection.dimensions.x() * Metatile::pixelWidth(); + int selectionHeight = this->selection.dimensions.y() * Metatile::pixelHeight(); if (pos.x() < 0 || pos.x() >= selectionWidth || pos.y() < 0 || pos.y() >= selectionHeight) return; - int metatileX = pos.x() / 16; - int metatileY = pos.y() / 16; + int metatileX = pos.x() / Metatile::pixelWidth(); + int metatileY = pos.y() / Metatile::pixelHeight(); int index = metatileY * this->selection.dimensions.x() + metatileX; bool toggledState = !this->selection.metatileItems[index].enabled; this->selection.metatileItems[index].enabled = toggledState; diff --git a/src/ui/projectsettingseditor.cpp b/src/ui/projectsettingseditor.cpp index c4becaa5..dadaefd7 100644 --- a/src/ui/projectsettingseditor.cpp +++ b/src/ui/projectsettingseditor.cpp @@ -161,6 +161,7 @@ void ProjectSettingsEditor::initUi() { ui->spinBox_PlayerViewDistance_North->setMaximum(INT_MAX); ui->spinBox_PlayerViewDistance_East->setMaximum(INT_MAX); ui->spinBox_PlayerViewDistance_South->setMaximum(INT_MAX); + ui->spinBox_MetatileSelectorWidth->setMaximum(maxMetatileId + 1); // The values for some of the settings we provide in this window can be determined using constants in the user's projects. // If the user has these constants we disable these settings in the UI -- they can modify them using their constants. @@ -232,7 +233,7 @@ QList ProjectSettingsEditor::getBorderMetatileIds(bool customSize) { // Custom border size, read metatiles from line edit for (auto s : ui->lineEdit_BorderMetatiles->text().split(",")) { uint16_t metatileId = s.toUInt(nullptr, 0); - metatileIds.append(qMin(metatileId, static_cast(Project::getNumMetatilesTotal() - 1))); + metatileIds.append(qMin(metatileId, Block::getMaxMetatileId())); } } else { // Default border size, read metatiles from spin boxes @@ -475,7 +476,7 @@ void ProjectSettingsEditor::refresh() { ui->checkBox_PreserveMatchingOnlyData->setChecked(projectConfig.preserveMatchingOnlyData); // Radio buttons - if (projectConfig.setTransparentPixelsBlack) + if (projectConfig.transparencyColor == QColor(Qt::black)) ui->radioButton_RenderBlack->setChecked(true); else ui->radioButton_RenderFirstPalColor->setChecked(true); @@ -503,6 +504,7 @@ void ProjectSettingsEditor::refresh() { ui->spinBox_PlayerViewDistance_North->setValue(projectConfig.playerViewDistance.top()); ui->spinBox_PlayerViewDistance_East->setValue(projectConfig.playerViewDistance.right()); ui->spinBox_PlayerViewDistance_South->setValue(projectConfig.playerViewDistance.bottom()); + ui->spinBox_MetatileSelectorWidth->setValue(projectConfig.metatileSelectorWidth); // Set (and sync) border metatile IDs this->setBorderMetatileIds(false, projectConfig.newMapBorderMetatileIds); @@ -574,7 +576,7 @@ void ProjectSettingsEditor::save() { projectConfig.tilesetsHaveCallback = ui->checkBox_OutputCallback->isChecked(); projectConfig.tilesetsHaveIsCompressed = ui->checkBox_OutputIsCompressed->isChecked(); porymapConfig.warpBehaviorWarningDisabled = ui->checkBox_DisableWarning->isChecked(); - projectConfig.setTransparentPixelsBlack = ui->radioButton_RenderBlack->isChecked(); + projectConfig.transparencyColor = ui->radioButton_RenderBlack->isChecked() ? QColor(Qt::black) : QColor(); projectConfig.preserveMatchingOnlyData = ui->checkBox_PreserveMatchingOnlyData->isChecked(); // Save spin box settings @@ -598,6 +600,7 @@ void ProjectSettingsEditor::save() { ui->spinBox_PlayerViewDistance_North->value(), ui->spinBox_PlayerViewDistance_East->value(), ui->spinBox_PlayerViewDistance_South->value()); + projectConfig.metatileSelectorWidth = ui->spinBox_MetatileSelectorWidth->value(); // Save line edit settings projectConfig.prefabFilepath = ui->lineEdit_PrefabsPath->text(); diff --git a/src/ui/resizelayoutpopup.cpp b/src/ui/resizelayoutpopup.cpp index 7f11836f..679e9ee2 100644 --- a/src/ui/resizelayoutpopup.cpp +++ b/src/ui/resizelayoutpopup.cpp @@ -7,42 +7,9 @@ #include "ui_resizelayoutpopup.h" -CheckeredBgScene::CheckeredBgScene(QObject *parent) : QGraphicsScene(parent) { } - -void CheckeredBgScene::drawBackground(QPainter *painter, const QRectF &rect) { - QRect r = rect.toRect(); - int xMin = r.left() - r.left() % this->gridSize - this->gridSize; - int yMin = r.top() - r.top() % this->gridSize - this->gridSize; - int xMax = r.right() - r.right() % this->gridSize + this->gridSize; - int yMax = r.bottom() - r.bottom() % this->gridSize + this->gridSize; - - // draw grid 16x16 from top to bottom of scene - QColor paintColor(0x00ff00); - for (int x = xMin, xTile = 0; x <= xMax; x += this->gridSize, xTile++) { - for (int y = yMin, yTile = 0; y <= yMax; y += this->gridSize, yTile++) { - if (!((xTile ^ yTile) & 1)) { // tile numbers have same parity (evenness) - if (this->validRect.contains(x, y)) - paintColor = QColor(132, 217, 165); // green light color - else - paintColor = 0xbcbcbc; // normal light color - } - else { - if (this->validRect.contains(x, y)) - paintColor = QColor(76, 178, 121); // green dark color - else - paintColor = 0x969696; // normal dark color - } - painter->fillRect(QRect(x, y, this->gridSize, this->gridSize), paintColor); - } - } -} - -/****************************************************************************** - ************************************************************************ - ******************************************************************************/ - -BoundedPixmapItem::BoundedPixmapItem(const QPixmap &pixmap, QGraphicsItem *parent) : QGraphicsPixmapItem(pixmap, parent) { +BoundedPixmapItem::BoundedPixmapItem(const QPixmap &pixmap, const QSize &cellSize, QGraphicsItem *parent) : QGraphicsPixmapItem(pixmap, parent) { setFlags(this->flags() | QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemSendsGeometryChanges | QGraphicsItem::ItemIsSelectable); + this->cellSize = cellSize; } void BoundedPixmapItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { @@ -61,7 +28,8 @@ void BoundedPixmapItem::paint(QPainter *painter, const QStyleOptionGraphicsItem QVariant BoundedPixmapItem::itemChange(GraphicsItemChange change, const QVariant &value) { if (change == ItemPositionChange && scene()) { QPointF newPos = value.toPointF(); - return QPointF(Util::roundUp(newPos.x(), 16), Util::roundUp(newPos.y(), 16)); + return QPointF(Util::roundUpToMultiple(newPos.x(), this->cellSize.width()), + Util::roundUpToMultiple(newPos.y(), this->cellSize.height())); } else return QGraphicsItem::itemChange(change, value); @@ -83,7 +51,7 @@ ResizeLayoutPopup::ResizeLayoutPopup(QWidget *parent, Layout *layout, Project *p this->setWindowFlags(this->windowFlags() | Qt::FramelessWindowHint); this->setWindowModality(Qt::ApplicationModal); - this->scene = new CheckeredBgScene(this); + this->scene = new CheckeredBgScene(Metatile::pixelSize(), this); this->ui->graphicsView->setScene(this->scene); this->ui->graphicsView->setRenderHints(QPainter::Antialiasing); this->ui->graphicsView->setViewportUpdateMode(QGraphicsView::FullViewportUpdate); @@ -124,11 +92,13 @@ void ResizeLayoutPopup::setupLayoutView() { this->ui->spinBox_borderHeight->setValue(this->layout->getBorderHeight()); // Layout stuff - this->layoutPixmap = new BoundedPixmapItem(this->layout->pixmap); + this->layoutPixmap = new BoundedPixmapItem(this->layout->pixmap, Metatile::pixelSize()); this->scene->addItem(layoutPixmap); int maxWidth = this->project->getMaxMapWidth(); int maxHeight = this->project->getMaxMapHeight(); - QGraphicsRectItem *cover = new QGraphicsRectItem(-maxWidth * 8, -maxHeight * 8, maxWidth * 16, maxHeight * 16); + int maxPixelWidth = maxWidth * Metatile::pixelWidth() * 2; // *2 to allow reaching max dimension by expanding from 0,0 in either direction + int maxPixelHeight = maxHeight * Metatile::pixelHeight() * 2; + QGraphicsRectItem *cover = new QGraphicsRectItem(-(maxPixelWidth / 2), -(maxPixelHeight / 2), maxPixelWidth, maxPixelHeight); this->scene->addItem(cover); this->ui->spinBox_width->setMinimum(1); @@ -136,12 +106,14 @@ void ResizeLayoutPopup::setupLayoutView() { this->ui->spinBox_height->setMinimum(1); this->ui->spinBox_height->setMaximum(maxHeight); - this->outline = new ResizableRect(this, this->layout->getWidth(), this->layout->getHeight(), qRgb(255, 0, 255)); + this->outline = new ResizableRect(this, Metatile::pixelSize(), this->layout->pixelSize(), qRgb(255, 0, 255)); this->outline->setZValue(Editor::ZValue::ResizeLayoutPopup); // Ensure on top of view this->outline->setLimit(cover->rect().toAlignedRect()); connect(outline, &ResizableRect::rectUpdated, [=](QRect rect){ // Note: this extra limit check needs access to the project values, so it is done here and not ResizableRect::mouseMoveEvent - int size = this->project->getMapDataSize(rect.width() / 16, rect.height() / 16); + int metatilesWide = rect.width() / Metatile::pixelWidth(); + int metatilesTall = rect.height() / Metatile::pixelHeight(); + int size = this->project->getMapDataSize(metatilesWide, metatilesTall); int maxSize = this->project->getMaxMapDataSize(); if (size > maxSize) { QSize addition = this->project->getMapSizeAddition(); @@ -151,8 +123,8 @@ void ResizeLayoutPopup::setupLayoutView() { .arg(addition.width()) .arg(addition.height()) .arg(maxSize) - .arg(rect.width() / 16) - .arg(rect.height() / 16) + .arg(metatilesWide) + .arg(metatilesTall) .arg(size), this); // adjust rect to last accepted size @@ -160,8 +132,11 @@ void ResizeLayoutPopup::setupLayoutView() { } this->scene->setValidRect(rect); this->outline->setRect(rect); - this->ui->spinBox_width->setValue(rect.width() / 16); - this->ui->spinBox_height->setValue(rect.height() / 16); + + const QSignalBlocker b_Width(this->ui->spinBox_width); + const QSignalBlocker b_Height(this->ui->spinBox_height); + this->ui->spinBox_width->setValue(metatilesWide); + this->ui->spinBox_height->setValue(metatilesTall); }); scene->addItem(outline); @@ -171,7 +146,7 @@ void ResizeLayoutPopup::setupLayoutView() { this->scale = 1.0; QRectF rect = this->outline->rect(); - const int marginSize = 10 * 16; // Leave a margin of 10 metatiles around the map + const int marginSize = 10 * Metatile::pixelWidth(); // Leave a margin of 10 metatiles around the map rect += QMargins(marginSize, marginSize, marginSize, marginSize); this->ui->graphicsView->fitInView(rect, Qt::KeepAspectRatio); } @@ -179,25 +154,23 @@ void ResizeLayoutPopup::setupLayoutView() { void ResizeLayoutPopup::on_spinBox_width_valueChanged(int value) { if (!this->outline) return; QRectF rect = this->outline->rect(); - this->outline->updatePosFromRect(QRect(rect.x(), rect.y(), value * 16, rect.height())); + this->outline->updatePosFromRect(QRect(rect.x(), rect.y(), value * Metatile::pixelWidth(), rect.height())); } void ResizeLayoutPopup::on_spinBox_height_valueChanged(int value) { if (!this->outline) return; QRectF rect = this->outline->rect(); - this->outline->updatePosFromRect(QRect(rect.x(), rect.y(), rect.width(), value * 16)); + this->outline->updatePosFromRect(QRect(rect.x(), rect.y(), rect.width(), value * Metatile::pixelHeight())); } /// Result is the number of metatiles to add (or subtract) to each side of the map after dimension changes QMargins ResizeLayoutPopup::getResult() { QMargins result = QMargins(); - - result.setLeft(this->layoutPixmap->x() - this->outline->rect().left()); - result.setTop(this->layoutPixmap->y() - this->outline->rect().top()); - result.setRight(this->outline->rect().right() - (this->layoutPixmap->x() + this->layoutPixmap->pixmap().width())); - result.setBottom(this->outline->rect().bottom() - (this->layoutPixmap->y() + this->layoutPixmap->pixmap().height())); - - return result / 16; + result.setLeft((this->layoutPixmap->x() - this->outline->rect().left()) / Metatile::pixelWidth()); + result.setTop((this->layoutPixmap->y() - this->outline->rect().top()) / Metatile::pixelHeight()); + result.setRight((this->outline->rect().right() - (this->layoutPixmap->x() + this->layoutPixmap->pixmap().width())) / Metatile::pixelWidth()); + result.setBottom((this->outline->rect().bottom() - (this->layoutPixmap->y() + this->layoutPixmap->pixmap().height())) / Metatile::pixelHeight()); + return result; } QSize ResizeLayoutPopup::getBorderResult() { diff --git a/src/ui/selectablepixmapitem.cpp b/src/ui/selectablepixmapitem.cpp index e15824ad..bd001e59 100644 --- a/src/ui/selectablepixmapitem.cpp +++ b/src/ui/selectablepixmapitem.cpp @@ -1,7 +1,7 @@ #include "selectablepixmapitem.h" #include -QPoint SelectablePixmapItem::getSelectionDimensions() +QPoint SelectablePixmapItem::getSelectionDimensions() const { return QPoint(abs(this->selectionOffsetX) + 1, abs(this->selectionOffsetY) + 1); } @@ -19,8 +19,8 @@ void SelectablePixmapItem::select(int x, int y, int width, int height) { this->selectionInitialX = x; this->selectionInitialY = y; - this->selectionOffsetX = qMax(0, qMin(width, this->maxSelectionWidth)); - this->selectionOffsetY = qMax(0, qMin(height, this->maxSelectionHeight)); + this->selectionOffsetX = qBound(0, width, this->maxSelectionWidth); + this->selectionOffsetY = qBound(0, height, this->maxSelectionHeight); this->draw(); emit this->selectionChanged(x, y, width, height); } diff --git a/src/ui/tileseteditor.cpp b/src/ui/tileseteditor.cpp index 6fe96f0f..4df24065 100644 --- a/src/ui/tileseteditor.cpp +++ b/src/ui/tileseteditor.cpp @@ -24,8 +24,10 @@ TilesetEditor::TilesetEditor(Project *project, Layout *layout, QWidget *parent) hasUnsavedChanges(false) { setAttribute(Qt::WA_DeleteOnClose); - setTilesets(this->layout->tileset_primary_label, this->layout->tileset_secondary_label); ui->setupUi(this); + + setTilesets(this->layout->tileset_primary_label, this->layout->tileset_secondary_label); + connect(ui->checkBox_xFlip, &QCheckBox::toggled, this, &TilesetEditor::setXFlip); connect(ui->checkBox_yFlip, &QCheckBox::toggled, this, &TilesetEditor::setYFlip); @@ -35,6 +37,20 @@ TilesetEditor::TilesetEditor(Project *project, Layout *layout, QWidget *parent) connect(ui->actionSave_Tileset, &QAction::triggered, this, &TilesetEditor::save); + connect(ui->actionImport_Primary_Tiles_Image, &QAction::triggered, [this] { importTilesetTiles(this->primaryTileset); }); + connect(ui->actionImport_Secondary_Tiles_Image, &QAction::triggered, [this] { importTilesetTiles(this->secondaryTileset); }); + + connect(ui->actionImport_Primary_AdvanceMap_Metatiles, &QAction::triggered, [this] { importAdvanceMapMetatiles(this->primaryTileset); }); + connect(ui->actionImport_Secondary_AdvanceMap_Metatiles, &QAction::triggered, [this] { importAdvanceMapMetatiles(this->secondaryTileset); }); + + connect(ui->actionExport_Primary_Tiles_Image, &QAction::triggered, [this] { exportTilesImage(this->primaryTileset); }); + connect(ui->actionExport_Secondary_Tiles_Image, &QAction::triggered, [this] { exportTilesImage(this->secondaryTileset); }); + + connect(ui->actionExport_Primary_Porytiles_Layer_Images, &QAction::triggered, [this] { exportPorytilesLayerImages(this->primaryTileset); }); + connect(ui->actionExport_Secondary_Porytiles_Layer_Images, &QAction::triggered, [this] { exportPorytilesLayerImages(this->secondaryTileset); }); + + connect(ui->actionExport_Metatiles_Image, &QAction::triggered, [this] { exportMetatilesImage(); }); + ui->actionShow_Tileset_Divider->setChecked(porymapConfig.showTilesetEditorDivider); ui->actionShow_Raw_Metatile_Attributes->setChecked(porymapConfig.showTilesetEditorRawAttributes); @@ -74,6 +90,7 @@ TilesetEditor::~TilesetEditor() delete selectedTileScene; delete metatileLayersScene; delete copiedMetatile; + delete metatileImageExportSettings; this->metatileHistory.clear(); } @@ -198,7 +215,7 @@ void TilesetEditor::setRawAttributesVisible(bool visible) { void TilesetEditor::initMetatileSelector() { - this->metatileSelector = new TilesetEditorMetatileSelector(this->primaryTileset, this->secondaryTileset, this->layout); + this->metatileSelector = new TilesetEditorMetatileSelector(projectConfig.metatileSelectorWidth, this->primaryTileset, this->secondaryTileset, this->layout); connect(this->metatileSelector, &TilesetEditorMetatileSelector::hoveredMetatileChanged, this, &TilesetEditor::onHoveredMetatileChanged); connect(this->metatileSelector, &TilesetEditorMetatileSelector::hoveredMetatileCleared, @@ -227,8 +244,9 @@ void TilesetEditor::initMetatileLayersItem() { this, &TilesetEditor::onMetatileLayerTileChanged); connect(this->metatileLayersItem, &MetatileLayersItem::selectedTilesChanged, this, &TilesetEditor::onMetatileLayerSelectionChanged); - connect(this->metatileLayersItem, &MetatileLayersItem::hoveredTileChanged, - this, &TilesetEditor::onHoveredTileChanged); + connect(this->metatileLayersItem, &MetatileLayersItem::hoveredTileChanged, [this](const Tile &tile) { + onHoveredTileChanged(tile); + }); connect(this->metatileLayersItem, &MetatileLayersItem::hoveredTileCleared, this, &TilesetEditor::onHoveredTileCleared); @@ -244,8 +262,9 @@ void TilesetEditor::initMetatileLayersItem() { void TilesetEditor::initTileSelector() { this->tileSelector = new TilesetEditorTileSelector(this->primaryTileset, this->secondaryTileset, projectConfig.getNumLayersInMetatile()); - connect(this->tileSelector, &TilesetEditorTileSelector::hoveredTileChanged, - this, &TilesetEditor::onHoveredTileChanged); + connect(this->tileSelector, &TilesetEditorTileSelector::hoveredTileChanged, [this](uint16_t tileId) { + onHoveredTileChanged(tileId); + }); connect(this->tileSelector, &TilesetEditorTileSelector::hoveredTileCleared, this, &TilesetEditor::onHoveredTileCleared); connect(this->tileSelector, &TilesetEditorTileSelector::selectedTilesChanged, @@ -374,19 +393,21 @@ void TilesetEditor::drawSelectedTiles() { return; } + const int imgTileWidth = 16; + const int imgTileHeight = 16; this->selectedTileScene->clear(); QList tiles = this->tileSelector->getSelectedTiles(); QPoint dimensions = this->tileSelector->getSelectionDimensions(); - QImage selectionImage(16 * dimensions.x(), 16 * dimensions.y(), QImage::Format_RGBA8888); + QImage selectionImage(imgTileWidth * dimensions.x(), imgTileHeight * dimensions.y(), QImage::Format_RGBA8888); QPainter painter(&selectionImage); int tileIndex = 0; for (int j = 0; j < dimensions.y(); j++) { for (int i = 0; i < dimensions.x(); i++) { auto tile = tiles.at(tileIndex); - QImage tileImage = getPalettedTileImage(tile.tileId, this->primaryTileset, this->secondaryTileset, tile.palette, true).scaled(16, 16); + QImage tileImage = getPalettedTileImage(tile.tileId, this->primaryTileset, this->secondaryTileset, tile.palette, true).scaled(imgTileWidth, imgTileHeight); tile.flip(&tileImage); tileIndex++; - painter.drawImage(i * 16, j * 16, tileImage); + painter.drawImage(i * imgTileWidth, j * imgTileHeight, tileImage); } } @@ -437,8 +458,17 @@ void TilesetEditor::queueMetatileReload(uint16_t metatileId) { this->metatileReloadQueue.insert(metatileId); } -void TilesetEditor::onHoveredTileChanged(uint16_t tile) { - this->ui->statusbar->showMessage(QString("Tile: %1").arg(Util::toHexString(tile, 3))); +void TilesetEditor::onHoveredTileChanged(const Tile &tile) { + this->ui->statusbar->showMessage(QString("Tile: %1, Palette: %2%3%4") + .arg(Util::toHexString(tile.tileId, 3)) + .arg(QString::number(tile.palette)) + .arg(tile.xflip ? ", X-flipped" : "") + .arg(tile.yflip ? ", Y-flipped" : "") + ); +} + +void TilesetEditor::onHoveredTileChanged(uint16_t tileId) { + this->ui->statusbar->showMessage(QString("Tile: %1").arg(Util::toHexString(tileId, 3))); } void TilesetEditor::onHoveredTileCleared() { @@ -694,17 +724,8 @@ bool TilesetEditor::save() { return success; } -void TilesetEditor::on_actionImport_Primary_Tiles_triggered() -{ - this->importTilesetTiles(this->primaryTileset, true); -} - -void TilesetEditor::on_actionImport_Secondary_Tiles_triggered() -{ - this->importTilesetTiles(this->secondaryTileset, false); -} - -void TilesetEditor::importTilesetTiles(Tileset *tileset, bool primary) { +void TilesetEditor::importTilesetTiles(Tileset *tileset) { + bool primary = !tileset->is_secondary; QString descriptor = primary ? "primary" : "secondary"; QString descriptorCaps = primary ? "Primary" : "Secondary"; @@ -725,18 +746,20 @@ void TilesetEditor::importTilesetTiles(Tileset *tileset, bool primary) { } else { logError(QString("Failed to open image file: '%1'").arg(filepath)); } - if (image.width() == 0 || image.height() == 0 || image.width() % 8 != 0 || image.height() % 8 != 0) { + if (image.width() == 0 || image.height() == 0 || image.width() % Tile::pixelWidth() != 0 || image.height() % Tile::pixelHeight() != 0) { ErrorMessage::show(QStringLiteral("Failed to import tiles."), - QString("The image dimensions (%1 x %2) are invalid. Width and height must be multiples of 8 pixels.") + QString("The image dimensions (%1x%2) are invalid. The dimensions must be a multiple of %3x%4 pixels.") .arg(image.width()) - .arg(image.height()), + .arg(image.height()) + .arg(Tile::pixelWidth()) + .arg(Tile::pixelHeight()), this); return; } // Validate total number of tiles in image. - int numTilesWide = image.width() / 8; - int numTilesHigh = image.height() / 8; + int numTilesWide = image.width() / Tile::pixelWidth(); + int numTilesHigh = image.height() / Tile::pixelHeight(); int totalTiles = numTilesHigh * numTilesWide; int maxAllowedTiles = primary ? Project::getNumTilesPrimary() : Project::getNumTilesTotal() - Project::getNumTilesPrimary(); if (totalTiles > maxAllowedTiles) { @@ -774,7 +797,10 @@ void TilesetEditor::importTilesetTiles(Tileset *tileset, bool primary) { image = image.convertToFormat(QImage::Format::Format_Indexed8, colorTable); } - tileset->loadTilesImage(&image); + if (!tileset->loadTilesImage(&image)) { + RecentErrorMessage::show(QStringLiteral("Failed to import tiles."), this); + return; + } this->refresh(); this->hasUnsavedChanges = true; } @@ -822,7 +848,7 @@ void TilesetEditor::on_actionChange_Metatiles_Count_triggered() primarySpinBox->setMinimum(1); secondarySpinBox->setMinimum(1); primarySpinBox->setMaximum(Project::getNumMetatilesPrimary()); - secondarySpinBox->setMaximum(Project::getNumMetatilesTotal() - Project::getNumMetatilesPrimary()); + secondarySpinBox->setMaximum(Project::getNumMetatilesSecondary()); primarySpinBox->setValue(this->primaryTileset->numMetatiles()); secondarySpinBox->setValue(this->secondaryTileset->numMetatiles()); form.addRow(new QLabel("Primary Tileset"), primarySpinBox); @@ -968,62 +994,70 @@ void TilesetEditor::pasteMetatile(const Metatile * toPaste, QString newLabel) this->commitMetatileAndLabelChange(prevMetatile, prevLabel); } -void TilesetEditor::on_actionExport_Primary_Tiles_Image_triggered() -{ - QString defaultName = QString("%1_Tiles_Pal%2").arg(this->primaryTileset->name).arg(this->paletteId); - QString defaultFilepath = QString("%1/%2.png").arg(FileDialog::getDirectory()).arg(defaultName); - QString filepath = FileDialog::getSaveFileName(this, "Export Primary Tiles Image", defaultFilepath, "Image Files (*.png)"); +void TilesetEditor::exportTilesImage(Tileset *tileset) { + bool primary = !tileset->is_secondary; + QString defaultFilepath = QString("%1/%2_Tiles_Pal%3.png").arg(FileDialog::getDirectory()).arg(tileset->name).arg(this->paletteId); + QString filepath = FileDialog::getSaveFileName(this, QString("Export %1 Tiles Image").arg(primary ? "Primary" : "Secondary"), defaultFilepath, "Image Files (*.png)"); if (!filepath.isEmpty()) { - QImage image = this->tileSelector->buildPrimaryTilesIndexedImage(); + QImage image = primary ? this->tileSelector->buildPrimaryTilesIndexedImage() : this->tileSelector->buildSecondaryTilesIndexedImage(); exportIndexed4BPPPng(image, filepath); } } -void TilesetEditor::on_actionExport_Secondary_Tiles_Image_triggered() -{ - QString defaultName = QString("%1_Tiles_Pal%2").arg(this->secondaryTileset->name).arg(this->paletteId); - QString defaultFilepath = QString("%1/%2.png").arg(FileDialog::getDirectory()).arg(defaultName); - QString filepath = FileDialog::getSaveFileName(this, "Export Secondary Tiles Image", defaultFilepath, "Image Files (*.png)"); - if (!filepath.isEmpty()) { - QImage image = this->tileSelector->buildSecondaryTilesIndexedImage(); - exportIndexed4BPPPng(image, filepath); +// There are many more options for exporting metatile images than tile images, so we open a separate dialog to ask the user for settings. +void TilesetEditor::exportMetatilesImage() { + if (!this->metatileImageExportSettings) { + this->metatileImageExportSettings = new MetatileImageExporter::Settings; + } + auto dialog = new MetatileImageExporter(this, this->primaryTileset, this->secondaryTileset, this->metatileImageExportSettings); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->open(); +} + +void TilesetEditor::exportPorytilesLayerImages(Tileset *tileset) { + QString dir = FileDialog::getExistingDirectory(this, QStringLiteral("Choose Folder to Export Images")); + if (dir.isEmpty()) { + return; + } + + MetatileImageExporter layerExporter(this, this->primaryTileset, this->secondaryTileset); + MetatileImageExporter::Settings settings = {}; + settings.usePrimaryTileset = !tileset->is_secondary; + settings.useSecondaryTileset = tileset->is_secondary; + + QMap images; + QStringList pathCollisions; + for (int i = 0; i < 3; i++) { + settings.layerOrder.clear(); + settings.layerOrder[i] = true; + layerExporter.applySettings(settings); + + QString filename = layerExporter.getDefaultFileName(); + QString path = QString("%1/%2").arg(dir).arg(filename); + if (QFileInfo::exists(path)) { + pathCollisions.append(filename); + } + images[path] = layerExporter.getImage(); + } + + if (!pathCollisions.isEmpty()) { + QString message = QString("The following files will be overwritten, are you sure you want to export?\n\n%1").arg(pathCollisions.join("\n")); + auto reply = QuestionMessage::show(message, this); + if (reply != QMessageBox::Yes) { + return; + } + } + + for (auto it = images.constBegin(); it != images.constEnd(); it++) { + QString path = it.key(); + if (!it.value().save(path)) { + logError(QString("Failed to save Porytiles layer image '%1'.").arg(path)); + } } } -void TilesetEditor::on_actionExport_Primary_Metatiles_Image_triggered() -{ - QString defaultName = QString("%1_Metatiles").arg(this->primaryTileset->name); - QString defaultFilepath = QString("%1/%2.png").arg(FileDialog::getDirectory()).arg(defaultName); - QString filepath = FileDialog::getSaveFileName(this, "Export Primary Metatiles Image", defaultFilepath, "Image Files (*.png)"); - if (!filepath.isEmpty()) { - QImage image = this->metatileSelector->buildPrimaryMetatilesImage(); - image.save(filepath, "PNG"); - } -} - -void TilesetEditor::on_actionExport_Secondary_Metatiles_Image_triggered() -{ - QString defaultName = QString("%1_Metatiles").arg(this->secondaryTileset->name); - QString defaultFilepath = QString("%1/%2.png").arg(FileDialog::getDirectory()).arg(defaultName); - QString filepath = FileDialog::getSaveFileName(this, "Export Secondary Metatiles Image", defaultFilepath, "Image Files (*.png)"); - if (!filepath.isEmpty()) { - QImage image = this->metatileSelector->buildSecondaryMetatilesImage(); - image.save(filepath, "PNG"); - } -} - -void TilesetEditor::on_actionImport_Primary_Metatiles_triggered() -{ - this->importTilesetMetatiles(this->primaryTileset, true); -} - -void TilesetEditor::on_actionImport_Secondary_Metatiles_triggered() -{ - this->importTilesetMetatiles(this->secondaryTileset, false); -} - -void TilesetEditor::importTilesetMetatiles(Tileset *tileset, bool primary) -{ +void TilesetEditor::importAdvanceMapMetatiles(Tileset *tileset) { + bool primary = !tileset->is_secondary; QString descriptorCaps = primary ? "Primary" : "Secondary"; QString filepath = FileDialog::getOpenFileName(this, QString("Import %1 Tileset Metatiles from Advance Map 1.92").arg(descriptorCaps), "", "Advance Map 1.92 Metatile Files (*.bvd)"); @@ -1041,7 +1075,7 @@ void TilesetEditor::importTilesetMetatiles(Tileset *tileset, bool primary) // TODO: This is crude because it makes a history entry for every newly-imported metatile. // Revisit this when tiles and num metatiles are added to tileset editory history. - int metatileIdBase = primary ? 0 : Project::getNumMetatilesPrimary(); + uint16_t metatileIdBase = tileset->firstMetatileId(); for (int i = 0; i < metatiles.length(); i++) { if (i >= tileset->numMetatiles()) { break; diff --git a/src/ui/tileseteditormetatileselector.cpp b/src/ui/tileseteditormetatileselector.cpp index 5026a49b..bba51e77 100644 --- a/src/ui/tileseteditormetatileselector.cpp +++ b/src/ui/tileseteditormetatileselector.cpp @@ -3,17 +3,20 @@ #include "project.h" #include -TilesetEditorMetatileSelector::TilesetEditorMetatileSelector(Tileset *primaryTileset, Tileset *secondaryTileset, Layout *layout) - : SelectablePixmapItem(16, 16, 1, 1) { +// TODO: This class has a decent bit of overlap with the MetatileSelector class. +// They should be refactored to inherit from a single parent class. + +TilesetEditorMetatileSelector::TilesetEditorMetatileSelector(int numMetatilesWide, Tileset *primaryTileset, Tileset *secondaryTileset, Layout *layout) + : SelectablePixmapItem(32, 32, 1, 1), + numMetatilesWide(qMax(numMetatilesWide, 1)) { this->primaryTileset = primaryTileset; this->secondaryTileset = secondaryTileset; - this->numMetatilesWide = 8; this->layout = layout; setAcceptHoverEvents(true); this->usedMetatiles.resize(Project::getNumMetatilesTotal()); } -int TilesetEditorMetatileSelector::numRows(int numMetatiles) { +int TilesetEditorMetatileSelector::numRows(int numMetatiles) const { int numMetatilesHigh = numMetatiles / this->numMetatilesWide; if (numMetatiles % this->numMetatilesWide != 0) { // Round up height for incomplete last row @@ -22,67 +25,29 @@ int TilesetEditorMetatileSelector::numRows(int numMetatiles) { return numMetatilesHigh; } -int TilesetEditorMetatileSelector::numRows() { +int TilesetEditorMetatileSelector::numRows() const { return this->numRows(this->numPrimaryMetatilesRounded() + this->secondaryTileset->numMetatiles()); } int TilesetEditorMetatileSelector::numPrimaryMetatilesRounded() const { - // We round up the number of primary metatiles to keep the tilesets on separate rows. - return ceil((double)this->primaryTileset->numMetatiles() / this->numMetatilesWide) * this->numMetatilesWide; -} - -QImage TilesetEditorMetatileSelector::buildAllMetatilesImage() { - return this->buildImage(0, this->numPrimaryMetatilesRounded() + this->secondaryTileset->numMetatiles()); -} - -QImage TilesetEditorMetatileSelector::buildPrimaryMetatilesImage() { - return this->buildImage(0, this->primaryTileset->numMetatiles()); -} - -QImage TilesetEditorMetatileSelector::buildSecondaryMetatilesImage() { - return this->buildImage(Project::getNumMetatilesPrimary(), this->secondaryTileset->numMetatiles()); -} - -QImage TilesetEditorMetatileSelector::buildImage(int metatileIdStart, int numMetatiles) { - int numMetatilesHigh = this->numRows(numMetatiles); - int numPrimary = this->numPrimaryMetatilesRounded(); - int maxPrimary = Project::getNumMetatilesPrimary(); - bool includesPrimary = metatileIdStart < maxPrimary; - - QImage image(this->numMetatilesWide * this->cellWidth, numMetatilesHigh * this->cellHeight, QImage::Format_RGBA8888); - image.fill(Qt::magenta); - QPainter painter(&image); - for (int i = 0; i < numMetatiles; i++) { - int metatileId = i + metatileIdStart; - if (includesPrimary && metatileId >= numPrimary) - metatileId += maxPrimary - numPrimary; // Skip over unused region of primary tileset - QImage metatile_image = getMetatileImage( - metatileId, - this->primaryTileset, - this->secondaryTileset, - this->layout->metatileLayerOrder, - this->layout->metatileLayerOpacity, - true) - .scaled(this->cellWidth, this->cellHeight); - int map_y = i / this->numMetatilesWide; - int map_x = i % this->numMetatilesWide; - QPoint metatile_origin = QPoint(map_x * this->cellWidth, map_y * this->cellHeight); - painter.drawImage(metatile_origin, metatile_image); - } - painter.end(); - return image; + if (!this->primaryTileset) + return 0; + return Util::roundUpToMultiple(this->primaryTileset->numMetatiles(), this->numMetatilesWide); } void TilesetEditorMetatileSelector::drawMetatile(uint16_t metatileId) { - QPoint pos = getMetatileIdCoords(metatileId); + bool ok; + QPoint pos = metatileIdToPos(metatileId, &ok); + if (!ok) + return; QPainter painter(&this->baseImage); QImage metatile_image = getMetatileImage( metatileId, this->primaryTileset, this->secondaryTileset, - this->layout->metatileLayerOrder, - this->layout->metatileLayerOpacity, + this->layout->metatileLayerOrder(), + this->layout->metatileLayerOpacity(), true) .scaled(this->cellWidth, this->cellHeight); painter.drawImage(QPoint(pos.x() * this->cellWidth, pos.y() * this->cellHeight), metatile_image); @@ -97,7 +62,13 @@ void TilesetEditorMetatileSelector::drawSelectedMetatile() { } void TilesetEditorMetatileSelector::updateBasePixmap() { - this->baseImage = buildAllMetatilesImage(); + this->baseImage = getMetatileSheetImage(this->primaryTileset, + this->secondaryTileset, + this->numMetatilesWide, + this->layout->metatileLayerOrder(), + this->layout->metatileLayerOpacity(), + QSize(this->cellWidth, this->cellHeight), + true); this->basePixmap = QPixmap::fromImage(this->baseImage); } @@ -114,9 +85,11 @@ void TilesetEditorMetatileSelector::draw() { } bool TilesetEditorMetatileSelector::select(uint16_t metatileId) { - if (!Tileset::metatileIsValid(metatileId, this->primaryTileset, this->secondaryTileset)) return false; - QPoint coords = this->getMetatileIdCoords(metatileId); - SelectablePixmapItem::select(coords.x(), coords.y(), 0, 0); + bool ok; + QPoint pos = metatileIdToPos(metatileId, &ok); + if (!ok) + return false; + SelectablePixmapItem::select(pos); this->selectedMetatileId = metatileId; emit selectedMetatileChanged(metatileId); return true; @@ -131,32 +104,19 @@ void TilesetEditorMetatileSelector::setTilesets(Tileset *primaryTileset, Tileset } void TilesetEditorMetatileSelector::updateSelectedMetatile() { - QPoint origin = this->getSelectionStart(); - uint16_t metatileId = this->getMetatileId(origin.x(), origin.y()); - if (Tileset::metatileIsValid(metatileId, this->primaryTileset, this->secondaryTileset)) - this->selectedMetatileId = metatileId; - else - this->selectedMetatileId = Project::getNumMetatilesPrimary() + this->secondaryTileset->numMetatiles() - 1; + bool ok; + uint16_t metatileId = posToMetatileId(getSelectionStart(), &ok); + if (!ok) + return; + + this->selectedMetatileId = metatileId; emit selectedMetatileChanged(this->selectedMetatileId); } -uint16_t TilesetEditorMetatileSelector::getSelectedMetatileId() { - return this->selectedMetatileId; -} - -uint16_t TilesetEditorMetatileSelector::getMetatileId(int x, int y) { - int index = y * this->numMetatilesWide + x; - int numPrimary = numPrimaryMetatilesRounded(); - if (index < numPrimary) { - return static_cast(index); - } else { - return static_cast(Project::getNumMetatilesPrimary() + index - numPrimary); - } -} - bool TilesetEditorMetatileSelector::shouldAcceptEvent(QGraphicsSceneMouseEvent *event) { - QPoint pos = this->getCellPos(event->pos()); - return Tileset::metatileIsValid(getMetatileId(pos.x(), pos.y()), this->primaryTileset, this->secondaryTileset); + bool ok; + posToMetatileId(getCellPos(event->pos()), &ok); + return ok; } void TilesetEditorMetatileSelector::mousePressEvent(QGraphicsSceneMouseEvent *event) { @@ -179,29 +139,64 @@ void TilesetEditorMetatileSelector::mouseReleaseEvent(QGraphicsSceneMouseEvent * } void TilesetEditorMetatileSelector::hoverMoveEvent(QGraphicsSceneHoverEvent *event) { - QPoint pos = this->getCellPos(event->pos()); - uint16_t metatileId = this->getMetatileId(pos.x(), pos.y()); - emit this->hoveredMetatileChanged(metatileId); + bool ok; + uint16_t metatileId = posToMetatileId(getCellPos(event->pos()), &ok); + if (ok) { + emit this->hoveredMetatileChanged(metatileId); + } else { + emit this->hoveredMetatileCleared(); + } } void TilesetEditorMetatileSelector::hoverLeaveEvent(QGraphicsSceneHoverEvent*) { emit this->hoveredMetatileCleared(); } -QPoint TilesetEditorMetatileSelector::getMetatileIdCoords(uint16_t metatileId) { - if (!Tileset::metatileIsValid(metatileId, this->primaryTileset, this->secondaryTileset)) - { - // Invalid metatile id. - return QPoint(0, 0); - } - int index = metatileId < Project::getNumMetatilesPrimary() - ? metatileId - : metatileId - Project::getNumMetatilesPrimary() + this->numPrimaryMetatilesRounded(); - return QPoint(index % this->numMetatilesWide, index / this->numMetatilesWide); +uint16_t TilesetEditorMetatileSelector::posToMetatileId(const QPoint &pos, bool *ok) const { + return posToMetatileId(pos.x(), pos.y(), ok); } -QPoint TilesetEditorMetatileSelector::getMetatileIdCoordsOnWidget(uint16_t metatileId) { - QPoint pos = getMetatileIdCoords(metatileId); +uint16_t TilesetEditorMetatileSelector::posToMetatileId(int x, int y, bool *ok) const { + if (ok) *ok = true; + int index = y * this->numMetatilesWide + x; + uint16_t metatileId = static_cast(index); + if (this->primaryTileset && this->primaryTileset->contains(metatileId)) { + return metatileId; + } + + // There's some extra handling here because we round the tilesets to keep them on separate rows. + // This means if the maximum number of primary metatiles is not divisible by the metatile width + // then the metatiles we used to round the primary tileset would have the index of valid secondary metatiles. + // These need to be ignored, or they'll appear to be duplicates of the subseqeunt secondary metatiles. + int numPrimaryRounded = numPrimaryMetatilesRounded(); + int firstSecondaryRow = numPrimaryRounded / this->numMetatilesWide; + metatileId = static_cast(Project::getNumMetatilesPrimary() + index - numPrimaryRounded); + if (this->secondaryTileset && this->secondaryTileset->contains(metatileId) && y >= firstSecondaryRow) { + return metatileId; + } + + if (ok) *ok = false; + return 0; +} + +QPoint TilesetEditorMetatileSelector::metatileIdToPos(uint16_t metatileId, bool *ok) const { + if (this->primaryTileset && this->primaryTileset->contains(metatileId)) { + if (ok) *ok = true; + int index = metatileId; + return QPoint(index % this->numMetatilesWide, index / this->numMetatilesWide); + } + if (this->secondaryTileset && this->secondaryTileset->contains(metatileId)) { + if (ok) *ok = true; + int index = metatileId - Project::getNumMetatilesPrimary() + numPrimaryMetatilesRounded(); + return QPoint(index % this->numMetatilesWide, index / this->numMetatilesWide); + } + + if (ok) *ok = false; + return QPoint(0,0); +} + +QPoint TilesetEditorMetatileSelector::getMetatileIdCoordsOnWidget(uint16_t metatileId) const { + QPoint pos = metatileIdToPos(metatileId); pos.rx() = (pos.x() * this->cellWidth) + (this->cellWidth / 2); pos.ry() = (pos.y() * this->cellHeight) + (this->cellHeight / 2); return pos; diff --git a/src/ui/tileseteditortileselector.cpp b/src/ui/tileseteditortileselector.cpp index 034a86a7..95d5be6c 100644 --- a/src/ui/tileseteditortileselector.cpp +++ b/src/ui/tileseteditortileselector.cpp @@ -4,7 +4,7 @@ #include #include -QPoint TilesetEditorTileSelector::getSelectionDimensions() { +QPoint TilesetEditorTileSelector::getSelectionDimensions() const { if (this->externalSelection) { return QPoint(this->externalSelectionWidth, this->externalSelectionHeight); } else { @@ -12,53 +12,40 @@ QPoint TilesetEditorTileSelector::getSelectionDimensions() { } } -void TilesetEditorTileSelector::draw() { - if (!this->primaryTileset || !this->secondaryTileset) { - this->setPixmap(QPixmap()); +void TilesetEditorTileSelector::updateBasePixmap() { + if (!this->primaryTileset || !this->secondaryTileset || this->numTilesWide == 0) { + this->basePixmap = QPixmap(); + return; } int totalTiles = Project::getNumTilesTotal(); - int primaryLength = this->primaryTileset->tiles.length(); - int secondaryLength = this->secondaryTileset->tiles.length(); int height = totalTiles / this->numTilesWide; - QList palette = Tileset::getPalette(this->paletteId, this->primaryTileset, this->secondaryTileset, true); QImage image(this->numTilesWide * this->cellWidth, height * this->cellHeight, QImage::Format_RGBA8888); QPainter painter(&image); - for (uint16_t tile = 0; tile < totalTiles; tile++) { - QImage tileImage; - if (tile < primaryLength) { - tileImage = getPalettedTileImage(tile, this->primaryTileset, this->secondaryTileset, this->paletteId, true).scaled(this->cellWidth, this->cellHeight); - } else if (tile < Project::getNumTilesPrimary()) { - tileImage = QImage(this->cellWidth, this->cellHeight, QImage::Format_RGBA8888); - tileImage.fill(palette.at(0)); - } else if (tile < Project::getNumTilesPrimary() + secondaryLength) { - tileImage = getPalettedTileImage(tile, this->primaryTileset, this->secondaryTileset, this->paletteId, true).scaled(this->cellWidth, this->cellHeight); - } else { - tileImage = QImage(this->cellWidth, this->cellHeight, QImage::Format_RGBA8888); - QPainter painter(&tileImage); - painter.fillRect(0, 0, this->cellWidth, this->cellHeight, palette.at(0)); - } - - int y = tile / this->numTilesWide; - int x = tile % this->numTilesWide; - QPoint origin = QPoint(x * this->cellWidth, y * this->cellHeight); - painter.drawImage(origin, tileImage); + for (uint16_t tileId = 0; tileId < totalTiles; tileId++) { + QImage tileImage = getPalettedTileImage(tileId, this->primaryTileset, this->secondaryTileset, this->paletteId, true) + .scaled(this->cellWidth, this->cellHeight); + int x = (tileId % this->numTilesWide) * this->cellWidth; + int y = (tileId / this->numTilesWide) * this->cellHeight; + painter.drawImage(x, y, tileImage); } if (this->showDivider) { - int row = this->primaryTileset->tiles.length() / this->numTilesWide; - if (this->primaryTileset->tiles.length() % this->numTilesWide != 0) { - // Round up height for incomplete last row - row++; - } + int row = Util::roundUpToMultiple(Project::getNumTilesPrimary(), this->numTilesWide) / this->numTilesWide; const int y = row * this->cellHeight; painter.setPen(Qt::white); painter.drawLine(0, y, this->numTilesWide * this->cellWidth, y); } painter.end(); - this->setPixmap(QPixmap::fromImage(image)); + this->basePixmap = QPixmap::fromImage(image); +} + +void TilesetEditorTileSelector::draw() { + if (this->basePixmap.isNull()) + updateBasePixmap(); + setPixmap(this->basePixmap); if (!this->externalSelection || (this->externalSelectionWidth == 1 && this->externalSelectionHeight == 1)) { this->drawSelection(); @@ -82,12 +69,14 @@ void TilesetEditorTileSelector::highlight(uint16_t tile) { void TilesetEditorTileSelector::setTilesets(Tileset *primaryTileset, Tileset *secondaryTileset) { this->primaryTileset = primaryTileset; this->secondaryTileset = secondaryTileset; + this->updateBasePixmap(); this->draw(); } void TilesetEditorTileSelector::setPaletteId(int paletteId) { this->paletteId = paletteId; this->paletteChanged = true; + this->updateBasePixmap(); this->draw(); } @@ -213,7 +202,7 @@ void TilesetEditorTileSelector::hoverLeaveEvent(QGraphicsSceneHoverEvent*) { } QPoint TilesetEditorTileSelector::getTileCoords(uint16_t tile) { - if (tile >= Project::getNumTilesTotal()) + if (tile >= Project::getNumTilesTotal() || this->numTilesWide == 0) { // Invalid tile. return QPoint(0, 0); @@ -233,30 +222,30 @@ QImage TilesetEditorTileSelector::buildPrimaryTilesIndexedImage() { if (!this->primaryTileset) return QImage(); - return buildImage(0, this->primaryTileset->tiles.length()); + return buildImage(0, this->primaryTileset->numTiles()); } QImage TilesetEditorTileSelector::buildSecondaryTilesIndexedImage() { if (!this->secondaryTileset) return QImage(); - return buildImage(Project::getNumTilesPrimary(), this->secondaryTileset->tiles.length()); + return buildImage(Project::getNumTilesPrimary(), this->secondaryTileset->numTiles()); } QImage TilesetEditorTileSelector::buildImage(int tileIdStart, int numTiles) { - const int tileWidth = 8; - const int tileHeight = 8; + if (this->numTilesWide == 0) + return QImage(); + int height = qCeil(numTiles / static_cast(this->numTilesWide)); - QImage image(this->numTilesWide * tileWidth, height * tileHeight, QImage::Format_RGBA8888); + QImage image(this->numTilesWide * Tile::pixelWidth(), height * Tile::pixelHeight(), QImage::Format_RGBA8888); image.fill(0); QPainter painter(&image); for (int i = 0; i < numTiles; i++) { QImage tileImage = getGreyscaleTileImage(tileIdStart + i, this->primaryTileset, this->secondaryTileset); - int y = i / this->numTilesWide; - int x = i % this->numTilesWide; - QPoint origin = QPoint(x * tileWidth, y * tileHeight); - painter.drawImage(origin, tileImage); + int x = (i % this->numTilesWide) * Tile::pixelWidth(); + int y = (i / this->numTilesWide) * Tile::pixelHeight(); + painter.drawImage(x, y, tileImage); } painter.end(); diff --git a/src/ui/uintspinbox.cpp b/src/ui/uintspinbox.cpp index 095d754e..7155aa67 100644 --- a/src/ui/uintspinbox.cpp +++ b/src/ui/uintspinbox.cpp @@ -2,24 +2,24 @@ #include UIntSpinBox::UIntSpinBox(QWidget *parent) - : QAbstractSpinBox(parent) + : QAbstractSpinBox(parent), + m_minimum(0), + m_maximum(99), + m_value(m_minimum), + m_singleStep(1), + m_displayIntegerBase(10), + m_hasPadding(false), + m_numChars(2) { // Don't let scrolling hijack focus. setFocusPolicy(Qt::StrongFocus); - m_minimum = 0; - m_maximum = 99; - m_value = m_minimum; - m_displayIntegerBase = 10; - m_numChars = 2; - m_hasPadding = false; - this->updateEdit(); connect(lineEdit(), SIGNAL(textEdited(QString)), this, SLOT(onEditFinished())); }; void UIntSpinBox::setValue(uint32_t val) { - val = qMax(m_minimum, qMin(m_maximum, val)); + val = qBound(m_minimum, val, m_maximum); if (m_value != val) { m_value = val; emit valueChanged(m_value); @@ -69,6 +69,12 @@ void UIntSpinBox::setRange(uint32_t min, uint32_t max) { this->updateEdit(); } +void UIntSpinBox::setSingleStep(uint32_t val) { + if (m_singleStep != val) { + m_singleStep = val; + } +} + void UIntSpinBox::setPrefix(const QString &prefix) { if (m_prefix != prefix) { m_prefix = prefix; @@ -132,6 +138,7 @@ void UIntSpinBox::onEditFinished() { } void UIntSpinBox::stepBy(int steps) { + steps *= m_singleStep; auto newValue = m_value; if (steps < 0 && newValue + steps > newValue) { newValue = 0; diff --git a/src/ui/wildmonchart.cpp b/src/ui/wildmonchart.cpp index fd2d9216..ea5b4814 100644 --- a/src/ui/wildmonchart.cpp +++ b/src/ui/wildmonchart.cpp @@ -367,7 +367,7 @@ QChart* WildMonChart::createLevelDistributionChart() { series->attachAxis(axisY); // We round the y-axis max up to a multiple of 5. - axisY->setMax(Util::roundUp(qCeil(axisY->max()), 5)); + axisY->setMax(Util::roundUpToMultiple(qCeil(axisY->max()), 5)); return chart; }