yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
project_graphics_source_ui.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <filesystem>
5#include <memory>
6#include <string>
7
8#include "absl/strings/str_format.h"
9#include "core/project.h"
10#include "imgui/imgui.h"
11
12namespace yaze::editor {
13
15 auto* owner = TerrainProject();
16 const auto* map = overworld_.overworld_map(current_map_);
17 if (!owner || owner->git_repository.empty()) {
18 return absl::FailedPreconditionError(
19 "Select a native map in a project with a bound graphics source");
20 }
21 const std::string root = owner->GetAbsolutePath(owner->git_repository);
22 const std::string catalog = ProjectGraphicsPath();
23 // A combined import action retains this document for main Undo/Redo even
24 // after publication has made its byte baseline clean.
25 if (graphics_source_ &&
28 if (graphics_source_project_ != owner ||
29 graphics_source_->source_root() != root ||
30 graphics_source_->portable_catalog() != catalog) {
31 return absl::FailedPreconditionError(
32 "The graphics draft belongs to another project. Return to that "
33 "project before editing or publishing its graphics.");
34 }
35 return absl::OkStatus();
36 }
38 return absl::FailedPreconditionError(
39 "Select a native map before opening its project graphics source");
40 }
41 auto next = std::make_shared<ProjectGraphicsSource>();
42 const auto status = next->Load(root, catalog, map->parent());
43 if (!status.ok())
44 return status;
45 graphics_source_ = std::move(next);
49 return absl::OkStatus();
50}
51
53 constexpr const char* kPopup = "Edit Project Graphics";
54 if (graphics_source_open_ && !ImGui::IsPopupOpen(kPopup))
55 ImGui::OpenPopup(kPopup);
56 if (!ImGui::IsPopupOpen(kPopup))
57 return;
58 const auto* viewport = ImGui::GetMainViewport();
59 ImGui::SetNextWindowSize(
60 ImVec2(std::min(720.0f, viewport->WorkSize.x * 0.9f),
61 std::min(740.0f, viewport->WorkSize.y * 0.9f)),
62 ImGuiCond_Appearing);
63 if (!ImGui::BeginPopupModal(kPopup))
64 return;
65 // Session teardown can clear the source while ImGui retains the modal's
66 // open flag for this frame.
68 ImGui::CloseCurrentPopup();
69 ImGui::EndPopup();
70 return;
71 }
72 auto& source = *graphics_source_;
73 const auto* owner = TerrainProject();
74 const bool bound =
75 graphics_source_project_ == owner &&
76 source.portable_catalog() == ProjectGraphicsPath() && owner &&
77 source.source_root() == owner->GetAbsolutePath(owner->git_repository);
78 const auto show_status = [this](const absl::Status& status) {
81 };
82 const auto refresh_after_edit = [this,
83 &show_status](const absl::Status& status) {
84 show_status(status.ok() ? RefreshPendingProjectGraphics() : status);
85 };
86 if (!bound) {
87 ImGui::TextWrapped(
88 "This draft belongs to another project. Return to its project to "
89 "edit or publish its graphics.");
90 }
92 ImGui::TextWrapped("%s", graphics_source_status_.ToString().c_str());
93 if (!graphics_source_message_.empty())
94 ImGui::TextWrapped("%s", graphics_source_message_.c_str());
95 ImGui::TextWrapped(
96 "Publish graphics first, then Save ROM for palettes, definitions and "
97 "map placement. Build Project includes source assets in the game.");
98
99 if (ImGui::BeginChild("Resource",
100 ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), true)) {
101 ImGui::BeginDisabled(!bound);
102 const auto* resource = source.selected_resource();
103 ImGui::BeginDisabled(graphics_import_applied_);
104 if (resource &&
105 ImGui::BeginCombo("Resource", resource->source_path.c_str())) {
106 for (const auto& choice : source.resources()) {
107 ImGui::PushID(choice.id.c_str());
108 const std::string label =
109 choice.source_path + (choice.dirty() ? " *" : "");
110 if (ImGui::Selectable(label.c_str(), choice.id == resource->id)) {
111 show_status(source.SelectResource(choice.id));
113 }
114 ImGui::PopID();
115 }
116 ImGui::EndCombo();
117 }
118 ImGui::EndDisabled();
119 resource = source.selected_resource();
120 if (resource) {
121 ImGui::TextWrapped("Content ID: %s", resource->id.c_str());
122 std::string consumers;
123 for (int parent : resource->parent_maps)
124 consumers += absl::StrFormat("$%02X ", parent);
125 ImGui::TextWrapped("Affected parent maps: %s", consumers.c_str());
126 if (ImGui::TreeNode("Source slices")) {
127 for (const auto& slice : resource->slices) {
128 ImGui::TextWrapped(
129 "%s: resource tiles %zu..%zu -> BG characters $%03X..$%03X",
130 slice.profile_id.c_str(), slice.first_resource_tile(),
131 slice.first_resource_tile() + slice.byte_count / 32 - 1,
132 slice.first_character(),
133 slice.first_character() +
134 static_cast<int>(slice.byte_count / 32) - 1);
135 }
136 ImGui::TreePop();
137 }
139 ImGui::SliderInt("Preview palette row", &graphics_source_palette_, 0, 7);
140 const auto* map = overworld_.overworld_map(source.parent_map());
141 const auto color = [&](uint8_t index) {
142 const size_t at = graphics_source_palette_ * 16 + index;
143 if (map && at < map->current_palette().size())
144 return gfx::SnesTo8bppColor(map->current_palette()[at].snes());
145 const float value = index / 15.0f;
146 return ImVec4(value, value, value, 1);
147 };
148 for (int index = 0; index < 16; ++index) {
149 ImGui::PushID(index);
150 if (index % 8)
151 ImGui::SameLine(0, 2);
152 if (ImGui::ColorButton("Index", color(index),
153 ImGuiColorEditFlags_NoTooltip, ImVec2(24, 24)))
155 if (ImGui::IsItemHovered())
156 ImGui::SetTooltip("Pixel index %d%s", index,
157 index == 0 ? " (transparent)" : "");
158 if (index == graphics_source_index_)
159 ImGui::GetWindowDrawList()->AddRect(
160 ImGui::GetItemRectMin(), ImGui::GetItemRectMax(),
161 IM_COL32(255, 255, 255, 255), 0.0f, 2.0f);
162 ImGui::PopID();
163 }
164 ImGui::Text("Paint index: %d", graphics_source_index_);
166 std::clamp(graphics_source_tile_, 0,
167 static_cast<int>(resource->tiles.size()) - 1);
168 ImGui::SliderInt("Resource tile", &graphics_source_tile_, 0,
169 static_cast<int>(resource->tiles.size()) - 1);
170 const auto draw_tile = [&](int tile, ImVec2 origin, float pixel_size) {
171 auto* list = ImGui::GetWindowDrawList();
172 for (int y = 0; y < 8; ++y) {
173 for (int x = 0; x < 8; ++x) {
174 const auto index = source.GetPixel(tile, x, y);
175 if (!index.ok())
176 continue;
177 const ImVec2 pos(origin.x + x * pixel_size,
178 origin.y + y * pixel_size);
179 list->AddRectFilled(pos,
180 ImVec2(pos.x + pixel_size, pos.y + pixel_size),
181 ImGui::ColorConvertFloat4ToU32(color(*index)));
182 }
183 }
184 };
185 if (ImGui::BeginChild("Tiles", ImVec2(0, 136), true)) {
186 const int columns = std::max(
187 1, static_cast<int>(ImGui::GetContentRegionAvail().x / 28));
188 ImGuiListClipper clipper;
189 clipper.Begin(
190 (static_cast<int>(resource->tiles.size()) + columns - 1) / columns,
191 24 + ImGui::GetStyle().ItemSpacing.y);
192 while (clipper.Step()) {
193 for (int row = clipper.DisplayStart; row < clipper.DisplayEnd;
194 ++row) {
195 for (int column = 0; column < columns; ++column) {
196 const int tile = row * columns + column;
197 if (tile >= static_cast<int>(resource->tiles.size()))
198 break;
199 if (column)
200 ImGui::SameLine(0, 4);
201 ImGui::PushID(tile);
202 const auto origin = ImGui::GetCursorScreenPos();
203 if (ImGui::InvisibleButton("Tile", ImVec2(24, 24)))
205 draw_tile(tile, origin, 3);
206 if (tile == graphics_source_tile_)
207 ImGui::GetWindowDrawList()->AddRect(
208 origin, ImVec2(origin.x + 24, origin.y + 24),
209 IM_COL32(255, 255, 255, 255), 0.0f, 2.0f);
210 ImGui::PopID();
211 }
212 }
213 }
214 }
215 ImGui::EndChild();
216 const auto origin = ImGui::GetCursorScreenPos();
217 ImGui::BeginDisabled(graphics_import_applied_);
218 ImGui::InvisibleButton("Paint resource tile", ImVec2(160, 160));
219 draw_tile(graphics_source_tile_, origin, 20);
220 if (ImGui::IsItemHovered() &&
221 ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
222 const auto mouse = ImGui::GetIO().MousePos;
223 refresh_after_edit(source.SetPixel(
224 graphics_source_tile_, static_cast<int>((mouse.x - origin.x) / 20),
225 static_cast<int>((mouse.y - origin.y) / 20),
227 }
228 ImGui::BeginDisabled(!source.can_undo());
229 if (ImGui::Button("Undo Pixel"))
230 refresh_after_edit(source.Undo());
231 ImGui::EndDisabled();
232 ImGui::SameLine();
233 ImGui::BeginDisabled(!source.can_redo());
234 if (ImGui::Button("Redo Pixel"))
235 refresh_after_edit(source.Redo());
236 ImGui::EndDisabled();
237 ImGui::EndDisabled();
239 ImGui::TextWrapped(
240 "Use the main Undo and Redo commands to revert or restore the "
241 "combined graphics import, palette, definitions and placement.");
242 ImGui::BeginDisabled(!source.dirty() || !source.backend_available());
243 if (ImGui::Button("Review Source Publish")) {
244 const auto preview = source.Preview();
245 show_status(preview.status());
246 }
247 ImGui::EndDisabled();
248 if (!source.backend_available()) {
249#if defined(_WIN32) || defined(__EMSCRIPTEN__)
250 ImGui::TextWrapped(
251 "Source publication currently requires a native macOS or Linux "
252 "build with Python 3.");
253#else
254 ImGui::TextWrapped(
255 "The bound source needs Scripts/Graphics/edit_graphics_source.py "
256 "before this draft can be published.");
257#endif
258 }
259 if (source.has_preview()) {
260 const auto& plan = source.preview_json();
261 ImGui::TextWrapped(
262 "New content ID: %s",
263 plan.value("after_resource_id", std::string()).c_str());
264 if (ImGui::TreeNode("Review files and fingerprints")) {
265 ImGui::TextWrapped("%s", plan.dump(2).c_str());
266 ImGui::TreePop();
267 }
268 if (ImGui::Button("Publish Reviewed Source")) {
269 const auto published = source.Publish();
270 show_status(published.status());
271 if (published.ok()) {
272 // Publication is durable before render reload. A reload error
273 // must not be reported as a source transaction rollback.
274 const auto reload = ReloadProjectGraphics();
275 if (!reload.ok()) {
276 show_status(absl::FailedPreconditionError(
277 "Source published; graphics reload needs attention: " +
278 std::string(reload.message())));
279 } else {
281 "Graphics published and reloaded. Save ROM to persist "
282 "palettes, definitions and placement, then Build Project "
283 "to include the source assets in the game.";
284 }
285 }
286 }
287 }
288 }
289 ImGui::EndDisabled();
290 }
291 ImGui::EndChild();
292 if (ImGui::Button("Close (keep draft)")) {
293 graphics_source_open_ = false;
294 ImGui::CloseCurrentPopup();
295 }
296 ImGui::EndPopup();
297}
298
299} // namespace yaze::editor
bool IsProjectTerrainMap(int map_id) const
std::shared_ptr< ProjectGraphicsSource > graphics_source_
const project::YazeProject * graphics_source_project_
project::YazeProject * TerrainProject() const
auto overworld_map(int i) const
Definition overworld.h:770
Editors are the view controllers for the application.
ImVec4 SnesTo8bppColor(uint16_t color_value)
Convert SNES 15-bit color directly to ImVec4 (0.0-1.0)
Definition snes_color.h:80
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:2675