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 DrawProposalShape(ImDrawList* draw_list, const ProposalShape& shape,
49 const ProposalLayer& layer, ImVec2 room_origin,
50 float scale) {
51 const ImU32 stroke = ToImColor(layer.color_rgba);
52 const ImU32 fill = ToImColor(layer.color_rgba, 0.30f);
53 const ImU32 text = IM_COL32(255, 255, 255, 255);
54 auto to_screen = [&](float x, float y) {
55 return ImVec2(room_origin.x + x * scale, room_origin.y + y * scale);
56 };
57 switch (shape.type) {
59 const auto r = ProposalTileRectToPixels(shape.tiles[0], shape.tiles[1]);
60 const ImVec2 p0 = to_screen(r.x0, r.y0);
61 const ImVec2 p1 = to_screen(r.x1, r.y1);
62 draw_list->AddRectFilled(p0, p1, fill);
63 draw_list->AddRect(p0, p1, stroke, 0.0f, 0, 2.0f);
64 DrawOutlinedText(draw_list, ImVec2(p0.x + 3, p0.y + 2), text,
65 shape.label);
66 break;
67 }
69 std::vector<ImVec2> points;
70 points.reserve(shape.tiles.size());
71 for (const auto& tile : shape.tiles) {
72 float x, y;
73 ProposalTileCenter(tile, &x, &y);
74 points.push_back(to_screen(x, y));
75 }
76 const float thickness = std::max(2.0f, 3.0f * scale);
77 draw_list->AddPolyline(points.data(), static_cast<int>(points.size()),
78 stroke, ImDrawFlags_None, thickness);
79 // Arrowhead on the last segment shows travel direction.
80 const ImVec2 a = points[points.size() - 2];
81 const ImVec2 b = points.back();
82 const float dx = b.x - a.x;
83 const float dy = b.y - a.y;
84 const float len = std::max(0.001f, std::sqrt(dx * dx + dy * dy));
85 const float ux = dx / len;
86 const float uy = dy / len;
87 const float head = std::max(7.0f, 7.0f * scale);
88 draw_list->AddTriangleFilled(b,
89 ImVec2(b.x - ux * head - uy * head * 0.6f,
90 b.y - uy * head + ux * head * 0.6f),
91 ImVec2(b.x - ux * head + uy * head * 0.6f,
92 b.y - uy * head - ux * head * 0.6f),
93 stroke);
94 if (!shape.label.empty()) {
95 const ImVec2 mid((a.x + b.x) * 0.5f, (a.y + b.y) * 0.5f);
96 DrawOutlinedText(draw_list, ImVec2(mid.x + 4, mid.y + 4), text,
97 shape.label);
98 }
99 break;
100 }
102 float x, y;
103 ProposalTileCenter(shape.tiles[0], &x, &y);
104 const ImVec2 c = to_screen(x, y);
105 const float radius = std::max(5.0f, 5.0f * scale);
106 draw_list->AddCircleFilled(c, radius, stroke);
107 draw_list->AddCircle(c, radius, IM_COL32(255, 255, 255, 230), 0, 1.5f);
108 DrawOutlinedText(draw_list, ImVec2(c.x + radius + 3, c.y - 7), text,
109 shape.label);
110 break;
111 }
113 float x, y;
114 ProposalTileCenter(shape.tiles[0], &x, &y);
115 const ImVec2 c = to_screen(x, y);
116 const float r = std::max(6.0f, 6.0f * scale);
117 const float thickness = std::max(2.0f, 2.5f * scale);
118 draw_list->AddCircle(c, r + 2.0f, stroke, 0, thickness * 0.6f);
119 draw_list->AddLine(ImVec2(c.x - r, c.y - r), ImVec2(c.x + r, c.y + r),
120 stroke, thickness);
121 draw_list->AddLine(ImVec2(c.x - r, c.y + r), ImVec2(c.x + r, c.y - r),
122 stroke, thickness);
123 DrawOutlinedText(draw_list, ImVec2(c.x + r + 4, c.y - 7), text,
124 shape.label);
125 break;
126 }
127 }
128}
129
130} // namespace
131
133 const DungeonProposalOverlay& overlay, float scale) const {
134 const int count = static_cast<int>(overlay.rooms.size());
135 if (count == 0) {
136 return ImVec2(0, 0);
137 }
138 const float width =
139 ProposalRoomOriginX(count - 1, kProposalRoomGap) + kDungeonRoomPixelSize;
140 return ImVec2(width * scale,
141 kProposalLabelHeight + kDungeonRoomPixelSize * scale);
142}
143
145 const DungeonProposalOverlay& overlay,
146 const std::vector<char>& layer_visible, bool show_overlay, float scale) {
147 if (!rom_ || !rom_->is_loaded() || !rooms_) {
148 ImGui::TextDisabled("%s", tr("Load a ROM to render the proposal rooms."));
149 return;
150 }
151 if (overlay.rooms.empty()) {
152 return;
153 }
154
155 // Prepare every room composite first, then upload textures once so a newly
156 // opened preview draws in the same frame.
157 std::vector<gfx::Bitmap*> bitmaps;
158 bitmaps.reserve(overlay.rooms.size());
159 for (const auto& room : overlay.rooms) {
160 // Rooms the user has not opened are materialized but not loaded; load
161 // them from the ROM the same way the connected-room view does, or the
162 // composite shows placeholder data.
164 bitmaps.push_back(PrepareConnectedRoomCompositeBitmap(room.room_id));
165 }
167
168 const ImVec2 origin = ImGui::GetCursorScreenPos();
169 const ImVec2 content = GetProposalPreviewContentSize(overlay, scale);
170 ImGui::InvisibleButton("##DungeonProposalPreviewSurface", content);
171 const bool surface_hovered = ImGui::IsItemHovered();
172 ImDrawList* draw_list = ImGui::GetWindowDrawList();
173 const ImVec2 mouse = ImGui::GetMousePos();
174 const float room_size = kDungeonRoomPixelSize * scale;
175
176 std::vector<std::string> hover_lines;
177 for (size_t i = 0; i < overlay.rooms.size(); ++i) {
178 const auto& room = overlay.rooms[i];
179 const float x =
180 origin.x +
181 ProposalRoomOriginX(static_cast<int>(i), kProposalRoomGap) * scale;
182 const ImVec2 room_min(x, origin.y + kProposalLabelHeight);
183 const ImVec2 room_max(room_min.x + room_size, room_min.y + room_size);
184
185 const std::string title =
186 room.label.empty()
187 ? absl::StrFormat("Room $%02X", room.room_id)
188 : absl::StrFormat("Room $%02X - %s", room.room_id, room.label);
189 draw_list->AddText(ImVec2(x, origin.y + 2),
190 ImGui::GetColorU32(ImGuiCol_Text), title.c_str());
191
192 if (bitmaps[i] && bitmaps[i]->texture()) {
193 draw_list->AddImage((ImTextureID)(intptr_t)bitmaps[i]->texture(),
194 room_min, room_max, ImVec2(0, 0), ImVec2(1, 1),
195 ImGui::GetColorU32(ImVec4(1, 1, 1, 1)));
196 } else {
197 draw_list->AddRectFilled(room_min, room_max, IM_COL32(40, 40, 48, 255));
198 draw_list->AddText(ImVec2(room_min.x + 8, room_min.y + 8),
199 IM_COL32(200, 200, 200, 255), tr("Rendering room..."));
200 }
201 draw_list->AddRect(room_min, room_max, IM_COL32(255, 255, 255, 90));
202
203 const bool mouse_in_room = surface_hovered && mouse.x >= room_min.x &&
204 mouse.x < room_max.x && mouse.y >= room_min.y &&
205 mouse.y < room_max.y;
206 const float local_x = (mouse.x - room_min.x) / scale;
207 const float local_y = (mouse.y - room_min.y) / scale;
208
209 if (!show_overlay) {
210 continue;
211 }
212 draw_list->PushClipRect(room_min, room_max, true);
213 for (size_t li = 0; li < overlay.layers.size(); ++li) {
214 if (li < layer_visible.size() && !layer_visible[li]) {
215 continue;
216 }
217 const auto& layer = overlay.layers[li];
218 for (const auto& shape : layer.shapes) {
219 if (shape.room_id != room.room_id) {
220 continue;
221 }
222 DrawProposalShape(draw_list, shape, layer, room_min, scale);
223 if (mouse_in_room && ProposalShapeHitTest(shape, local_x, local_y,
224 6.0f / scale + 2.0f)) {
225 std::string line = layer.label;
226 if (!shape.label.empty()) {
227 line += ": " + shape.label;
228 }
229 if (!shape.detail.empty()) {
230 line += " - " + shape.detail;
231 }
232 hover_lines.push_back(std::move(line));
233 }
234 }
235 }
236 draw_list->PopClipRect();
237
238 if (mouse_in_room) {
239 hover_lines.insert(
240 hover_lines.begin(),
241 absl::StrFormat("Room $%02X tile (%d, %d)", room.room_id,
242 static_cast<int>(local_x) / kProposalTilePixels,
243 static_cast<int>(local_y) / kProposalTilePixels));
244 }
245 }
246
247 if (!hover_lines.empty()) {
248 ImGui::BeginTooltip();
249 for (const auto& line : hover_lines) {
250 ImGui::TextUnformatted(line.c_str());
251 }
252 ImGui::EndTooltip();
253 }
254}
255
256} // namespace yaze::editor
bool is_loaded() const
Definition rom.h:187
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)
constexpr float kDungeonRoomPixelSize
Editors are the view controllers for the application.
float ProposalRoomOriginX(int index, float gap_pixels)
ProposalPixelRect ProposalTileRectToPixels(ProposalTile a, ProposalTile b)
void ProposalTileCenter(ProposalTile tile, float *x, float *y)
constexpr int kProposalTilePixels
bool ProposalShapeHitTest(const ProposalShape &shape, float px, float py, float tolerance)
std::vector< ProposalTile > tiles