yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
object_selection.cc
Go to the documentation of this file.
1#include "object_selection.h"
2
3#include <algorithm>
4
5#include "absl/strings/str_format.h"
8#include "imgui/imgui.h"
9#include "util/log.h"
11
12namespace yaze::editor {
13
14// ============================================================================
15// Selection Operations
16// ============================================================================
17
19 switch (mode) {
21 // Replace entire selection with single object
22 selected_indices_.clear();
23 selected_indices_.insert(index);
24 break;
25
27 // Add to existing selection (Shift+click)
28 selected_indices_.insert(index);
29 break;
30
32 // Toggle object in selection (Ctrl+click)
33 if (selected_indices_.count(index)) {
34 selected_indices_.erase(index);
35 } else {
36 selected_indices_.insert(index);
37 }
38 break;
39
41 // This shouldn't be used for single object selection
42 LOG_ERROR("ObjectSelection",
43 "Rectangle mode used for single object selection");
44 selected_indices_.insert(index);
45 break;
46 }
47
49}
50
52 int room_min_x, int room_min_y, int room_max_x, int room_max_y,
53 const std::vector<zelda3::RoomObject>& objects, SelectionMode mode) {
54 // Normalize rectangle bounds
55 int min_x = std::min(room_min_x, room_max_x);
56 int max_x = std::max(room_min_x, room_max_x);
57 int min_y = std::min(room_min_y, room_max_y);
58 int max_y = std::max(room_min_y, room_max_y);
59
60 // For Single mode, clear previous selection first
61 if (mode == SelectionMode::Single) {
62 selected_indices_.clear();
63 }
64
65 // Find all objects within rectangle
66 for (size_t i = 0; i < objects.size(); ++i) {
67 if (IsObjectInRectangle(objects[i], min_x, min_y, max_x, max_y)) {
68 if (mode == SelectionMode::Toggle) {
69 // Toggle each object
70 if (selected_indices_.count(i)) {
71 selected_indices_.erase(i);
72 } else {
73 selected_indices_.insert(i);
74 }
75 } else {
76 // Add or Replace mode - just add
77 selected_indices_.insert(i);
78 }
79 }
80 }
81
83}
84
85void ObjectSelection::SelectAll(size_t object_count) {
86 selected_indices_.clear();
87 for (size_t i = 0; i < object_count; ++i) {
88 selected_indices_.insert(i);
89 }
91}
92
93void ObjectSelection::SelectAll(const std::vector<zelda3::RoomObject>& objects) {
94 selected_indices_.clear();
95 for (size_t i = 0; i < objects.size(); ++i) {
96 // Only select objects that pass the layer filter
97 if (PassesLayerFilter(objects[i])) {
98 selected_indices_.insert(i);
99 }
100 }
102}
103
105 if (selected_indices_.empty()) {
106 return; // No change
107 }
108
109 selected_indices_.clear();
111}
112
113bool ObjectSelection::IsObjectSelected(size_t index) const {
114 return selected_indices_.count(index) > 0;
115}
116
117std::vector<size_t> ObjectSelection::GetSelectedIndices() const {
118 // Safely convert set to vector with bounds checking
119 std::vector<size_t> result;
120 result.reserve(selected_indices_.size());
121 for (size_t idx : selected_indices_) {
122 result.push_back(idx);
123 }
124 return result;
125}
126
127std::optional<size_t> ObjectSelection::GetPrimarySelection() const {
128 if (selected_indices_.empty()) {
129 return std::nullopt;
130 }
131 return *selected_indices_.begin(); // First element (lowest index)
132}
133
134// ============================================================================
135// Rectangle Selection State
136// ============================================================================
137
138void ObjectSelection::BeginRectangleSelection(int canvas_x, int canvas_y) {
140 rect_start_x_ = canvas_x;
141 rect_start_y_ = canvas_y;
142 rect_end_x_ = canvas_x;
143 rect_end_y_ = canvas_y;
144}
145
146void ObjectSelection::UpdateRectangleSelection(int canvas_x, int canvas_y) {
148 LOG_ERROR("ObjectSelection",
149 "UpdateRectangleSelection called when not active");
150 return;
151 }
152
153 rect_end_x_ = canvas_x;
154 rect_end_y_ = canvas_y;
155}
156
158 const std::vector<zelda3::RoomObject>& objects, SelectionMode mode) {
160 LOG_ERROR("ObjectSelection",
161 "EndRectangleSelection called when not active");
162 return;
163 }
164
165 // Convert canvas coordinates to room coordinates
166 auto [start_room_x, start_room_y] =
168 auto [end_room_x, end_room_y] =
170
171 // Select objects in rectangle
172 SelectObjectsInRect(start_room_x, start_room_y, end_room_x, end_room_y,
173 objects, mode);
174
176}
177
181
182std::tuple<int, int, int, int> ObjectSelection::GetRectangleSelectionBounds()
183 const {
184 int min_x = std::min(rect_start_x_, rect_end_x_);
185 int max_x = std::max(rect_start_x_, rect_end_x_);
186 int min_y = std::min(rect_start_y_, rect_end_y_);
187 int max_y = std::max(rect_start_y_, rect_end_y_);
188 return {min_x, min_y, max_x, max_y};
189}
190
191bool ObjectSelection::IsRectangleLargeEnough(int min_pixels) const {
193 return false;
194 }
195
196 auto [min_x, min_y, max_x, max_y] = GetRectangleSelectionBounds();
197 const int width = std::abs(max_x - min_x);
198 const int height = std::abs(max_y - min_y);
199 return (width >= min_pixels) || (height >= min_pixels);
200}
201
202// ============================================================================
203// Visual Rendering
204// ============================================================================
205
207 gui::Canvas* canvas, const std::vector<zelda3::RoomObject>& objects,
208 std::function<std::tuple<int, int, int, int>(const zelda3::RoomObject&)>
209 bounds_calculator) {
210 if (selected_indices_.empty() || !canvas) {
211 return;
212 }
213
214 ImDrawList* draw_list = ImGui::GetWindowDrawList();
215 ImVec2 canvas_pos = canvas->zero_point();
216 float scale = canvas->global_scale();
217
218 for (size_t index : selected_indices_) {
219 if (index >= objects.size()) {
220 continue;
221 }
222
223 const auto& object = objects[index];
224
225 // Calculate object position in canvas coordinates
226 auto [obj_x, obj_y] = RoomToCanvasCoordinates(object.x_, object.y_);
227
228 int offset_x = 0;
229 int offset_y = 0;
230 int pixel_width = 0;
231 int pixel_height = 0;
232 if (bounds_calculator) {
233 auto [dx, dy, w, h] = bounds_calculator(object);
234 offset_x = dx;
235 offset_y = dy;
236 pixel_width = w;
237 pixel_height = h;
238 } else {
239 // Fallback to old logic if no calculator provided
240 auto [tile_x, tile_y, tile_width, tile_height] = GetObjectBounds(object);
241 offset_x = (tile_x - object.x_) * 8;
242 offset_y = (tile_y - object.y_) * 8;
243 pixel_width = tile_width * 8;
244 pixel_height = tile_height * 8;
245 }
246
247 obj_x += offset_x;
248 obj_y += offset_y;
249
250 // Apply scale and canvas offset
251 ImVec2 obj_start(canvas_pos.x + obj_x * scale,
252 canvas_pos.y + obj_y * scale);
253 ImVec2 obj_end(obj_start.x + pixel_width * scale,
254 obj_start.y + pixel_height * scale);
255
256 // Expand selection box slightly for visibility
257 constexpr float margin = 2.0f;
258 obj_start.x -= margin;
259 obj_start.y -= margin;
260 obj_end.x += margin;
261 obj_end.y += margin;
262
263 // Get color based on theme
264 const auto& theme = AgentUI::GetTheme();
265
266 // Draw pulsing animated border
267 float pulse =
268 0.6f + 0.4f * std::sin(static_cast<float>(ImGui::GetTime()) * 6.0f);
269 ImVec4 pulsing_color = theme.selection_primary;
270 pulsing_color.w = 0.4f + 0.4f * pulse;
271
272 ImU32 border_color = ImGui::GetColorU32(theme.selection_primary);
273 ImU32 pulse_color_u32 = ImGui::GetColorU32(pulsing_color);
274
275 draw_list->AddRect(obj_start, obj_end, border_color, 0.0f, 0, 2.0f);
276 draw_list->AddRect(obj_start, obj_end, pulse_color_u32, 0.0f, 0, 4.0f);
277
278 // Draw corner handles with theme handle color
279 float handle_size = 6.0f * std::max(1.0f, scale * 0.5f);
280 ImU32 handle_color = ImGui::GetColorU32(theme.selection_handle);
281 ImU32 handle_outline = ImGui::GetColorU32(ImVec4(0, 0, 0, 0.8f));
282
283 auto draw_handle = [&](ImVec2 pos) {
284 draw_list->AddRectFilled(
285 ImVec2(pos.x - handle_size / 2, pos.y - handle_size / 2),
286 ImVec2(pos.x + handle_size / 2, pos.y + handle_size / 2),
287 handle_color, 2.0f);
288 draw_list->AddRect(
289 ImVec2(pos.x - handle_size / 2, pos.y - handle_size / 2),
290 ImVec2(pos.x + handle_size / 2, pos.y + handle_size / 2),
291 handle_outline, 2.0f, 0, 1.0f);
292 };
293
294 draw_handle(obj_start);
295 draw_handle(ImVec2(obj_end.x, obj_start.y));
296 draw_handle(ImVec2(obj_start.x, obj_end.y));
297 draw_handle(obj_end);
298 }
299}
300
302 const auto& theme = AgentUI::GetTheme();
303 int layer = object.GetLayerValue();
304
305 switch (layer) {
306 case 0: return theme.dungeon_outline_layer0;
307 case 1: return theme.dungeon_outline_layer1;
308 case 2: return theme.dungeon_outline_layer2;
309 default: return theme.status_inactive;
310 }
311}
312
314 if (!rectangle_selection_active_ || !canvas) {
315 return;
316 }
317
318 const auto& theme = AgentUI::GetTheme();
319 ImDrawList* draw_list = ImGui::GetWindowDrawList();
320 ImVec2 canvas_pos = canvas->zero_point();
321 float scale = canvas->global_scale();
322
323 // Get normalized bounds
324 auto [min_x, min_y, max_x, max_y] = GetRectangleSelectionBounds();
325
326 // Apply scale and canvas offset
327 ImVec2 box_start(canvas_pos.x + min_x * scale, canvas_pos.y + min_y * scale);
328 ImVec2 box_end(canvas_pos.x + max_x * scale, canvas_pos.y + max_y * scale);
329
330 // Draw selection box with theme selection color
331 // Border: High-contrast at 0.85f alpha
332 ImU32 border_color = ImGui::ColorConvertFloat4ToU32(
333 ImVec4(theme.selection_secondary.x, theme.selection_secondary.y, theme.selection_secondary.z,
334 0.85f));
335 // Fill: Subtle at 0.15f alpha
336 ImU32 fill_color = ImGui::ColorConvertFloat4ToU32(
337 ImVec4(theme.selection_secondary.x, theme.selection_secondary.y, theme.selection_secondary.z,
338 0.15f));
339
340 draw_list->AddRectFilled(box_start, box_end, fill_color);
341 draw_list->AddRect(box_start, box_end, border_color, 0.0f, 0, 2.0f);
342}
343
344// ============================================================================
345// Utility Functions
346// ============================================================================
347
348std::pair<int, int> ObjectSelection::RoomToCanvasCoordinates(int room_x,
349 int room_y) {
350 // Dungeon tiles are 8x8 pixels
351 return {room_x * 8, room_y * 8};
352}
353
354std::pair<int, int> ObjectSelection::CanvasToRoomCoordinates(int canvas_x,
355 int canvas_y) {
356 // Convert pixels back to tiles (round down)
357 return {canvas_x / 8, canvas_y / 8};
358}
359
360std::tuple<int, int, int, int> ObjectSelection::GetObjectBounds(
361 const zelda3::RoomObject& object) {
363}
364
365// ============================================================================
366// Private Helper Functions
367// ============================================================================
368
374
376 int min_x, int min_y, int max_x,
377 int max_y) const {
378 // Check layer filter first
379 if (!PassesLayerFilter(object)) {
380 return false;
381 }
382
383 // Get object bounds
384 auto [obj_x, obj_y, obj_width, obj_height] = GetObjectBounds(object);
385
386 // Check if object's bounding box intersects with selection rectangle
387 // Object is selected if ANY part of it is within the rectangle
388 int obj_min_x = obj_x;
389 int obj_max_x = obj_x + obj_width - 1;
390 int obj_min_y = obj_y;
391 int obj_max_y = obj_y + obj_height - 1;
392
393 // Rectangle intersection test
394 bool x_overlap = (obj_min_x <= max_x) && (obj_max_x >= min_x);
395 bool y_overlap = (obj_min_y <= max_y) && (obj_max_y >= min_y);
396
397 return x_overlap && y_overlap;
398}
399
401 // If no layer filter is active, all objects pass
403 return true;
404 }
405
406 // Mask mode: only allow BG2/Layer 1 objects (overlay content like platforms)
408 return object.GetLayerValue() == kLayer2; // Layer 1 = BG2 = overlay
409 }
410
411 // Check if the object's layer matches the filter
412 return object.GetLayerValue() == static_cast<uint8_t>(active_layer_filter_);
413}
414
415} // namespace yaze::editor
bool IsObjectInRectangle(const zelda3::RoomObject &object, int min_x, int min_y, int max_x, int max_y) const
std::tuple< int, int, int, int > GetRectangleSelectionBounds() const
Get rectangle selection bounds in canvas coordinates.
void UpdateRectangleSelection(int canvas_x, int canvas_y)
Update rectangle selection endpoint.
static std::pair< int, int > RoomToCanvasCoordinates(int room_x, int room_y)
Convert room tile coordinates to canvas pixel coordinates.
void DrawSelectionHighlights(gui::Canvas *canvas, const std::vector< zelda3::RoomObject > &objects, std::function< std::tuple< int, int, int, int >(const zelda3::RoomObject &)> bounds_calculator)
Draw selection highlights for all selected objects.
std::function< void()> selection_changed_callback_
static constexpr int kMaskLayer
bool IsObjectSelected(size_t index) const
Check if an object is selected.
std::set< size_t > selected_indices_
std::vector< size_t > GetSelectedIndices() const
Get all selected object indices.
static std::tuple< int, int, int, int > GetObjectBounds(const zelda3::RoomObject &object)
Calculate the bounding box of an object.
bool IsRectangleLargeEnough(int min_pixels) const
Check if rectangle selection exceeds a minimum pixel size.
void SelectAll(size_t object_count)
Select all objects in the current room.
static std::pair< int, int > CanvasToRoomCoordinates(int canvas_x, int canvas_y)
Convert canvas pixel coordinates to room tile coordinates.
void SelectObject(size_t index, SelectionMode mode=SelectionMode::Single)
Select a single object by index.
void ClearSelection()
Clear all selections.
void EndRectangleSelection(const std::vector< zelda3::RoomObject > &objects, SelectionMode mode=SelectionMode::Single)
Complete rectangle selection operation.
void BeginRectangleSelection(int canvas_x, int canvas_y)
Begin a rectangle selection operation.
bool PassesLayerFilter(const zelda3::RoomObject &object) const
ImVec4 GetLayerTypeColor(const zelda3::RoomObject &object) const
Get selection highlight color based on object layer and type.
static constexpr int kLayerAll
void CancelRectangleSelection()
Cancel rectangle selection without modifying selection.
std::optional< size_t > GetPrimarySelection() const
Get the primary selected object (first in selection)
void DrawRectangleSelectionBox(gui::Canvas *canvas)
Draw the active rectangle selection box.
void SelectObjectsInRect(int room_min_x, int room_min_y, int room_max_x, int room_max_y, const std::vector< zelda3::RoomObject > &objects, SelectionMode mode=SelectionMode::Single)
Select multiple objects within a rectangle.
Modern, robust canvas for drawing and manipulating graphics.
Definition canvas.h:150
auto global_scale() const
Definition canvas.h:491
auto zero_point() const
Definition canvas.h:443
static DimensionService & Get()
std::tuple< int, int, int, int > GetHitTestBounds(const RoomObject &obj) const
#define LOG_ERROR(category, format,...)
Definition log.h:109
const AgentUITheme & GetTheme()
Editors are the view controllers for the application.