yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
polyhedral_editor_view.cc
Go to the documentation of this file.
2#include "util/i18n/tr.h"
3
4#include <algorithm>
5#include <cmath>
6#include <string>
7#include <vector>
8
9#include "absl/status/status.h"
10#include "absl/status/statusor.h"
11#include "absl/strings/str_format.h"
12#include "app/gui/core/icons.h"
15#include "imgui/imgui.h"
16#include "implot.h"
17#include "rom/snes.h"
18#include "util/macro.h"
19
20namespace yaze {
21namespace editor {
22
23namespace {
24
25constexpr uint32_t kPolyTableSnes = 0x09FF8C;
26constexpr uint32_t kPolyEntrySize = 6;
27constexpr uint32_t kPolyRegionSize = 0x74; // 116 bytes, $09:FF8C-$09:FFFF
28constexpr uint8_t kPolyBank = 0x09;
29
30constexpr ImVec4 kVertexColor(0.3f, 0.8f, 1.0f, 1.0f);
31constexpr ImVec4 kSelectedVertexColor(1.0f, 0.75f, 0.2f, 1.0f);
32
33template <typename T>
34T Clamp(T value, T min_v, T max_v) {
35 return std::max(min_v, std::min(max_v, value));
36}
37
38std::string ShapeNameForIndex(int index) {
39 switch (index) {
40 case 0:
41 return "Crystal";
42 case 1:
43 return "Triforce";
44 default:
45 return absl::StrFormat("Shape %d", index);
46 }
47}
48
49uint32_t ToPc(uint16_t bank_offset) {
50 return SnesToPc((kPolyBank << 16) | bank_offset);
51}
52
53} // namespace
54
56 return SnesToPc(kPolyTableSnes);
57}
58
61 dirty_ = false;
62 return absl::OkStatus();
63}
64
66 if (!rom_ || !rom_->is_loaded()) {
67 return absl::FailedPreconditionError("ROM is not loaded");
68 }
69
70 // Read the whole 3D object region to keep parsing bounds explicit.
71 ASSIGN_OR_RETURN(auto region,
72 rom_->ReadByteVector(TablePc(), kPolyRegionSize));
73
74 shapes_.clear();
75
76 // Two entries live in the table (crystal, triforce). Stop if we run out of
77 // room rather than reading garbage.
78 for (int i = 0; i < 2; ++i) {
79 size_t base = i * kPolyEntrySize;
80 if (base + kPolyEntrySize > region.size()) {
81 break;
82 }
83
84 PolyShape shape;
85 shape.name = ShapeNameForIndex(i);
86 shape.vertex_count = region[base];
87 shape.face_count = region[base + 1];
88 shape.vertex_ptr =
89 static_cast<uint16_t>(region[base + 2] | (region[base + 3] << 8));
90 shape.face_ptr =
91 static_cast<uint16_t>(region[base + 4] | (region[base + 5] << 8));
92
93 // Vertices (signed bytes, XYZ triples)
94 const uint32_t vertex_pc = ToPc(shape.vertex_ptr);
95 const size_t vertex_bytes = static_cast<size_t>(shape.vertex_count) * 3;
96 ASSIGN_OR_RETURN(auto vertex_blob,
97 rom_->ReadByteVector(vertex_pc, vertex_bytes));
98
99 shape.vertices.reserve(shape.vertex_count);
100 for (size_t idx = 0; idx + 2 < vertex_blob.size(); idx += 3) {
101 PolyVertex v;
102 v.x = static_cast<int8_t>(vertex_blob[idx]);
103 v.y = static_cast<int8_t>(vertex_blob[idx + 1]);
104 v.z = static_cast<int8_t>(vertex_blob[idx + 2]);
105 shape.vertices.push_back(v);
106 }
107
108 // Faces (count byte, indices[count], shade byte)
109 uint32_t face_pc = ToPc(shape.face_ptr);
110 shape.faces.reserve(shape.face_count);
111 for (int f = 0; f < shape.face_count; ++f) {
112 ASSIGN_OR_RETURN(auto count_byte, rom_->ReadByte(face_pc++));
113 PolyFace face;
114 face.vertex_indices.reserve(count_byte);
115
116 for (int j = 0; j < count_byte; ++j) {
117 ASSIGN_OR_RETURN(auto idx_byte, rom_->ReadByte(face_pc++));
118 face.vertex_indices.push_back(idx_byte);
119 }
120
121 ASSIGN_OR_RETURN(auto shade_byte, rom_->ReadByte(face_pc++));
122 face.shade = shade_byte;
123 shape.faces.push_back(std::move(face));
124 }
125
126 shapes_.push_back(std::move(shape));
127 }
128
129 selected_shape_ = 0;
131 data_loaded_ = true;
132 return absl::OkStatus();
133}
134
136 for (auto& shape : shapes_) {
137 shape.vertex_count = static_cast<uint8_t>(shape.vertices.size());
138 shape.face_count = static_cast<uint8_t>(shape.faces.size());
140 }
141 dirty_ = false;
142 return absl::OkStatus();
143}
144
145absl::Status PolyhedralEditorView::WriteShape(const PolyShape& shape) {
146 // Vertices
147 std::vector<uint8_t> vertex_blob;
148 vertex_blob.reserve(shape.vertices.size() * 3);
149 for (const auto& v : shape.vertices) {
150 vertex_blob.push_back(static_cast<uint8_t>(static_cast<int8_t>(v.x)));
151 vertex_blob.push_back(static_cast<uint8_t>(static_cast<int8_t>(v.y)));
152 vertex_blob.push_back(static_cast<uint8_t>(static_cast<int8_t>(v.z)));
153 }
154
156 rom_->WriteVector(ToPc(shape.vertex_ptr), std::move(vertex_blob)));
157
158 // Faces
159 std::vector<uint8_t> face_blob;
160 for (const auto& face : shape.faces) {
161 face_blob.push_back(static_cast<uint8_t>(face.vertex_indices.size()));
162 for (auto idx : face.vertex_indices) {
163 face_blob.push_back(idx);
164 }
165 face_blob.push_back(face.shade);
166 }
167
168 return rom_->WriteVector(ToPc(shape.face_ptr), std::move(face_blob));
169}
170
171void PolyhedralEditorView::Draw(bool* p_open) {
172 // WindowContent interface - delegate to existing Update() logic
173 if (!rom_ || !rom_->is_loaded()) {
174 ImGui::TextUnformatted(tr("Load a ROM to edit 3D objects."));
175 return;
176 }
177
178 if (!data_loaded_) {
179 auto status = LoadShapes();
180 if (!status.ok()) {
181 ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
182 tr("Failed to load shapes: %s"),
183 status.message().data());
184 return;
185 }
186 }
187
189
190 ImGui::Text(tr("ALTTP polyhedral data @ $09:%04X (PC $%05X), %u bytes"),
191 static_cast<uint16_t>(kPolyTableSnes & 0xFFFF), TablePc(),
192 kPolyRegionSize);
193 ImGui::TextUnformatted(
194 tr("Shapes: 0 = Crystal, 1 = Triforce (IDs used by POLYSHAPE)"));
195
196 // Shape selector
197 if (!shapes_.empty()) {
198 ImGui::SetNextItemWidth(gui::LayoutHelpers::GetStandardInputWidth());
199 if (ImGui::BeginCombo(tr("Shape"), shapes_[selected_shape_].name.c_str())) {
200 for (size_t i = 0; i < shapes_.size(); ++i) {
201 bool selected = static_cast<int>(i) == selected_shape_;
202 if (ImGui::Selectable(shapes_[i].name.c_str(), selected)) {
203 selected_shape_ = static_cast<int>(i);
205 }
206 }
207 ImGui::EndCombo();
208 }
209 }
210
211 if (ImGui::Button(ICON_MD_REFRESH " Reload from ROM")) {
212 auto status = LoadShapes();
213 if (!status.ok()) {
214 ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
215 tr("Reload failed: %s"), status.message().data());
216 }
217 }
218 ImGui::SameLine();
219 ImGui::BeginDisabled(!dirty_);
220 if (ImGui::Button(ICON_MD_SAVE " Save 3D objects")) {
221 auto status = SaveShapes();
222 if (!status.ok()) {
223 ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), tr("Save failed: %s"),
224 status.message().data());
225 }
226 }
227 ImGui::EndDisabled();
228
229 if (shapes_.empty()) {
230 ImGui::TextUnformatted(tr("No polyhedral shapes found."));
231 return;
232 }
233
234 ImGui::Separator();
236}
237
239 if (!rom_ || !rom_->is_loaded()) {
240 ImGui::TextUnformatted(tr("Load a ROM to edit 3D objects."));
241 return absl::OkStatus();
242 }
243
244 if (!data_loaded_) {
246 }
247
249
250 ImGui::Text(tr("ALTTP polyhedral data @ $09:%04X (PC $%05X), %u bytes"),
251 static_cast<uint16_t>(kPolyTableSnes & 0xFFFF), TablePc(),
252 kPolyRegionSize);
253 ImGui::TextUnformatted(
254 tr("Shapes: 0 = Crystal, 1 = Triforce (IDs used by POLYSHAPE)"));
255
256 // Shape selector
257 if (!shapes_.empty()) {
258 ImGui::SetNextItemWidth(gui::LayoutHelpers::GetStandardInputWidth());
259 if (ImGui::BeginCombo(tr("Shape"), shapes_[selected_shape_].name.c_str())) {
260 for (size_t i = 0; i < shapes_.size(); ++i) {
261 bool selected = static_cast<int>(i) == selected_shape_;
262 if (ImGui::Selectable(shapes_[i].name.c_str(), selected)) {
263 selected_shape_ = static_cast<int>(i);
265 }
266 }
267 ImGui::EndCombo();
268 }
269 }
270
271 if (ImGui::Button(ICON_MD_REFRESH " Reload from ROM")) {
273 }
274 ImGui::SameLine();
275 ImGui::BeginDisabled(!dirty_);
276 if (ImGui::Button(ICON_MD_SAVE " Save 3D objects")) {
278 }
279 ImGui::EndDisabled();
280
281 if (shapes_.empty()) {
282 ImGui::TextUnformatted(tr("No polyhedral shapes found."));
283 return absl::OkStatus();
284 }
285
286 ImGui::Separator();
288 return absl::OkStatus();
289}
290
292 ImGui::Text(tr("Vertices: %u Faces: %u"), shape.vertex_count,
293 shape.face_count);
294 ImGui::Text(tr("Vertex data @ $09:%04X (PC $%05X)"), shape.vertex_ptr,
295 ToPc(shape.vertex_ptr));
296 ImGui::Text(tr("Face data @ $09:%04X (PC $%05X)"), shape.face_ptr,
297 ToPc(shape.face_ptr));
298
299 ImGui::Spacing();
300
301 if (ImGui::BeginTable(
302 "##poly_editor", 2,
303 ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp)) {
304 ImGui::TableSetupColumn("Data", ImGuiTableColumnFlags_WidthStretch, 0.45f);
305 ImGui::TableSetupColumn("Plots", ImGuiTableColumnFlags_WidthStretch, 0.55f);
306
307 ImGui::TableNextColumn();
308 DrawVertexList(shape);
309 ImGui::Spacing();
310 DrawFaceList(shape);
311
312 ImGui::TableNextColumn();
313 DrawPlot("XY (X vs Y)", PlotPlane::kXY, shape);
314 DrawPlot("XZ (X vs Z)", PlotPlane::kXZ, shape);
315 ImGui::Spacing();
316 DrawPreview(shape);
317 ImGui::EndTable();
318 }
319}
320
322 if (shape.vertices.empty()) {
323 ImGui::TextUnformatted(tr("No vertices"));
324 return;
325 }
326
327 for (size_t i = 0; i < shape.vertices.size(); ++i) {
328 ImGui::PushID(static_cast<int>(i));
329 const bool is_selected = static_cast<int>(i) == selected_vertex_;
330 std::string label = absl::StrFormat("Vertex %zu", i);
331 if (ImGui::Selectable(label.c_str(), is_selected)) {
332 selected_vertex_ = static_cast<int>(i);
333 }
334
335 ImGui::SameLine();
336 ImGui::SetNextItemWidth(gui::LayoutHelpers::GetStandardInputWidth() * 1.2f);
337 int coords[3] = {shape.vertices[i].x, shape.vertices[i].y,
338 shape.vertices[i].z};
339 if (ImGui::InputInt3("##coords", coords)) {
340 shape.vertices[i].x = Clamp(coords[0], -127, 127);
341 shape.vertices[i].y = Clamp(coords[1], -127, 127);
342 shape.vertices[i].z = Clamp(coords[2], -127, 127);
343 dirty_ = true;
344 }
345 ImGui::PopID();
346 }
347}
348
350 if (shape.faces.empty()) {
351 ImGui::TextUnformatted(tr("No faces"));
352 return;
353 }
354
355 ImGui::TextUnformatted(tr("Faces (vertex indices + shade)"));
356 for (size_t i = 0; i < shape.faces.size(); ++i) {
357 ImGui::PushID(static_cast<int>(i));
358 ImGui::Text(tr("Face %zu"), i);
359 ImGui::SameLine();
360 int shade = shape.faces[i].shade;
361 ImGui::SetNextItemWidth(gui::LayoutHelpers::GetCompactInputWidth());
362 if (ImGui::InputInt(tr("Shade##face"), &shade, 0, 0)) {
363 shape.faces[i].shade = static_cast<uint8_t>(Clamp(shade, 0, 0xFF));
364 dirty_ = true;
365 }
366
367 ImGui::SameLine();
368 ImGui::TextUnformatted(tr("Vertices:"));
369 const int max_idx = shape.vertices.empty()
370 ? 0
371 : static_cast<int>(shape.vertices.size() - 1);
372 for (size_t v = 0; v < shape.faces[i].vertex_indices.size(); ++v) {
373 ImGui::SameLine();
374 int idx = shape.faces[i].vertex_indices[v];
375 ImGui::SetNextItemWidth(gui::LayoutHelpers::GetCompactInputWidth() *
376 0.75f);
377 if (ImGui::InputInt(absl::StrFormat("##v%zu", v).c_str(), &idx, 0, 0)) {
378 idx = Clamp(idx, 0, max_idx);
379 shape.faces[i].vertex_indices[v] = static_cast<uint8_t>(idx);
380 dirty_ = true;
381 }
382 }
383 ImGui::PopID();
384 }
385}
386
387void PolyhedralEditorView::DrawPlot(const char* label, PlotPlane plane,
388 PolyShape& shape) {
389 if (shape.vertices.empty()) {
390 return;
391 }
392
393 ImVec2 plot_size = ImVec2(-1, 220);
394 ImPlotFlags flags = ImPlotFlags_NoLegend | ImPlotFlags_Equal;
395 if (ImPlot::BeginPlot(label, plot_size, flags)) {
396 const char* x_label = (plane == PlotPlane::kYZ) ? "Y" : "X";
397 const char* y_label = (plane == PlotPlane::kXY) ? "Y" : "Z";
398 ImPlot::SetupAxes(x_label, y_label, ImPlotAxisFlags_AutoFit,
399 ImPlotAxisFlags_AutoFit);
400 ImPlot::SetupAxisLimits(ImAxis_X1, -80, 80, ImGuiCond_Once);
401 ImPlot::SetupAxisLimits(ImAxis_Y1, -80, 80, ImGuiCond_Once);
402
403 for (size_t i = 0; i < shape.vertices.size(); ++i) {
404 double x = shape.vertices[i].x;
405 double y = 0.0;
406 switch (plane) {
407 case PlotPlane::kXY:
408 y = shape.vertices[i].y;
409 break;
410 case PlotPlane::kXZ:
411 y = shape.vertices[i].z;
412 break;
413 case PlotPlane::kYZ:
414 x = shape.vertices[i].y;
415 y = shape.vertices[i].z;
416 break;
417 }
418
419 const bool is_selected = static_cast<int>(i) == selected_vertex_;
420 ImVec4 color = is_selected ? kSelectedVertexColor : kVertexColor;
421 // ImPlot::DragPoint wants an int ID, so compose one from vertex index and plane.
422 int point_id = static_cast<int>(i * 10 + static_cast<size_t>(plane));
423 if (ImPlot::DragPoint(point_id, &x, &y, color, 6.0f)) {
424 // Round so we keep integer coordinates in ROM
425 int rounded_x = Clamp(static_cast<int>(std::lround(x)), -127, 127);
426 int rounded_y = Clamp(static_cast<int>(std::lround(y)), -127, 127);
427
428 switch (plane) {
429 case PlotPlane::kXY:
430 shape.vertices[i].x = rounded_x;
431 shape.vertices[i].y = rounded_y;
432 break;
433 case PlotPlane::kXZ:
434 shape.vertices[i].x = rounded_x;
435 shape.vertices[i].z = rounded_y;
436 break;
437 case PlotPlane::kYZ:
438 shape.vertices[i].y = rounded_x;
439 shape.vertices[i].z = rounded_y;
440 break;
441 }
442
443 dirty_ = true;
444 if (!is_selected) {
445 selected_vertex_ = static_cast<int>(i);
446 }
447 }
448 }
449 ImPlot::EndPlot();
450 }
451}
452
454 if (shape.vertices.empty() || shape.faces.empty()) {
455 return;
456 }
457
458 static float rot_x = 0.35f;
459 static float rot_y = -0.4f;
460 static float rot_z = 0.0f;
461 static float zoom = 1.0f;
462
463 ImGui::TextUnformatted(tr("Preview (orthographic)"));
464 ImGui::SetNextItemWidth(gui::LayoutHelpers::GetComboWidth());
465 ImGui::SliderFloat(tr("Rot X"), &rot_x, -3.14f, 3.14f, "%.2f");
466 ImGui::SameLine();
467 ImGui::SetNextItemWidth(gui::LayoutHelpers::GetComboWidth());
468 ImGui::SliderFloat(tr("Rot Y"), &rot_y, -3.14f, 3.14f, "%.2f");
469 ImGui::SameLine();
470 ImGui::SetNextItemWidth(gui::LayoutHelpers::GetComboWidth());
471 ImGui::SliderFloat(tr("Rot Z"), &rot_z, -3.14f, 3.14f, "%.2f");
472 ImGui::SameLine();
473 ImGui::SetNextItemWidth(gui::LayoutHelpers::GetSliderWidth());
474 ImGui::SliderFloat(tr("Zoom"), &zoom, 0.5f, 3.0f, "%.2f");
475
476 // Precompute rotated vertices
477 struct RotV {
478 double x;
479 double y;
480 double z;
481 };
482 std::vector<RotV> rotated(shape.vertices.size());
483
484 const double cx = std::cos(rot_x);
485 const double sx = std::sin(rot_x);
486 const double cy = std::cos(rot_y);
487 const double sy = std::sin(rot_y);
488 const double cz = std::cos(rot_z);
489 const double sz = std::sin(rot_z);
490
491 for (size_t i = 0; i < shape.vertices.size(); ++i) {
492 const auto& v = shape.vertices[i];
493 double x = v.x;
494 double y = v.y;
495 double z = v.z;
496
497 // Rotate around X
498 double y1 = y * cx - z * sx;
499 double z1 = y * sx + z * cx;
500 // Rotate around Y
501 double x2 = x * cy + z1 * sy;
502 double z2 = -x * sy + z1 * cy;
503 // Rotate around Z
504 double x3 = x2 * cz - y1 * sz;
505 double y3 = x2 * sz + y1 * cz;
506
507 rotated[i] = {x3 * zoom, y3 * zoom, z2 * zoom};
508 }
509
510 struct FaceDepth {
511 double depth;
512 size_t idx;
513 };
514 std::vector<FaceDepth> order;
515 order.reserve(shape.faces.size());
516 for (size_t i = 0; i < shape.faces.size(); ++i) {
517 double accum = 0.0;
518 for (auto idx : shape.faces[i].vertex_indices) {
519 if (idx < rotated.size()) {
520 accum += rotated[idx].z;
521 }
522 }
523 double avg =
524 shape.faces[i].vertex_indices.empty()
525 ? 0.0
526 : accum / static_cast<double>(shape.faces[i].vertex_indices.size());
527 order.push_back({avg, i});
528 }
529
530 std::sort(order.begin(), order.end(),
531 [](const FaceDepth& a, const FaceDepth& b) {
532 return a.depth < b.depth; // back to front
533 });
534
535 ImVec2 preview_size(-1, 260);
536 ImPlotFlags flags = ImPlotFlags_NoLegend | ImPlotFlags_Equal;
537 if (ImPlot::BeginPlot("PreviewXY", preview_size, flags)) {
538 ImPlot::SetupAxes(nullptr, nullptr, ImPlotAxisFlags_NoDecorations,
539 ImPlotAxisFlags_NoDecorations);
540 ImPlot::SetupAxisLimits(ImAxis_X1, -120, 120, ImGuiCond_Always);
541 ImPlot::SetupAxisLimits(ImAxis_Y1, -120, 120, ImGuiCond_Always);
542
543 ImDrawList* dl = ImPlot::GetPlotDrawList();
544 ImVec4 base_color = ImVec4(0.8f, 0.9f, 1.0f, 0.55f);
545
546 for (const auto& fd : order) {
547 const auto& face = shape.faces[fd.idx];
548 if (face.vertex_indices.size() < 3) {
549 continue;
550 }
551
552 std::vector<ImVec2> pts;
553 pts.reserve(face.vertex_indices.size());
554
555 for (auto idx : face.vertex_indices) {
556 if (idx >= rotated.size()) {
557 continue;
558 }
559 ImVec2 p = ImPlot::PlotToPixels(rotated[idx].x, rotated[idx].y);
560 pts.push_back(p);
561 }
562
563 if (pts.size() < 3) {
564 continue;
565 }
566
567 ImU32 fill_col = ImGui::GetColorU32(base_color);
568 ImU32 line_col = ImGui::GetColorU32(ImVec4(0.2f, 0.4f, 0.6f, 1.0f));
569 dl->AddConvexPolyFilled(pts.data(), static_cast<int>(pts.size()),
570 fill_col);
571 dl->AddPolyline(pts.data(), static_cast<int>(pts.size()), line_col,
572 ImDrawFlags_Closed, 2.0f);
573 }
574
575 // Draw vertices as dots
576 for (size_t i = 0; i < rotated.size(); ++i) {
577 ImVec2 p = ImPlot::PlotToPixels(rotated[i].x, rotated[i].y);
578 ImU32 col = ImGui::GetColorU32(kVertexColor);
579 dl->AddCircleFilled(p, 4.0f, col);
580 }
581
582 ImPlot::EndPlot();
583 }
584}
585
586} // namespace editor
587} // namespace yaze
absl::StatusOr< std::vector< uint8_t > > ReadByteVector(uint32_t offset, uint32_t length) const
Definition rom.cc:541
absl::StatusOr< uint8_t > ReadByte(int offset) const
Definition rom.cc:518
absl::Status WriteVector(int addr, std::vector< uint8_t > data)
Definition rom.cc:658
bool is_loaded() const
Definition rom.h:144
void Draw(bool *p_open) override
Draw the polyhedral editor UI (WindowContent interface)
void DrawPlot(const char *label, PlotPlane plane, PolyShape &shape)
absl::Status Update()
Legacy Update method for backward compatibility.
absl::Status WriteShape(const PolyShape &shape)
static float GetSliderWidth()
static float GetCompactInputWidth()
static float GetStandardInputWidth()
#define ICON_MD_REFRESH
Definition icons.h:1572
#define ICON_MD_SAVE
Definition icons.h:1644
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
uint32_t SnesToPc(uint32_t addr) noexcept
Definition snes.h:8
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
std::vector< uint8_t > vertex_indices
std::vector< PolyFace > faces
std::vector< PolyVertex > vertices