yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_canvas_proposal_preview.cc
Go to the documentation of this file.
1// Read-only proposal preview: real room renders side by side with a
2// DungeonProposalOverlay drawn on top. Reuses the connected-view composite
3// cache; never edits room data.
4
6#include "util/i18n/tr.h"
7
8#include <algorithm>
9#include <cmath>
10#include <string>
11#include <vector>
12
13#include "absl/strings/str_format.h"
17#include "imgui/imgui.h"
18
19namespace yaze::editor {
20namespace {
21
23
24constexpr float kProposalRoomGap = 24.0f;
25constexpr float kProposalLabelHeight = 22.0f;
26
27ImU32 ToImColor(uint32_t rgba, float alpha_scale = 1.0f) {
28 const int r = static_cast<int>((rgba >> 24) & 0xFF);
29 const int g = static_cast<int>((rgba >> 16) & 0xFF);
30 const int b = static_cast<int>((rgba >> 8) & 0xFF);
31 const int a = std::clamp(
32 static_cast<int>(static_cast<float>(rgba & 0xFF) * alpha_scale), 0, 255);
33 return IM_COL32(r, g, b, a);
34}
35
36void DrawOutlinedText(ImDrawList* draw_list, ImVec2 pos, ImU32 color,
37 const std::string& text) {
38 if (text.empty()) {
39 return;
40 }
41 const ImVec2 size = ImGui::CalcTextSize(text.c_str());
42 draw_list->AddRectFilled(ImVec2(pos.x - 2, pos.y - 1),
43 ImVec2(pos.x + size.x + 2, pos.y + size.y + 1),
44 IM_COL32(0, 0, 0, 170), 3.0f);
45 draw_list->AddText(pos, color, text.c_str());
46}
47
48void DrawArrow(ImDrawList* draw_list, ImVec2 a, ImVec2 b, ImU32 color,
49 float scale) {
50 const float dx = b.x - a.x;
51 const float dy = b.y - a.y;
52 const float len = std::sqrt(dx * dx + dy * dy);
53 if (len < 0.001f)
54 return;
55 const float ux = dx / len;
56 const float uy = dy / len;
57 const float head = std::max(5.0f, 6.0f * scale);
58 draw_list->AddLine(a, b, color, std::max(2.0f, 2.0f * scale));
59 draw_list->AddTriangleFilled(b,
60 ImVec2(b.x - ux * head - uy * head * 0.6f,
61 b.y - uy * head + ux * head * 0.6f),
62 ImVec2(b.x - ux * head + uy * head * 0.6f,
63 b.y - uy * head - ux * head * 0.6f),
64 color);
65}
66
67// UI-only selection, scoped to the preview window and proposal title. Loading
68// an older proposal with no steps continues to show its complete overlay.
70 if (overlay.steps.empty())
71 return -1;
72 ImGui::PushID(overlay.title.c_str());
73 const ImGuiID key = ImGui::GetID("ProposalSelectedStep");
74 ImGuiStorage* storage = ImGui::GetStateStorage();
75 int selected = storage->GetInt(key, -1);
76 if (selected < -1 || static_cast<size_t>(selected) >= overlay.steps.size())
77 selected = -1;
78 ImGui::SetNextItemWidth(260.0f);
79 const char* label =
80 selected < 0 ? tr("All steps") : overlay.steps[selected].label.c_str();
81 if (ImGui::BeginCombo(tr("Preview step"), label)) {
82 if (ImGui::Selectable(tr("All steps"), selected < 0))
83 selected = -1;
84 for (size_t i = 0; i < overlay.steps.size(); ++i) {
85 const auto& step = overlay.steps[i];
86 ImGui::PushID(step.id.c_str());
87 if (ImGui::Selectable(step.label.c_str(),
88 selected == static_cast<int>(i)))
89 selected = static_cast<int>(i);
90 ImGui::PopID();
91 }
92 ImGui::EndCombo();
93 }
94 ImGui::SameLine();
95 ImGui::BeginDisabled(selected <= 0);
96 if (ImGui::Button(tr("Previous")))
97 --selected;
98 ImGui::EndDisabled();
99 ImGui::SameLine();
100 ImGui::BeginDisabled(selected + 1 >= static_cast<int>(overlay.steps.size()));
101 if (ImGui::Button(tr("Next")))
102 ++selected;
103 ImGui::EndDisabled();
104 storage->SetInt(key, selected);
105 if (selected >= 0 && !overlay.steps[selected].detail.empty())
106 ImGui::TextWrapped("%s", overlay.steps[selected].detail.c_str());
107 ImGui::TextDisabled("%s", tr("Preview only. Actor markers and object "
108 "footprints do not change the room."));
109 ImGui::PopID();
110 return selected;
111}
112
113void DrawProposalShape(ImDrawList* draw_list, const ProposalShape& shape,
114 const ProposalLayer& layer, ImVec2 room_origin,
115 float scale) {
116 const ImU32 stroke = ToImColor(layer.color_rgba);
117 const ImU32 fill = ToImColor(layer.color_rgba, 0.30f);
118 const ImU32 text = IM_COL32(255, 255, 255, 255);
119 auto to_screen = [&](float x, float y) {
120 return ImVec2(room_origin.x + x * scale, room_origin.y + y * scale);
121 };
122 switch (shape.type) {
124 const auto r = ProposalFootprintToPixels(shape);
125 const ImVec2 p0 = to_screen(r.x0, r.y0);
126 const ImVec2 p1 = to_screen(r.x1, r.y1);
127 const ImVec2 center =
128 to_screen((r.x0 + r.x1) * 0.5f, (r.y0 + r.y1) * 0.5f);
129 // Deliberately abstract: do not imply loaded NPC art or collision bounds.
130 draw_list->AddRectFilled(p0, p1, fill, 3.0f * scale);
131 draw_list->AddRect(p0, p1, stroke, 3.0f * scale, 0, 2.0f);
132 ImVec2 direction(0, 0);
133 switch (shape.facing) {
135 direction.y = -1;
136 break;
138 direction.x = 1;
139 break;
141 direction.y = 1;
142 break;
144 direction.x = -1;
145 break;
146 }
147 DrawArrow(draw_list, center,
148 ImVec2(center.x + direction.x * 14 * scale,
149 center.y + direction.y * 14 * scale),
150 stroke, scale);
151 DrawOutlinedText(draw_list, ImVec2(p1.x + 5, p0.y), text, shape.label);
152 break;
153 }
155 const auto r = ProposalFootprintToPixels(shape);
156 const ImVec2 p0 = to_screen(r.x0, r.y0);
157 const ImVec2 p1 = to_screen(r.x1, r.y1);
158 draw_list->AddRectFilled(p0, p1, ToImColor(layer.color_rgba, 0.12f));
159 // Corner brackets keep the underlying room legible and distinguish a
160 // proposed footprint from an already rendered object.
161 const float tick =
162 std::min(8.0f * scale, std::min(p1.x - p0.x, p1.y - p0.y) * 0.4f);
163 for (int corner = 0; corner < 4; ++corner) {
164 const bool right = (corner & 1) != 0;
165 const bool bottom = (corner & 2) != 0;
166 const ImVec2 p(right ? p1.x : p0.x, bottom ? p1.y : p0.y);
167 draw_list->AddLine(p, ImVec2(p.x + (right ? -tick : tick), p.y), stroke,
168 2);
169 draw_list->AddLine(p, ImVec2(p.x, p.y + (bottom ? -tick : tick)),
170 stroke, 2);
171 }
172 const std::string label =
173 absl::StrFormat("$%03X%s%s", shape.object_id,
174 shape.label.empty() ? "" : " ", shape.label);
175 DrawOutlinedText(draw_list, ImVec2(p0.x + 3, p0.y + 2), text, label);
176 break;
177 }
179 const auto r = ProposalTileRectToPixels(shape.tiles[0], shape.tiles[1]);
180 const ImVec2 p0 = to_screen(r.x0, r.y0);
181 const ImVec2 p1 = to_screen(r.x1, r.y1);
182 draw_list->AddRectFilled(p0, p1, fill);
183 draw_list->AddRect(p0, p1, stroke, 0.0f, 2.0f);
184 DrawOutlinedText(draw_list, ImVec2(p0.x + 3, p0.y + 2), text,
185 shape.label);
186 break;
187 }
189 std::vector<ImVec2> points;
190 points.reserve(shape.tiles.size());
191 for (const auto& tile : shape.tiles) {
192 float x, y;
193 ProposalTileCenter(tile, &x, &y);
194 points.push_back(to_screen(x, y));
195 }
196 const float thickness = std::max(2.0f, 3.0f * scale);
197 draw_list->AddPolyline(points.data(), static_cast<int>(points.size()),
198 stroke, thickness);
199 // Arrowhead on the last segment shows travel direction.
200 const ImVec2 a = points[points.size() - 2];
201 const ImVec2 b = points.back();
202 DrawArrow(draw_list, a, b, stroke, scale);
203 if (!shape.label.empty()) {
204 const ImVec2 mid((a.x + b.x) * 0.5f, (a.y + b.y) * 0.5f);
205 DrawOutlinedText(draw_list, ImVec2(mid.x + 4, mid.y + 4), text,
206 shape.label);
207 }
208 break;
209 }
211 float x, y;
212 ProposalTileCenter(shape.tiles[0], &x, &y);
213 const ImVec2 c = to_screen(x, y);
214 const float radius = std::max(5.0f, 5.0f * scale);
215 draw_list->AddCircleFilled(c, radius, stroke);
216 draw_list->AddCircle(c, radius, IM_COL32(255, 255, 255, 230), 0, 1.5f);
217 DrawOutlinedText(draw_list, ImVec2(c.x + radius + 3, c.y - 7), text,
218 shape.label);
219 break;
220 }
222 float x, y;
223 ProposalTileCenter(shape.tiles[0], &x, &y);
224 const ImVec2 c = to_screen(x, y);
225 const float r = std::max(6.0f, 6.0f * scale);
226 const float thickness = std::max(2.0f, 2.5f * scale);
227 draw_list->AddCircle(c, r + 2.0f, stroke, 0, thickness * 0.6f);
228 draw_list->AddLine(ImVec2(c.x - r, c.y - r), ImVec2(c.x + r, c.y + r),
229 stroke, thickness);
230 draw_list->AddLine(ImVec2(c.x - r, c.y + r), ImVec2(c.x + r, c.y - r),
231 stroke, thickness);
232 DrawOutlinedText(draw_list, ImVec2(c.x + r + 4, c.y - 7), text,
233 shape.label);
234 break;
235 }
236 }
237}
238
239} // namespace
240
242 const DungeonProposalOverlay& overlay, float scale) const {
243 const int count = static_cast<int>(overlay.rooms.size());
244 if (count == 0) {
245 return ImVec2(0, 0);
246 }
247 const float width =
248 ProposalRoomOriginX(count - 1, kProposalRoomGap) + kDungeonRoomPixelSize;
249 return ImVec2(width * scale,
250 kProposalLabelHeight + kDungeonRoomPixelSize * scale);
251}
252
254 const DungeonProposalOverlay& overlay,
255 const std::vector<char>& layer_visible, bool show_overlay, float scale) {
256 if (!rom_ || !rom_->is_loaded() || !rooms_) {
257 ImGui::TextDisabled("%s", tr("Load a ROM to render the proposal rooms."));
258 return;
259 }
260 if (overlay.rooms.empty()) {
261 return;
262 }
263 const int selected_step = DrawProposalStepSelector(overlay);
264
265 // Prepare every room composite first, then upload textures once so a newly
266 // opened preview draws in the same frame.
267 std::vector<gfx::Bitmap*> bitmaps;
268 bitmaps.reserve(overlay.rooms.size());
269 for (const auto& room : overlay.rooms) {
270 // Rooms the user has not opened are materialized but not loaded; load
271 // them from the ROM the same way the connected-room view does, or the
272 // composite shows placeholder data.
274 bitmaps.push_back(PrepareConnectedRoomCompositeBitmap(room.room_id));
275 }
277
278 const ImVec2 origin = ImGui::GetCursorScreenPos();
279 const ImVec2 content = GetProposalPreviewContentSize(overlay, scale);
280 ImGui::InvisibleButton("##DungeonProposalPreviewSurface", content);
281 const bool surface_hovered =
282 ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal);
283 ImDrawList* draw_list = ImGui::GetWindowDrawList();
284 const ImVec2 mouse = ImGui::GetMousePos();
285 const float room_size = kDungeonRoomPixelSize * scale;
286
287 std::vector<std::string> hover_lines;
288 for (size_t i = 0; i < overlay.rooms.size(); ++i) {
289 const auto& room = overlay.rooms[i];
290 const float x =
291 origin.x +
292 ProposalRoomOriginX(static_cast<int>(i), kProposalRoomGap) * scale;
293 const ImVec2 room_min(x, origin.y + kProposalLabelHeight);
294 const ImVec2 room_max(room_min.x + room_size, room_min.y + room_size);
295
296 const std::string title =
297 room.label.empty()
298 ? absl::StrFormat("Room $%02X", room.room_id)
299 : absl::StrFormat("Room $%02X - %s", room.room_id, room.label);
300 draw_list->AddText(ImVec2(x, origin.y + 2),
301 ImGui::GetColorU32(ImGuiCol_Text), title.c_str());
302
303 if (bitmaps[i] && bitmaps[i]->texture()) {
304 draw_list->AddImage((ImTextureID)(intptr_t)bitmaps[i]->texture(),
305 room_min, room_max, ImVec2(0, 0), ImVec2(1, 1),
306 ImGui::GetColorU32(ImVec4(1, 1, 1, 1)));
307 } else {
308 draw_list->AddRectFilled(room_min, room_max, IM_COL32(40, 40, 48, 255));
309 draw_list->AddText(ImVec2(room_min.x + 8, room_min.y + 8),
310 IM_COL32(200, 200, 200, 255), tr("Rendering room..."));
311 }
312 draw_list->AddRect(room_min, room_max, IM_COL32(255, 255, 255, 90));
313
314 const bool mouse_in_room = surface_hovered && mouse.x >= room_min.x &&
315 mouse.x < room_max.x && mouse.y >= room_min.y &&
316 mouse.y < room_max.y;
317 const float local_x = (mouse.x - room_min.x) / scale;
318 const float local_y = (mouse.y - room_min.y) / scale;
319
320 if (!show_overlay) {
321 continue;
322 }
323 draw_list->PushClipRect(room_min, room_max, true);
324 for (size_t li = 0; li < overlay.layers.size(); ++li) {
325 if (li < layer_visible.size() && !layer_visible[li]) {
326 continue;
327 }
328 const auto& layer = overlay.layers[li];
329 if (!ProposalLayerInStep(overlay, layer.id, selected_step))
330 continue;
331 for (const auto& shape : layer.shapes) {
332 if (shape.room_id != room.room_id) {
333 continue;
334 }
335 DrawProposalShape(draw_list, shape, layer, room_min, scale);
336 if (mouse_in_room && ProposalShapeHitTest(shape, local_x, local_y,
337 6.0f / scale + 2.0f)) {
338 std::string line = layer.label;
339 if (!shape.label.empty()) {
340 line += ": " + shape.label;
341 }
342 if (!shape.detail.empty()) {
343 line += " - " + shape.detail;
344 }
345 if (shape.type == ProposalShapeType::kActor) {
346 const auto r = ProposalFootprintToPixels(shape);
347 line +=
348 absl::StrFormat(" (actor anchor %d,%d px; preview only)",
349 static_cast<int>(r.x0), static_cast<int>(r.y0));
350 } else if (shape.type == ProposalShapeType::kObject) {
351 line += absl::StrFormat(
352 " ($%03X, size %d, list %d; suggested footprint only)",
353 shape.object_id, shape.object_size, shape.object_list);
354 }
355 hover_lines.push_back(std::move(line));
356 }
357 }
358 }
359 draw_list->PopClipRect();
360
361 if (mouse_in_room) {
362 hover_lines.insert(
363 hover_lines.begin(),
364 absl::StrFormat("Room $%02X tile (%d, %d)", room.room_id,
365 static_cast<int>(local_x) / kProposalTilePixels,
366 static_cast<int>(local_y) / kProposalTilePixels));
367 }
368 }
369
370 if (!hover_lines.empty()) {
371 const float wrap_width = std::max(
372 1.0f,
373 std::min(360.0f, ImGui::GetWindowViewport()->WorkSize.x -
374 2.0f * ImGui::GetStyle().WindowPadding.x - 16.0f));
375 ImGui::BeginTooltip();
376 ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + wrap_width);
377 for (const auto& line : hover_lines) {
378 ImGui::TextUnformatted(line.c_str());
379 }
380 ImGui::PopTextWrapPos();
381 ImGui::EndTooltip();
382 }
383}
384
385} // namespace yaze::editor
bool is_loaded() const
Definition rom.h:199
gfx::Bitmap * PrepareConnectedRoomCompositeBitmap(int room_id)
zelda3::Room * EnsureRoomLoadedForConnectedView(int room_id)
ImVec2 GetProposalPreviewContentSize(const DungeonProposalOverlay &overlay, float scale) const
void DrawProposalPreview(const DungeonProposalOverlay &overlay, const std::vector< char > &layer_visible, bool show_overlay, float scale)
void ProcessTextureQueue(IRenderer *renderer)
Definition arena.cc:211
static Arena & Get()
Definition arena.cc:24
void DrawOutlinedText(ImDrawList *draw_list, ImVec2 pos, ImU32 color, const std::string &text)
void DrawProposalShape(ImDrawList *draw_list, const ProposalShape &shape, const ProposalLayer &layer, ImVec2 room_origin, float scale)
void DrawArrow(ImDrawList *draw_list, ImVec2 a, ImVec2 b, ImU32 color, float scale)
constexpr float kDungeonRoomPixelSize
Editors are the view controllers for the application.
float ProposalRoomOriginX(int index, float gap_pixels)
ProposalPixelRect ProposalFootprintToPixels(const ProposalShape &shape)
ProposalPixelRect ProposalTileRectToPixels(ProposalTile a, ProposalTile b)
void ProposalTileCenter(ProposalTile tile, float *x, float *y)
constexpr int kProposalTilePixels
bool ProposalLayerInStep(const DungeonProposalOverlay &overlay, std::string_view layer_id, int step)
bool ProposalShapeHitTest(const ProposalShape &shape, float px, float py, float tolerance)
std::vector< ProposalTile > tiles