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