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