21using TileKey = std::pair<uint16_t, uint16_t>;
26constexpr int kTilesPerRow = kSheetWidth / 8;
30 std::array<bool, 8> seen{};
31 for (
const auto value : permutation) {
32 if (value >= seen.size() || seen[value]) {
33 return absl::InvalidArgumentError(
34 "Pixel permutation must be a bijection on 0..7 with zero fixed");
38 if (permutation[0] != 0) {
39 return absl::InvalidArgumentError(
40 "Pixel permutation must keep transparent index zero fixed");
42 return absl::OkStatus();
51 std::all_of(pixels.begin(), pixels.end(),
52 [](uint8_t value) { return value <= 7; });
58 if (!selection.is_active || selection.is_floating || !store ||
60 selection.height <= 0 || selection.width > kSheetWidth ||
61 selection.height >
kSheetHeight || selection.x < 0 || selection.y < 0 ||
62 selection.x > kSheetWidth - selection.width ||
67 selection.pixel_data.resize(selection.width * selection.height);
68 for (
int y = 0; y < selection.height; ++y) {
69 for (
int x = 0; x < selection.width; ++x) {
70 selection.pixel_data[y * selection.width + x] =
71 pixels[(selection.y + y) * kSheetWidth + selection.x + x];
79 return absl::InvalidArgumentError(
"Pixel edit plan has no valid selection");
81 std::set<TileKey> target_keys;
83 for (
const auto& target : plan.
targets) {
84 const auto snapshot = plan.
before_sheets.find(target.sheet_id);
86 target.tile_index >= kTilesPerSheet || target.pixel_mask == 0 ||
87 target.hflip || target.vflip ||
90 !target_keys.emplace(target.sheet_id, target.tile_index).second ||
93 return absl::InvalidArgumentError(
94 "Invalid physical pixel edit selection");
96 const int start = (target.tile_index / kTilesPerRow) * 8 * kSheetWidth +
97 (target.tile_index % kTilesPerRow) * 8;
98 for (
int y = 0; y < 8; ++y) {
99 for (
int x = 0; x < 8; ++x) {
100 if ((target.pixel_mask & (uint64_t{1} << (y * 8 + x))) != 0) {
101 selected_offsets[target.sheet_id].insert(start + y * kSheetWidth + x);
106 return selected_offsets;
111 std::vector<SourceCellKey> cells;
113 if (cell.target >= source.
targets.size()) {
114 return absl::FailedPreconditionError(
115 "Graphics source pattern has an unavailable tile target");
117 const auto& target = source.
targets[cell.target];
118 if (selected.contains({target.sheet_id, target.tile_index})) {
119 cells.emplace_back(cell.x, cell.y, cell.target, cell.hflip, cell.vflip);
122 std::sort(cells.begin(), cells.end());
129 return absl::InvalidArgumentError(
"Pixel edit source guard is missing");
131 return absl::OkStatus();
134 !plan.
source->resolve_live_source) {
135 return absl::FailedPreconditionError(
136 "This graphics source cannot resolve live pixel assignments");
144 return absl::FailedPreconditionError(
145 "Pixel edit source session or area changed");
147 std::set<TileKey> selected;
148 for (
const auto& target : plan.
targets) {
149 selected.emplace(target.sheet_id, target.tile_index);
151 std::vector<size_t> fresh_targets;
152 for (
size_t i = 0; i < fresh.
targets.size(); ++i) {
153 const auto& target = fresh.
targets[i];
154 if (selected.contains({target.sheet_id, target.tile_index})) {
155 fresh_targets.push_back(i);
159 return absl::FailedPreconditionError(
160 "Selected graphics source tile aliases changed");
163 if (index >= plan.
source->targets.size() || index >= fresh.
targets.size()) {
164 return absl::FailedPreconditionError(
"Selected graphics source changed");
166 const auto& old = plan.
source->targets[index];
167 const auto& now = fresh.
targets[index];
168 if (old.sheet_id != now.sheet_id || old.tile_index != now.tile_index ||
169 old.source_orientation_mask != now.source_orientation_mask) {
170 return absl::FailedPreconditionError(
171 "Source tile assignments changed; reopen Graphics from the source");
179 old_cells != new_cells) {
180 return absl::FailedPreconditionError(
181 "Source pattern positions or orientations changed; reopen Graphics "
184 return absl::OkStatus();
190 return absl::OkStatus();
193 return absl::FailedPreconditionError(
"Graphics source owner changed");
195 std::set<TileKey> selected;
196 for (
const auto& target : plan.
targets) {
197 selected.emplace(target.sheet_id, target.tile_index);
199 for (
size_t i = 0; i < state.
source_context->targets.size(); ++i) {
201 if (selected.contains({target.sheet_id, target.tile_index})) {
209 return absl::OkStatus();
215 : state_(&state), plan_(plan) {}
223 std::string
Description()
const override {
return plan_.description; }
226 plan_.description.size();
227 for (
const auto& [sheet, pixels] : plan_.before_sheets) {
228 size += pixels.size();
230 for (
const auto& diff : plan_.sheets) {
231 size += diff.MemoryUsage();
244 uint16_t sheet_id,
int x,
int y,
int width,
int height) {
246 height <= 0 || width > kSheetWidth || height > kSheetHeight || x < 0 ||
247 y < 0 || x > kSheetWidth - width || y > kSheetHeight - height) {
248 return absl::InvalidArgumentError(
249 "Pixel selection must fit within one loaded 128x32 sheet");
251 std::map<uint16_t, uint64_t> masks;
252 for (
int yy = y; yy < y + height; ++yy) {
253 for (
int xx = x; xx < x + width; ++xx) {
254 const uint16_t tile = (yy / 8) * kTilesPerRow + xx / 8;
255 masks[tile] |= uint64_t{1} << ((yy % 8) * 8 + xx % 8);
258 std::vector<PixelIndexTarget> targets;
259 for (
const auto& [tile, mask] : masks) {
260 targets.push_back({sheet_id, tile, mask});
267 const std::vector<PixelIndexTarget>& targets,
272 return absl::FailedPreconditionError(
273 "Load this session's graphics and finish the floating paste first");
275 if (targets.empty()) {
276 return absl::InvalidArgumentError(
"Select tile pixels to remap");
282 std::map<TileKey, uint64_t> masks;
283 for (
const auto& target : targets) {
284 if (target.tile_index >= kTilesPerSheet ||
285 !store->HasSheet(target.sheet_id)) {
286 return absl::OutOfRangeError(
"Selected graphics tile is unavailable");
290 return absl::UnimplementedError(
291 "Pixel index permutations require a 3bpp graphics sheet");
293 const uint64_t mask = PhysicalMask(target);
295 masks[{target.sheet_id, target.tile_index}] |= mask;
299 return absl::InvalidArgumentError(
"Select at least one tile pixel");
302 std::map<uint16_t, std::vector<uint8_t>> next;
303 for (
const auto& [key, mask] : masks) {
304 const auto [sheet, tile] = key;
306 const auto pixels = store->Sheet(sheet);
307 if (!IsRaw3bpp(pixels)) {
308 return absl::FailedPreconditionError(
309 "Selected graphics sheet is not loaded as raw 3bpp indices");
313 std::vector<uint8_t>(pixels.begin(), pixels.end());
316 plan.
targets.push_back({sheet, tile, mask});
318 (tile / kTilesPerRow) * 8 * kSheetWidth + (tile % kTilesPerRow) * 8;
319 for (
int y = 0; y < 8; ++y) {
320 for (
int x = 0; x < 8; ++x) {
321 if ((mask & (uint64_t{1} << (y * 8 + x))) != 0) {
322 auto& pixel = next[sheet][start + y * kSheetWidth + x];
323 pixel = permutation[pixel];
331 for (
const auto& [sheet, pixels] : next) {
334 plan.
sheets.push_back(std::move(diff));
342 const std::vector<PixelIndexTarget>& targets,
343 const std::map<uint16_t, std::vector<uint8_t>>& after_sheets,
344 std::string description) {
348 if (after_sheets.size() != plan.before_sheets.size()) {
349 return absl::InvalidArgumentError(
350 "Artwork replacement needs exactly the selected graphics sheets");
353 for (
const auto& [sheet, before] : plan.before_sheets) {
354 const auto after = after_sheets.find(sheet);
355 if (after == after_sheets.end() || !IsRaw3bpp(after->second)) {
356 return absl::InvalidArgumentError(
357 "Artwork replacement needs full 4096-byte sheets with indices 0..7");
360 for (
const auto offset : diff.offsets) {
361 if (!selected_offsets.at(sheet).contains(offset)) {
362 return absl::InvalidArgumentError(
363 "Artwork replacement changes pixels outside the selected mask");
367 plan.sheets.push_back(std::move(diff));
371 plan.keep_transparency =
false;
372 if (!description.empty()) {
373 plan.description = std::move(description);
375 plan.description =
"Apply art to object source";
383 const std::function<absl::Status(
size_t)>& after_sheet_write) {
387 return absl::FailedPreconditionError(
388 "Pixel edit session changed or a floating paste is pending");
393 return absl::InvalidArgumentError(
"Invalid pixel edit execution mode");
399 return absl::InvalidArgumentError(
"Invalid pixel edit operation");
405 return absl::FailedPreconditionError(
406 "Graphics source changed since the pixel edit was planned");
410 const auto pixels = store->Sheet(sheet);
412 !IsRaw3bpp(snapshot->second)) {
413 return absl::InvalidArgumentError(
"Invalid pixel edit sheet snapshot");
415 if (!store->HasSheet(sheet) || store->Revision(sheet) != revision ||
416 !std::equal(pixels.begin(), pixels.end(), snapshot->second.begin(),
417 snapshot->second.end())) {
418 return absl::FailedPreconditionError(
419 "Graphics pixels changed since the pixel edit was planned");
425 std::map<uint16_t, std::vector<uint8_t>> next;
426 for (
const auto& diff : plan.
sheets) {
427 if (!store->HasSheet(diff.sheet) ||
429 !plan.
before_sheets.contains(diff.sheet) || diff.offsets.empty() ||
430 diff.offsets.size() != diff.before.size() ||
431 diff.offsets.size() != diff.after.size() || next.contains(diff.sheet)) {
432 return absl::InvalidArgumentError(
"Invalid pixel edit sheet diff");
434 const auto current = store->Sheet(diff.sheet);
435 if (!IsRaw3bpp(current)) {
436 return absl::FailedPreconditionError(
437 "Graphics sheet no longer contains raw 3bpp indices");
439 auto& pixels = next[diff.sheet];
440 pixels.assign(current.begin(), current.end());
441 std::set<uint16_t> offsets;
442 for (
size_t i = 0; i < diff.offsets.size(); ++i) {
443 const auto offset = diff.offsets[i];
444 if (offset >= pixels.size() || !offsets.insert(offset).second ||
445 !selected_offsets.contains(diff.sheet) ||
446 !selected_offsets.at(diff.sheet).contains(offset) ||
447 diff.before[i] > 7 || diff.after[i] > 7 ||
448 diff.before[i] == diff.after[i] ||
450 (diff.before[i] == 0 || diff.after[i] == 0)) ||
451 diff.before[i] != plan.
before_sheets.at(diff.sheet)[offset]) {
452 return absl::InvalidArgumentError(
"Invalid pixel edit pixel diff");
454 if (pixels[offset] != (redo ? diff.before[i] : diff.after[i])) {
455 return absl::FailedPreconditionError(
456 "Edited pixels changed since this history action");
458 pixels[offset] = redo ? diff.after[i] : diff.before[i];
462 const auto before = *store;
464 for (
const auto& [sheet, pixels] : next) {
465 auto status = store->WriteSheet(sheet, pixels, 7);
466 if (status.ok() && after_sheet_write) {
467 status = after_sheet_write(++written);
475 RefreshSelectionPixels(state);
477 for (
const auto& [sheet, pixels] : next) {
481 return absl::OkStatus();
489 if (!plan.
sheets.empty()) {
490 history.
Push(std::make_unique<PixelIndexAction>(state, plan));
492 return absl::OkStatus();
497 const std::vector<PixelIndexTarget>& targets,
506 const std::vector<PixelIndexTarget>& targets,
508 for (
const auto& target : targets) {
512 return absl::FailedPreconditionError(
513 "2bpp ROM sheets are read-only; swap IDs in a 2bpp scratch BIN");
518 std::copy_n(permutation.
ids.begin(), legacy.size(), legacy.begin());
524 const std::vector<PixelIndexTarget>& targets,
Shared state between GraphicsEditor panel components.
void MarkSheetModified(uint16_t sheet_id)
Mark a sheet as modified for save tracking.
std::optional< GraphicsEditContext > source_context
uint16_t current_sheet_id
uint64_t source_context_revision
zelda3::GameData * game_data
Abstract base for all undoable actions (Command pattern)
Manages undo/redo stacks for a single editor.
void Push(std::unique_ptr< UndoAction > action)
std::string Description() const override
Human-readable description (e.g., "Paint 12 tiles on map 5")
GraphicsEditorState * state_
PixelIndexAction(GraphicsEditorState &state, const PixelIndexPlan &plan)
absl::Status Redo() override
size_t MemoryUsage() const override
Approximate memory footprint for budget enforcement.
absl::Status Undo() override
static constexpr uint16_t kSheetCount
static constexpr size_t kSheetWidth
static constexpr size_t kSheetBytes
static constexpr size_t kSheetHeight
Locates graphics sheets in the ROM and writes edited sheets back.
One ROM session's graphics sheet pixels, with a revision per sheet.
#define ASSIGN_OR_RETURN(type_variable_name, expression)
std::tuple< int, int, size_t, bool, bool > SourceCellKey
absl::StatusOr< std::vector< SourceCellKey > > SelectedSourceCells(const GraphicsEditContext &source, const std::set< TileKey > &selected)
std::pair< uint16_t, uint16_t > TileKey
uint64_t PhysicalMask(const PixelIndexTarget &target)
void RefreshSelectionPixels(GraphicsEditorState &state)
constexpr int kSheetHeight
std::map< uint16_t, std::set< uint16_t > > PixelOffsets
absl::StatusOr< PixelOffsets > SelectionOffsets(const PixelIndexPlan &plan)
absl::Status CaptureSourceGuard(const GraphicsEditorState &state, PixelIndexPlan &plan)
bool IsRaw3bpp(absl::Span< const uint8_t > pixels)
absl::Status ValidatePermutation(const PixelIndexPermutation &permutation)
Editors are the view controllers for the application.
zelda3::GraphicsSheetStore * ActiveSheetStore(const GraphicsEditorState &state)
absl::StatusOr< PixelIndexPlan > PlanColorIdPermutation(const GraphicsEditorState &state, const std::vector< PixelIndexTarget > &targets, const ColorIdPermutation &permutation)
absl::StatusOr< PixelIndexPlan > PlanPixelIndexReplacement(const GraphicsEditorState &state, const std::vector< PixelIndexTarget > &targets, const std::map< uint16_t, std::vector< uint8_t > > &after_sheets, std::string description)
absl::Status ApplyColorIdPermutation(GraphicsEditorState &state, UndoManager &history, const std::vector< PixelIndexTarget > &targets, const ColorIdPermutation &permutation)
absl::Status ExecutePixelIndexPlan(GraphicsEditorState &state, const PixelIndexPlan &plan, PixelIndexExecution execution, const std::function< absl::Status(size_t)> &after_sheet_write)
absl::Status ApplyPixelIndexPermutation(GraphicsEditorState &state, UndoManager &history, const std::vector< PixelIndexTarget > &targets, const PixelIndexPermutation &permutation)
absl::Status ApplyPixelIndexPlan(GraphicsEditorState &state, UndoManager &history, const PixelIndexPlan &plan)
absl::StatusOr< PixelIndexPlan > PlanPixelIndexPermutation(const GraphicsEditorState &state, const std::vector< PixelIndexTarget > &targets, const PixelIndexPermutation &permutation)
void RefreshArenaSheet(GraphicsEditorState &state, uint16_t sheet)
absl::StatusOr< std::vector< PixelIndexTarget > > MakePixelIndexRectangleSelection(uint16_t sheet_id, int x, int y, int width, int height)
std::array< uint8_t, 8 > PixelIndexPermutation
absl::Status ValidateColorIdPermutationForBacking(const ColorIdPermutation &permutation, int backing_bpp)
uint64_t PhysicalPixelMask(uint64_t displayed_mask, bool hflip, bool vflip)
SheetPixelDiff MakeSheetPixelDiff(uint16_t sheet, absl::Span< const uint8_t > before, absl::Span< const uint8_t > after)
The pixels that differ between two 4096-byte sheets.
GfxSheetStorageKind GetGfxSheetStorageKind(uint16_t sheet_id)
#define RETURN_IF_ERROR(expr)
std::array< uint8_t, 16 > ids
zelda3::ResolvedGraphicsContext resolved
std::vector< GraphicsEditTarget > targets
std::vector< GraphicsSourceCell > pattern_cells
const zelda3::GameData * owner
std::vector< SheetPixelDiff > sheets
std::vector< size_t > source_targets
std::vector< PixelIndexTarget > targets
uint64_t source_context_revision
std::map< uint16_t, uint64_t > sheet_revisions
std::map< uint16_t, std::vector< uint8_t > > before_sheets
PixelIndexOperation operation
const zelda3::GameData * owner
std::optional< GraphicsEditContext > source