[nostalgia/core] Add ability to save tile sheets and store columns and rows in ng file

This commit is contained in:
Gary Talent 2020-03-11 00:17:07 -05:00
parent 748fea3d43
commit bf00ff0e41
8 changed files with 197 additions and 51 deletions

View File

@ -1 +0,0 @@
<03><>

Binary file not shown.

Binary file not shown.

View File

@ -34,8 +34,11 @@ struct NostalgiaPalette {
}; };
struct NostalgiaGraphic { struct NostalgiaGraphic {
static constexpr auto Fields = 4; static constexpr auto Fields = 6;
uint8_t bpp = 0; int8_t bpp = 0;
// rows and columns are really only used by TileSheetEditor
int rows = 1;
int columns = 1;
ox::FileAddress defaultPalette; ox::FileAddress defaultPalette;
NostalgiaPalette pal; NostalgiaPalette pal;
ox::Vector<uint8_t> tiles; ox::Vector<uint8_t> tiles;
@ -52,6 +55,8 @@ template<typename T>
ox::Error model(T *io, NostalgiaGraphic *ng) { ox::Error model(T *io, NostalgiaGraphic *ng) {
io->setTypeInfo("nostalgia::core::NostalgiaGraphic", NostalgiaGraphic::Fields); io->setTypeInfo("nostalgia::core::NostalgiaGraphic", NostalgiaGraphic::Fields);
oxReturnError(io->field("bpp", &ng->bpp)); oxReturnError(io->field("bpp", &ng->bpp));
oxReturnError(io->field("rows", &ng->rows));
oxReturnError(io->field("columns", &ng->columns));
oxReturnError(io->field("defaultPalette", &ng->defaultPalette)); oxReturnError(io->field("defaultPalette", &ng->defaultPalette));
oxReturnError(io->field("pal", &ng->pal)); oxReturnError(io->field("pal", &ng->pal));
oxReturnError(io->field("tiles", &ng->tiles)); oxReturnError(io->field("tiles", &ng->tiles));

View File

@ -104,6 +104,10 @@ Rectangle {
height: tileGrid.rows * tileGrid.baseTileSize * tileGrid.scaleFactor height: tileGrid.rows * tileGrid.baseTileSize * tileGrid.scaleFactor
rows: sheetData ? sheetData.rows : 1 rows: sheetData ? sheetData.rows : 1
columns: sheetData ? sheetData.columns : 1 columns: sheetData ? sheetData.columns : 1
states: State {
name: "widthChanged"
PropertyChanges { target: tileGrid.width; width: tileGrid.columns * tileGrid.baseTileSize * tileGrid.scaleFactor }
}
Repeater { Repeater {
model: tileGrid.rows * tileGrid.columns model: tileGrid.rows * tileGrid.columns
Tile { Tile {

View File

@ -15,6 +15,7 @@ constexpr auto PluginName = "NostalgiaCore";
// Command IDs to use with QUndoCommand::id() // Command IDs to use with QUndoCommand::id()
enum class CommandId { enum class CommandId {
UpdatePixel = 1, UpdatePixel = 1,
UpdateDimension = 2,
}; };
} }

View File

@ -10,14 +10,17 @@
#include <QHeaderView> #include <QHeaderView>
#include <QPointer> #include <QPointer>
#include <QQmlContext> #include <QQmlContext>
#include <QQuickItem>
#include <QQuickWidget> #include <QQuickWidget>
#include <QSet> #include <QSet>
#include <QSettings> #include <QSettings>
#include <QSpinBox> #include <QSpinBox>
#include <QSplitter> #include <QSplitter>
#include <QUndoCommand> #include <QUndoCommand>
#include <QTableWidget>
#include <QToolBar> #include <QToolBar>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <memory>
#include "consts.hpp" #include "consts.hpp"
#include "tilesheeteditor.hpp" #include "tilesheeteditor.hpp"
@ -53,6 +56,59 @@ struct LabeledSpinner: public QWidget {
}; };
class UpdateDimensionsCommand: public QUndoCommand {
public:
enum class Dimension {
Rows,
Columns,
};
private:
Dimension m_dimension = Dimension::Rows;
int m_oldVal = 0;
int m_newVal = 0;
SheetData *m_sheetData = nullptr;
public:
UpdateDimensionsCommand(SheetData *sheetData, Dimension dim, int oldVal, int newVal) {
m_sheetData = sheetData;
m_dimension = dim;
m_newVal = newVal;
m_oldVal = oldVal;
setObsolete(newVal == oldVal);
}
virtual ~UpdateDimensionsCommand() = default;
int id() const override {
return static_cast<int>(CommandId::UpdateDimension);
}
void redo() override {
switch (m_dimension) {
case Dimension::Rows:
m_sheetData->setRows(m_newVal);
break;
case Dimension::Columns:
m_sheetData->setColumns(m_newVal);
break;
}
}
void undo() override {
switch (m_dimension) {
case Dimension::Rows:
m_sheetData->setRows(m_oldVal);
break;
case Dimension::Columns:
m_sheetData->setColumns(m_oldVal);
break;
}
}
};
class UpdatePixelsCommand: public QUndoCommand { class UpdatePixelsCommand: public QUndoCommand {
private: private:
struct PixelUpdate { struct PixelUpdate {
@ -96,8 +152,8 @@ class UpdatePixelsCommand: public QUndoCommand {
} }
bool mergeWith(const QUndoCommand *cmd) override { bool mergeWith(const QUndoCommand *cmd) override {
auto other = static_cast<const UpdatePixelsCommand*>(cmd); auto other = dynamic_cast<const UpdatePixelsCommand*>(cmd);
if (m_cmdIdx == other->m_cmdIdx) { if (other && m_cmdIdx == other->m_cmdIdx) {
m_pixelUpdates.unite(other->m_pixelUpdates); m_pixelUpdates.unite(other->m_pixelUpdates);
return true; return true;
} }
@ -107,7 +163,14 @@ class UpdatePixelsCommand: public QUndoCommand {
void redo() override { void redo() override {
for (const auto &pu : m_pixelUpdates) { for (const auto &pu : m_pixelUpdates) {
pu.item->setProperty("color", m_palette[m_newColorId]); pu.item->setProperty("color", m_palette[m_newColorId]);
m_pixels[pu.item->property("pixelNumber").toInt()] = m_newColorId; const auto index = pu.item->property("pixelNumber").toInt();
// resize to appropriate number of tiles if pixel beyond current
// range
if (m_pixels.size() < index) {
auto sz = (index / 64 + 1) * 64;
m_pixels.resize(sz);
}
m_pixels[index] = m_newColorId;
} }
} }
@ -117,6 +180,7 @@ class UpdatePixelsCommand: public QUndoCommand {
m_pixels[pu.item->property("pixelNumber").toInt()] = pu.oldColorId; m_pixels[pu.item->property("pixelNumber").toInt()] = pu.oldColorId;
} }
} }
}; };
@ -145,19 +209,49 @@ QStringList SheetData::palette() {
return m_palette; return m_palette;
} }
void SheetData::updatePixels(const studio::Context *ctx, QString ngPath, QString palPath) { void SheetData::load(const studio::Context *ctx, QString ngPath, QString palPath) {
auto ng = ctx->project->loadObj<NostalgiaGraphic>(ngPath); auto ng = ctx->project->loadObj<NostalgiaGraphic>(ngPath);
std::unique_ptr<NostalgiaPalette> npal;
if (palPath == "" && ng->defaultPalette.type() == ox::FileAddressType::Path) { if (palPath == "" && ng->defaultPalette.type() == ox::FileAddressType::Path) {
palPath = ng->defaultPalette.getPath().value; palPath = ng->defaultPalette.getPath().value;
} }
m_tilesheetPath = ngPath;
m_currentPalettePath = palPath;
setRows(ng->rows);
setColumns(ng->columns);
if (palPath != "") {
setPalette(ctx, palPath);
} else {
setPalette(&ng->pal);
}
updatePixels(ng.get());
}
void SheetData::save(const studio::Context *ctx, QString ngPath) {
auto ng = toNostalgiaGraphic();
ctx->project->writeObj(ngPath, ng.get());
}
void SheetData::setPalette(const NostalgiaPalette *npal) {
// load palette
m_palette.clear();
for (std::size_t i = 0; i < npal->colors.size(); i++) {
const auto c = toQColor(npal->colors[i]);
const auto color = c.name(QColor::HexArgb);
m_palette.append(color);
}
}
void SheetData::setPalette(const studio::Context *ctx, QString palPath) {
std::unique_ptr<NostalgiaPalette> npal;
try { try {
npal = ctx->project->loadObj<NostalgiaPalette>(palPath); npal = ctx->project->loadObj<NostalgiaPalette>(palPath);
qInfo() << "Opened palette" << palPath; qInfo() << "Opened palette" << palPath;
} catch (ox::Error) { } catch (ox::Error) {
qWarning() << "Could not open palette" << palPath; qWarning() << "Could not open palette" << palPath;
} }
updatePixels(ng.get(), npal.get()); if (npal) {
setPalette(npal.get());
}
} }
void SheetData::setSelectedColor(int index) { void SheetData::setSelectedColor(int index) {
@ -171,25 +265,24 @@ QUndoStack *SheetData::undoStack() {
void SheetData::setColumns(int columns) { void SheetData::setColumns(int columns) {
m_columns = columns; m_columns = columns;
emit columnsChanged(columns); emit columnsChanged(columns);
emit changeOccurred();
} }
void SheetData::setRows(int rows) { void SheetData::setRows(int rows) {
m_rows = rows; m_rows = rows;
emit rowsChanged(rows); emit rowsChanged(rows);
emit changeOccurred();
} }
void SheetData::updatePixels(const NostalgiaGraphic *ng, const NostalgiaPalette *npal) { void SheetData::updateColumns(int columns) {
if (!npal) { m_cmdStack.push(new UpdateDimensionsCommand(this, UpdateDimensionsCommand::Dimension::Columns, m_columns, columns));
npal = &ng->pal; }
}
// load palette void SheetData::updateRows(int rows) {
for (std::size_t i = 0; i < npal->colors.size(); i++) { m_cmdStack.push(new UpdateDimensionsCommand(this, UpdateDimensionsCommand::Dimension::Rows, m_rows, rows));
const auto c = toQColor(npal->colors[i]); }
const auto color = c.name(QColor::HexArgb);
m_palette.append(color);
}
void SheetData::updatePixels(const NostalgiaGraphic *ng) {
if (ng->bpp == 8) { if (ng->bpp == 8) {
for (std::size_t i = 0; i < ng->tiles.size(); i++) { for (std::size_t i = 0; i < ng->tiles.size(); i++) {
m_pixels.push_back(ng->tiles[i]); m_pixels.push_back(ng->tiles[i]);
@ -209,6 +302,31 @@ void SheetData::updatePixels(const NostalgiaGraphic *ng, const NostalgiaPalette
emit paletteChanged(); emit paletteChanged();
} }
std::unique_ptr<NostalgiaGraphic> SheetData::toNostalgiaGraphic() {
auto ng = std::make_unique<NostalgiaGraphic>();
const auto highestColorIdx = static_cast<uint8_t>(*std::max_element(m_pixels.begin(), m_pixels.end()));
ng->defaultPalette = m_currentPalettePath.toUtf8().data();
ng->bpp = highestColorIdx > 15 ? 8 : 4;
ng->columns = m_columns;
ng->rows = m_rows;
if (ng->bpp == 4) {
ng->tiles.resize(m_pixels.size() / 2);
for (int i = 0; i < m_pixels.size(); ++i) {
if (i & 1) {
ng->tiles[i / 2] |= static_cast<uint8_t>(m_pixels[i]) << 4;
} else {
ng->tiles[i / 2] = static_cast<uint8_t>(m_pixels[i]);
}
}
} else {
ng->tiles.resize(m_pixels.size());
for (int i = 0; i < m_pixels.size(); ++i) {
ng->tiles[i] = static_cast<uint8_t>(m_pixels[i]);
}
}
return ng;
}
void SheetData::beginCmd() { void SheetData::beginCmd() {
++m_cmdIdx; ++m_cmdIdx;
} }
@ -220,6 +338,7 @@ void SheetData::endCmd() {
TileSheetEditor::TileSheetEditor(QString path, const studio::Context *ctx, QWidget *parent): studio::Editor(parent) { TileSheetEditor::TileSheetEditor(QString path, const studio::Context *ctx, QWidget *parent): studio::Editor(parent) {
m_ctx = ctx; m_ctx = ctx;
m_itemPath = path;
m_itemName = path.mid(path.lastIndexOf('/')); m_itemName = path.mid(path.lastIndexOf('/'));
auto lyt = new QVBoxLayout(this); auto lyt = new QVBoxLayout(this);
m_splitter = new QSplitter(this); m_splitter = new QSplitter(this);
@ -227,12 +346,29 @@ TileSheetEditor::TileSheetEditor(QString path, const studio::Context *ctx, QWidg
auto canvasLyt = new QVBoxLayout(canvasParent); auto canvasLyt = new QVBoxLayout(canvasParent);
m_canvas = new QQuickWidget(canvasParent); m_canvas = new QQuickWidget(canvasParent);
canvasLyt->addWidget(m_canvas); canvasLyt->addWidget(m_canvas);
canvasLyt->setMenuBar(setupToolBar()); auto tb = new QToolBar(tr("Tile Sheet Options"));
m_tilesX = new LabeledSpinner(tr("Tiles &X:"), 1, m_sheetData.columns());
m_tilesY = new LabeledSpinner(tr("Tiles &Y:"), 1, m_sheetData.rows());
tb->addWidget(m_tilesX);
tb->addWidget(m_tilesY);
canvasLyt->setMenuBar(tb);
lyt->addWidget(m_splitter); lyt->addWidget(m_splitter);
m_splitter->addWidget(canvasParent); m_splitter->addWidget(canvasParent);
m_splitter->addWidget(setupColorPicker(m_splitter)); m_splitter->addWidget(setupColorPicker(m_splitter));
m_splitter->setStretchFactor(0, 1); m_splitter->setStretchFactor(0, 1);
m_sheetData.updatePixels(m_ctx, path); connect(&m_sheetData, &SheetData::columnsChanged, [this](int val) {
disconnect(m_tilesX->spinBox, QOverload<int>::of(&QSpinBox::valueChanged), &m_sheetData, &SheetData::updateColumns);
m_tilesX->spinBox->setValue(val);
connect(m_tilesX->spinBox, QOverload<int>::of(&QSpinBox::valueChanged), &m_sheetData, &SheetData::updateColumns);
});
connect(&m_sheetData, &SheetData::rowsChanged, [this](int val) {
disconnect(m_tilesY->spinBox, QOverload<int>::of(&QSpinBox::valueChanged), &m_sheetData, &SheetData::updateRows);
m_tilesY->spinBox->setValue(val);
connect(m_tilesY->spinBox, QOverload<int>::of(&QSpinBox::valueChanged), &m_sheetData, &SheetData::updateRows);
});
m_sheetData.load(m_ctx, m_itemPath);
connect(m_tilesX->spinBox, QOverload<int>::of(&QSpinBox::valueChanged), &m_sheetData, &SheetData::updateColumns);
connect(m_tilesY->spinBox, QOverload<int>::of(&QSpinBox::valueChanged), &m_sheetData, &SheetData::updateRows);
m_canvas->rootContext()->setContextProperty("sheetData", &m_sheetData); m_canvas->rootContext()->setContextProperty("sheetData", &m_sheetData);
m_canvas->setSource(QUrl::fromLocalFile(":/qml/TileSheetEditor.qml")); m_canvas->setSource(QUrl::fromLocalFile(":/qml/TileSheetEditor.qml"));
m_canvas->setResizeMode(QQuickWidget::SizeRootObjectToView); m_canvas->setResizeMode(QQuickWidget::SizeRootObjectToView);
@ -249,24 +385,12 @@ QString TileSheetEditor::itemName() {
return m_itemName; return m_itemName;
} }
void TileSheetEditor::saveItem() {
}
QUndoStack *TileSheetEditor::undoStack() { QUndoStack *TileSheetEditor::undoStack() {
return m_sheetData.undoStack(); return m_sheetData.undoStack();
} }
QWidget *TileSheetEditor::setupToolBar() { void TileSheetEditor::saveItem() {
auto tb = new QToolBar(tr("Tile Sheet Options")); m_sheetData.save(m_ctx, m_itemPath);
m_tilesX = new LabeledSpinner(tr("Tiles &X:"), 1, m_sheetData.columns());
m_tilesY = new LabeledSpinner(tr("Tiles &Y:"), 1, m_sheetData.rows());
tb->addWidget(m_tilesX);
tb->addWidget(m_tilesY);
connect(&m_sheetData, &SheetData::columnsChanged, m_tilesX->spinBox, &QSpinBox::setValue);
connect(&m_sheetData, &SheetData::rowsChanged, m_tilesY->spinBox, &QSpinBox::setValue);
connect(m_tilesX->spinBox, QOverload<int>::of(&QSpinBox::valueChanged), &m_sheetData, &SheetData::setColumns);
connect(m_tilesY->spinBox, QOverload<int>::of(&QSpinBox::valueChanged), &m_sheetData, &SheetData::setRows);
return tb;
} }
QWidget *TileSheetEditor::setupColorPicker(QWidget *parent) { QWidget *TileSheetEditor::setupColorPicker(QWidget *parent) {
@ -314,8 +438,6 @@ void TileSheetEditor::restoreState() {
QSettings settings(m_ctx->orgName, PluginName); QSettings settings(m_ctx->orgName, PluginName);
settings.beginGroup("TileSheetEditor/" + m_itemName); settings.beginGroup("TileSheetEditor/" + m_itemName);
m_splitter->restoreState(settings.value("m_splitter/state", m_splitter->saveState()).toByteArray()); m_splitter->restoreState(settings.value("m_splitter/state", m_splitter->saveState()).toByteArray());
m_sheetData.setRows(settings.value("m_sheetData/tileRows", 1).toInt());
m_sheetData.setColumns(settings.value("m_sheetData/tileColumns", 1).toInt());
m_colorPicker.colorTable->horizontalHeader()->restoreState(settings.value("m_colorPicker.colorTable/geometry", m_colorPicker.colorTable->horizontalHeader()->saveState()).toByteArray()); m_colorPicker.colorTable->horizontalHeader()->restoreState(settings.value("m_colorPicker.colorTable/geometry", m_colorPicker.colorTable->horizontalHeader()->saveState()).toByteArray());
settings.endGroup(); settings.endGroup();
} }

View File

@ -8,14 +8,9 @@
#pragma once #pragma once
#include <QQuickItem>
#include <QSplitter>
#include <QStringList> #include <QStringList>
#include <QStringView>
#include <QTableWidget>
#include <QUndoStack> #include <QUndoStack>
#include <QVariant> #include <QVariant>
#include <QWidget>
#include <nostalgia/core/gfx.hpp> #include <nostalgia/core/gfx.hpp>
#include <nostalgia/studio/studio.hpp> #include <nostalgia/studio/studio.hpp>
@ -30,7 +25,9 @@ class SheetData: public QObject {
Q_PROPERTY(QStringList palette READ palette NOTIFY paletteChanged) Q_PROPERTY(QStringList palette READ palette NOTIFY paletteChanged)
private: private:
QQuickItem *m_prevPixelUpdated = nullptr; class QQuickItem *m_prevPixelUpdated = nullptr;
QString m_tilesheetPath;
QString m_currentPalettePath;
uint64_t m_cmdIdx = 0; uint64_t m_cmdIdx = 0;
QUndoStack m_cmdStack; QUndoStack m_cmdStack;
QStringList m_palette; QStringList m_palette;
@ -54,7 +51,13 @@ class SheetData: public QObject {
QStringList palette(); QStringList palette();
void updatePixels(const studio::Context *ctx, QString ngPath, QString palPath = ""); void load(const studio::Context *ctx, QString ngPath, QString palPath = "");
void save(const studio::Context *ctx, QString ngPath);
void setPalette(const NostalgiaPalette *pal);
void setPalette(const studio::Context *ctx, QString palPath);
void setSelectedColor(int index); void setSelectedColor(int index);
@ -65,8 +68,20 @@ class SheetData: public QObject {
void setRows(int rows); void setRows(int rows);
/**
* Sets columns through a QUndoCommand.
*/
void updateColumns(int columns);
/**
* Sets rows through a QUndoCommand.
*/
void updateRows(int rows);
private: private:
void updatePixels(const NostalgiaGraphic *ng, const NostalgiaPalette *npal); void updatePixels(const NostalgiaGraphic *ng);
[[nodiscard]] std::unique_ptr<NostalgiaGraphic> toNostalgiaGraphic();
signals: signals:
void changeOccurred(); void changeOccurred();
@ -86,16 +101,17 @@ class TileSheetEditor: public studio::Editor {
Q_OBJECT Q_OBJECT
private: private:
QString m_itemPath;
QString m_itemName; QString m_itemName;
const studio::Context *m_ctx = nullptr; const studio::Context *m_ctx = nullptr;
SheetData m_sheetData; SheetData m_sheetData;
QSplitter *m_splitter = nullptr; class QSplitter *m_splitter = nullptr;
struct LabeledSpinner *m_tilesX = nullptr; struct LabeledSpinner *m_tilesX = nullptr;
struct LabeledSpinner *m_tilesY = nullptr; struct LabeledSpinner *m_tilesY = nullptr;
class QQuickWidget* m_canvas = nullptr; class QQuickWidget* m_canvas = nullptr;
struct { struct {
QComboBox *palette = nullptr; QComboBox *palette = nullptr;
QTableWidget *colorTable = nullptr; class QTableWidget *colorTable = nullptr;
} m_colorPicker; } m_colorPicker;
public: public:
@ -105,13 +121,12 @@ class TileSheetEditor: public studio::Editor {
QString itemName() override; QString itemName() override;
void saveItem() override;
QUndoStack *undoStack() override; QUndoStack *undoStack() override;
private: protected:
QWidget *setupToolBar(); void saveItem() override;
private:
QWidget *setupColorPicker(QWidget *widget); QWidget *setupColorPicker(QWidget *widget);
void setColorTable(QStringList hexColors); void setColorTable(QStringList hexColors);