feat: Improve dungeon canvas rendering and object interaction
- Updated DungeonCanvasViewer and DungeonEditor to render room objects as primitives temporarily for debugging purposes. - Enhanced object interaction with new context menu options for deleting, copying, and pasting selected objects. - Implemented drag-and-drop functionality for moving selected objects within the dungeon. - Added texture processing for background layers and deferred texture commands to optimize rendering performance. - Refactored code for clarity and maintainability, ensuring better handling of object interactions and rendering logic.
This commit is contained in:
@@ -117,40 +117,49 @@ void DungeonCanvasViewer::DrawDungeonCanvas(int room_id) {
|
||||
}
|
||||
|
||||
// Render the room's background layers
|
||||
// This already includes objects drawn by ObjectDrawer in Room::RenderObjectsToBackground()
|
||||
RenderRoomBackgroundLayers(room_id);
|
||||
|
||||
// Render room objects with proper graphics (old system as fallback)
|
||||
// TEMPORARY: Render all objects as primitives until proper rendering is fixed
|
||||
// This ensures we can see objects while debugging the texture pipeline
|
||||
if (current_palette_id_ < current_palette_group_.size()) {
|
||||
auto room_palette = current_palette_group_[current_palette_id_];
|
||||
|
||||
// Render regular objects with proper graphics
|
||||
// Render regular objects as colored rectangles (FALLBACK)
|
||||
for (const auto& object : room.GetTileObjects()) {
|
||||
RenderObjectInCanvas(object, room_palette);
|
||||
}
|
||||
|
||||
// Render special objects with primitive shapes
|
||||
// Render special objects with primitive shapes (overlays)
|
||||
RenderStairObjects(room, room_palette);
|
||||
RenderChests(room);
|
||||
RenderDoorObjects(room);
|
||||
RenderWallObjects(room);
|
||||
RenderPotObjects(room);
|
||||
|
||||
// Render sprites as simple 16x16 squares with labels
|
||||
RenderSprites(room);
|
||||
}
|
||||
|
||||
// Render sprites as simple 16x16 squares with labels
|
||||
// (Sprites are not part of the background buffers)
|
||||
RenderSprites(room);
|
||||
|
||||
// Handle object interaction if enabled
|
||||
if (object_interaction_enabled_) {
|
||||
object_interaction_.HandleCanvasMouseInput();
|
||||
object_interaction_.CheckForObjectSelection();
|
||||
object_interaction_.DrawSelectBox();
|
||||
object_interaction_.DrawSelectionHighlights(); // Draw selection highlights on top
|
||||
object_interaction_.ShowContextMenu(); // Show dungeon-aware context menu
|
||||
}
|
||||
}
|
||||
|
||||
canvas_.DrawGrid();
|
||||
canvas_.DrawOverlay();
|
||||
|
||||
// Process queued texture commands
|
||||
if (rom_ && rom_->is_loaded()) {
|
||||
gfx::Arena::Get().ProcessTextureQueue(nullptr); // Will use default renderer
|
||||
}
|
||||
|
||||
// Draw layer information overlay
|
||||
if (rooms_ && rom_->is_loaded()) {
|
||||
auto& room = (*rooms_)[room_id];
|
||||
|
||||
@@ -715,20 +715,42 @@ void DungeonEditor::DrawDungeonCanvas(int room_id) {
|
||||
}
|
||||
|
||||
// Render background layers with proper positioning
|
||||
renderer_.RenderRoomBackgroundLayers(room_id);
|
||||
|
||||
// Render room objects and sprites with improved graphics
|
||||
// This uses per-room buffers which already include objects drawn by ObjectDrawer
|
||||
auto& room = rooms_[room_id];
|
||||
auto& bg1_bitmap = room.bg1_buffer().bitmap();
|
||||
auto& bg2_bitmap = room.bg2_buffer().bitmap();
|
||||
|
||||
if (bg1_bitmap.is_active() && bg1_bitmap.width() > 0) {
|
||||
if (!bg1_bitmap.texture()) {
|
||||
// Queue texture creation for background layer 1
|
||||
gfx::Arena::Get().QueueTextureCommand(
|
||||
gfx::Arena::TextureCommandType::CREATE, &bg1_bitmap);
|
||||
}
|
||||
canvas_.DrawBitmap(bg1_bitmap, 0, 0, 1.0f, 255);
|
||||
}
|
||||
|
||||
if (bg2_bitmap.is_active() && bg2_bitmap.width() > 0) {
|
||||
if (!bg2_bitmap.texture()) {
|
||||
// Queue texture creation for background layer 2
|
||||
gfx::Arena::Get().QueueTextureCommand(
|
||||
gfx::Arena::TextureCommandType::CREATE, &bg2_bitmap);
|
||||
}
|
||||
canvas_.DrawBitmap(bg2_bitmap, 0, 0, 1.0f, 200);
|
||||
}
|
||||
|
||||
// TEMPORARY: Render all objects as primitives until proper rendering is fixed
|
||||
if (current_palette_id_ < current_palette_group_.size()) {
|
||||
auto room_palette = current_palette_group_[current_palette_id_];
|
||||
|
||||
// Render regular objects with improved fallback
|
||||
for (const auto& object : rooms_[room_id].GetTileObjects()) {
|
||||
// Render regular objects with primitive fallback
|
||||
for (const auto& object : room.GetTileObjects()) {
|
||||
renderer_.RenderObjectInCanvas(object, room_palette);
|
||||
}
|
||||
|
||||
// Render sprites as simple 16x16 squares with labels
|
||||
renderer_.RenderSprites(rooms_[room_id]);
|
||||
}
|
||||
|
||||
// Render sprites as simple 16x16 squares with labels
|
||||
// (Sprites are not part of the background buffers)
|
||||
renderer_.RenderSprites(rooms_[room_id]);
|
||||
}
|
||||
|
||||
// Phase 5: Render with integrated object editor
|
||||
@@ -740,6 +762,9 @@ void DungeonEditor::DrawDungeonCanvas(int room_id) {
|
||||
|
||||
canvas_.DrawGrid();
|
||||
canvas_.DrawOverlay();
|
||||
|
||||
// Process queued texture commands
|
||||
ProcessDeferredTextures();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -847,4 +872,10 @@ absl::Status DungeonEditor::UpdateRoomBackgroundLayers(int /*room_id*/) {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
void DungeonEditor::ProcessDeferredTextures() {
|
||||
// Process queued texture commands via Arena's deferred system
|
||||
// Note: Arena will use its stored renderer reference
|
||||
gfx::Arena::Get().ProcessTextureQueue(nullptr);
|
||||
}
|
||||
|
||||
} // namespace yaze::editor
|
||||
|
||||
@@ -110,6 +110,9 @@ class DungeonEditor : public Editor {
|
||||
void RenderRoomWithObjects(int room_id);
|
||||
void UpdateObjectEditor();
|
||||
|
||||
// Texture processing
|
||||
void ProcessDeferredTextures();
|
||||
|
||||
// Room selection management
|
||||
void OnRoomSelected(int room_id);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "dungeon_object_interaction.h"
|
||||
|
||||
#include "app/gui/color.h"
|
||||
#include <algorithm>
|
||||
|
||||
#include "imgui/imgui.h"
|
||||
|
||||
namespace yaze::editor {
|
||||
@@ -67,7 +68,34 @@ void DungeonObjectInteraction::HandleCanvasMouseInput() {
|
||||
}
|
||||
if (is_dragging_) {
|
||||
is_dragging_ = false;
|
||||
// TODO: Apply drag transformation to selected objects
|
||||
// Apply drag transformation to selected objects
|
||||
if (!selected_object_indices_.empty() && rooms_ && current_room_id_ >= 0 && current_room_id_ < 296) {
|
||||
auto& room = (*rooms_)[current_room_id_];
|
||||
ImVec2 drag_delta = ImVec2(drag_current_pos_.x - drag_start_pos_.x,
|
||||
drag_current_pos_.y - drag_start_pos_.y);
|
||||
|
||||
// Convert pixel delta to tile delta
|
||||
int tile_delta_x = static_cast<int>(drag_delta.x) / 8;
|
||||
int tile_delta_y = static_cast<int>(drag_delta.y) / 8;
|
||||
|
||||
// Move all selected objects
|
||||
auto& objects = room.GetTileObjects();
|
||||
for (size_t index : selected_object_indices_) {
|
||||
if (index < objects.size()) {
|
||||
objects[index].x_ += tile_delta_x;
|
||||
objects[index].y_ += tile_delta_y;
|
||||
|
||||
// Clamp to room bounds (64x64 tiles)
|
||||
objects[index].x_ = std::clamp(static_cast<int>(objects[index].x_), 0, 63);
|
||||
objects[index].y_ = std::clamp(static_cast<int>(objects[index].y_), 0, 63);
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger cache invalidation and re-render
|
||||
if (cache_invalidation_callback_) {
|
||||
cache_invalidation_callback_();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -252,7 +280,8 @@ void DungeonObjectInteraction::DrawSelectBox() {
|
||||
}
|
||||
|
||||
void DungeonObjectInteraction::DrawDragPreview() {
|
||||
if (!is_dragging_) return;
|
||||
if (!is_dragging_ || selected_object_indices_.empty() || !rooms_) return;
|
||||
if (current_room_id_ < 0 || current_room_id_ >= 296) return;
|
||||
|
||||
// Draw drag preview for selected objects
|
||||
ImDrawList* draw_list = ImGui::GetWindowDrawList();
|
||||
@@ -260,11 +289,30 @@ void DungeonObjectInteraction::DrawDragPreview() {
|
||||
ImVec2 drag_delta = ImVec2(drag_current_pos_.x - drag_start_pos_.x,
|
||||
drag_current_pos_.y - drag_start_pos_.y);
|
||||
|
||||
auto& room = (*rooms_)[current_room_id_];
|
||||
const auto& objects = room.GetTileObjects();
|
||||
|
||||
// Draw preview of where objects would be moved
|
||||
for (int obj_id : selected_objects_) {
|
||||
// TODO: Draw preview of object at new position
|
||||
// This would require getting the object's current position and drawing it
|
||||
// offset by drag_delta
|
||||
for (size_t index : selected_object_indices_) {
|
||||
if (index < objects.size()) {
|
||||
const auto& object = objects[index];
|
||||
auto [canvas_x, canvas_y] = RoomToCanvasCoordinates(object.x_, object.y_);
|
||||
|
||||
// Calculate object size
|
||||
int obj_width = 8 + (object.size_ & 0x0F) * 4;
|
||||
int obj_height = 8 + ((object.size_ >> 4) & 0x0F) * 4;
|
||||
obj_width = std::min(obj_width, 64);
|
||||
obj_height = std::min(obj_height, 64);
|
||||
|
||||
// Draw semi-transparent preview at new position
|
||||
ImVec2 preview_start(canvas_pos.x + canvas_x + drag_delta.x,
|
||||
canvas_pos.y + canvas_y + drag_delta.y);
|
||||
ImVec2 preview_end(preview_start.x + obj_width, preview_start.y + obj_height);
|
||||
|
||||
// Draw ghosted object
|
||||
draw_list->AddRectFilled(preview_start, preview_end, IM_COL32(0, 255, 255, 64));
|
||||
draw_list->AddRect(preview_start, preview_end, IM_COL32(0, 255, 255, 255), 0.0f, 0, 1.5f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,4 +389,128 @@ void DungeonObjectInteraction::ClearSelection() {
|
||||
is_dragging_ = false;
|
||||
}
|
||||
|
||||
void DungeonObjectInteraction::ShowContextMenu() {
|
||||
if (!canvas_->IsMouseHovering()) return;
|
||||
|
||||
// Show context menu on right-click when not dragging
|
||||
if (ImGui::IsMouseClicked(ImGuiMouseButton_Right) && !is_dragging_) {
|
||||
ImGui::OpenPopup("DungeonObjectContextMenu");
|
||||
}
|
||||
|
||||
if (ImGui::BeginPopup("DungeonObjectContextMenu")) {
|
||||
// Show different options based on current state
|
||||
if (!selected_object_indices_.empty()) {
|
||||
if (ImGui::MenuItem("Delete Selected", "Del")) {
|
||||
HandleDeleteSelected();
|
||||
}
|
||||
if (ImGui::MenuItem("Copy Selected", "Ctrl+C")) {
|
||||
HandleCopySelected();
|
||||
}
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
if (has_clipboard_data_) {
|
||||
if (ImGui::MenuItem("Paste Objects", "Ctrl+V")) {
|
||||
HandlePasteObjects();
|
||||
}
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
if (object_loaded_) {
|
||||
ImGui::Text("Placing: Object 0x%02X", preview_object_.id_);
|
||||
if (ImGui::MenuItem("Cancel Placement", "Esc")) {
|
||||
object_loaded_ = false;
|
||||
}
|
||||
} else {
|
||||
ImGui::Text("Right-click + drag to select");
|
||||
ImGui::Text("Left-click + drag to move");
|
||||
}
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
void DungeonObjectInteraction::HandleDeleteSelected() {
|
||||
if (selected_object_indices_.empty() || !rooms_) return;
|
||||
if (current_room_id_ < 0 || current_room_id_ >= 296) return;
|
||||
|
||||
auto& room = (*rooms_)[current_room_id_];
|
||||
|
||||
// Sort indices in descending order to avoid index shifts during deletion
|
||||
std::vector<size_t> sorted_indices = selected_object_indices_;
|
||||
std::sort(sorted_indices.rbegin(), sorted_indices.rend());
|
||||
|
||||
// Delete selected objects using Room's RemoveTileObject method
|
||||
for (size_t index : sorted_indices) {
|
||||
room.RemoveTileObject(index);
|
||||
}
|
||||
|
||||
// Clear selection
|
||||
ClearSelection();
|
||||
|
||||
// Trigger cache invalidation and re-render
|
||||
if (cache_invalidation_callback_) {
|
||||
cache_invalidation_callback_();
|
||||
}
|
||||
}
|
||||
|
||||
void DungeonObjectInteraction::HandleCopySelected() {
|
||||
if (selected_object_indices_.empty() || !rooms_) return;
|
||||
if (current_room_id_ < 0 || current_room_id_ >= 296) return;
|
||||
|
||||
auto& room = (*rooms_)[current_room_id_];
|
||||
const auto& objects = room.GetTileObjects();
|
||||
|
||||
// Copy selected objects to clipboard
|
||||
clipboard_.clear();
|
||||
for (size_t index : selected_object_indices_) {
|
||||
if (index < objects.size()) {
|
||||
clipboard_.push_back(objects[index]);
|
||||
}
|
||||
}
|
||||
|
||||
has_clipboard_data_ = !clipboard_.empty();
|
||||
}
|
||||
|
||||
void DungeonObjectInteraction::HandlePasteObjects() {
|
||||
if (!has_clipboard_data_ || !rooms_) return;
|
||||
if (current_room_id_ < 0 || current_room_id_ >= 296) return;
|
||||
|
||||
auto& room = (*rooms_)[current_room_id_];
|
||||
|
||||
// Get mouse position for paste location
|
||||
const ImGuiIO& io = ImGui::GetIO();
|
||||
ImVec2 mouse_pos = io.MousePos;
|
||||
ImVec2 canvas_pos = canvas_->zero_point();
|
||||
ImVec2 canvas_mouse_pos =
|
||||
ImVec2(mouse_pos.x - canvas_pos.x, mouse_pos.y - canvas_pos.y);
|
||||
auto [paste_x, paste_y] = CanvasToRoomCoordinates(
|
||||
static_cast<int>(canvas_mouse_pos.x),
|
||||
static_cast<int>(canvas_mouse_pos.y));
|
||||
|
||||
// Calculate offset from first object in clipboard
|
||||
if (!clipboard_.empty()) {
|
||||
int offset_x = paste_x - clipboard_[0].x_;
|
||||
int offset_y = paste_y - clipboard_[0].y_;
|
||||
|
||||
// Paste all objects with offset
|
||||
for (const auto& obj : clipboard_) {
|
||||
auto new_obj = obj;
|
||||
new_obj.x_ = obj.x_ + offset_x;
|
||||
new_obj.y_ = obj.y_ + offset_y;
|
||||
|
||||
// Clamp to room bounds
|
||||
new_obj.x_ = std::clamp(static_cast<int>(new_obj.x_), 0, 63);
|
||||
new_obj.y_ = std::clamp(static_cast<int>(new_obj.y_), 0, 63);
|
||||
|
||||
room.AddTileObject(new_obj);
|
||||
}
|
||||
|
||||
// Trigger cache invalidation and re-render
|
||||
if (cache_invalidation_callback_) {
|
||||
cache_invalidation_callback_();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace yaze::editor
|
||||
|
||||
@@ -52,6 +52,12 @@ class DungeonObjectInteraction {
|
||||
bool IsObjectSelectActive() const { return object_select_active_; }
|
||||
void ClearSelection();
|
||||
|
||||
// Context menu
|
||||
void ShowContextMenu();
|
||||
void HandleDeleteSelected();
|
||||
void HandleCopySelected();
|
||||
void HandlePasteObjects();
|
||||
|
||||
// Callbacks
|
||||
void SetObjectPlacedCallback(std::function<void(const zelda3::RoomObject&)> callback) {
|
||||
object_placed_callback_ = callback;
|
||||
@@ -87,6 +93,10 @@ class DungeonObjectInteraction {
|
||||
// Callbacks
|
||||
std::function<void(const zelda3::RoomObject&)> object_placed_callback_;
|
||||
std::function<void()> cache_invalidation_callback_;
|
||||
|
||||
// Clipboard for copy/paste
|
||||
std::vector<zelda3::RoomObject> clipboard_;
|
||||
bool has_clipboard_data_ = false;
|
||||
};
|
||||
|
||||
} // namespace editor
|
||||
|
||||
Reference in New Issue
Block a user