diff --git a/src/app/editor/dungeon/dungeon_canvas_viewer.cc b/src/app/editor/dungeon/dungeon_canvas_viewer.cc index d53bd6a2..a91b2a9c 100644 --- a/src/app/editor/dungeon/dungeon_canvas_viewer.cc +++ b/src/app/editor/dungeon/dungeon_canvas_viewer.cc @@ -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]; diff --git a/src/app/editor/dungeon/dungeon_editor.cc b/src/app/editor/dungeon/dungeon_editor.cc index 43dedf7f..10188922 100644 --- a/src/app/editor/dungeon/dungeon_editor.cc +++ b/src/app/editor/dungeon/dungeon_editor.cc @@ -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 diff --git a/src/app/editor/dungeon/dungeon_editor.h b/src/app/editor/dungeon/dungeon_editor.h index bd1c7178..421cb5da 100644 --- a/src/app/editor/dungeon/dungeon_editor.h +++ b/src/app/editor/dungeon/dungeon_editor.h @@ -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); diff --git a/src/app/editor/dungeon/dungeon_object_interaction.cc b/src/app/editor/dungeon/dungeon_object_interaction.cc index 92cac6a4..9ab4a403 100644 --- a/src/app/editor/dungeon/dungeon_object_interaction.cc +++ b/src/app/editor/dungeon/dungeon_object_interaction.cc @@ -1,6 +1,7 @@ #include "dungeon_object_interaction.h" -#include "app/gui/color.h" +#include + #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(drag_delta.x) / 8; + int tile_delta_y = static_cast(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(objects[index].x_), 0, 63); + objects[index].y_ = std::clamp(static_cast(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 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(canvas_mouse_pos.x), + static_cast(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(new_obj.x_), 0, 63); + new_obj.y_ = std::clamp(static_cast(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 diff --git a/src/app/editor/dungeon/dungeon_object_interaction.h b/src/app/editor/dungeon/dungeon_object_interaction.h index 3c129fcb..7e7b5827 100644 --- a/src/app/editor/dungeon/dungeon_object_interaction.h +++ b/src/app/editor/dungeon/dungeon_object_interaction.h @@ -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 callback) { object_placed_callback_ = callback; @@ -87,6 +93,10 @@ class DungeonObjectInteraction { // Callbacks std::function object_placed_callback_; std::function cache_invalidation_callback_; + + // Clipboard for copy/paste + std::vector clipboard_; + bool has_clipboard_data_ = false; }; } // namespace editor