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"
9#include "imgui/imgui.h"
10#include "util/log.h"
12
13namespace yaze::editor {
14
15// ============================================================================
16// Selection Operations
17// ============================================================================
18
20 switch (mode) {
22 // Replace entire selection with single object
23 selected_indices_.clear();
24 selected_indices_.insert(index);
25 break;
26
28 // Add to existing selection (Shift+click)
29 selected_indices_.insert(index);
30 break;
31
33 // Toggle object in selection (Ctrl+click)
34 if (selected_indices_.count(index)) {
35 selected_indices_.erase(index);
36 } else {
37 selected_indices_.insert(index);
38 }
39 break;
40
42 // This shouldn't be used for single object selection
43 LOG_ERROR("ObjectSelection",
44 "Rectangle mode used for single object selection");
45 selected_indices_.insert(index);
46 break;
47 }
48
50}
51
53 int room_min_x, int room_min_y, int room_max_x, int room_max_y,
54 const std::vector<zelda3::RoomObject>& objects, SelectionMode mode) {
55 // Normalize rectangle bounds
56 int min_x = std::min(room_min_x, room_max_x);
57 int max_x = std::max(room_min_x, room_max_x);
58 int min_y = std::min(room_min_y, room_max_y);
59 int max_y = std::max(room_min_y, room_max_y);
60
61 // For Single mode, clear previous selection first
62 if (mode == SelectionMode::Single) {
63 selected_indices_.clear();
64 }
65
66 // Find all objects within rectangle
67 for (size_t i = 0; i < objects.size(); ++i) {
68 if (IsObjectInRectangle(objects[i], min_x, min_y, max_x, max_y)) {
69 if (mode == SelectionMode::Toggle) {
70 // Toggle each object
71 if (selected_indices_.count(i)) {
72 selected_indices_.erase(i);
73 } else {
74 selected_indices_.insert(i);
75 }
76 } else {
77 // Add or Replace mode - just add
78 selected_indices_.insert(i);
79 }
80 }
81 }
82
84}
85
86void ObjectSelection::SelectAll(size_t object_count) {
87 selected_indices_.clear();
88 for (size_t i = 0; i < object_count; ++i) {
89 selected_indices_.insert(i);
90 }
92}
93
95 const std::vector<zelda3::RoomObject>& objects) {
96 selected_indices_.clear();
97 for (size_t i = 0; i < objects.size(); ++i) {
98 // Only select objects that pass the layer filter
99 if (PassesLayerFilter(objects[i])) {
100 selected_indices_.insert(i);
101 }
102 }
104}
105
107 if (selected_indices_.empty()) {
108 return; // No change
109 }
110
111 selected_indices_.clear();
113}
114
115bool ObjectSelection::IsObjectSelected(size_t index) const {
116 return selected_indices_.count(index) > 0;
117}
118
119std::vector<size_t> ObjectSelection::GetSelectedIndices() const {
120 // Safely convert set to vector with bounds checking
121 std::vector<size_t> result;
122 result.reserve(selected_indices_.size());
123 for (size_t idx : selected_indices_) {
124 result.push_back(idx);
125 }
126 return result;
127}
128
129std::optional<size_t> ObjectSelection::GetPrimarySelection() const {
130 if (selected_indices_.empty()) {
131 return std::nullopt;
132 }
133 return *selected_indices_.begin(); // First element (lowest index)
134}
135
136// ============================================================================
137// Rectangle Selection State
138// ============================================================================
139
140void ObjectSelection::BeginRectangleSelection(int canvas_x, int canvas_y) {
142 rect_start_x_ = canvas_x;
143 rect_start_y_ = canvas_y;
144 rect_end_x_ = canvas_x;
145 rect_end_y_ = canvas_y;
146}
147
148void ObjectSelection::UpdateRectangleSelection(int canvas_x, int canvas_y) {
150 LOG_ERROR("ObjectSelection",
151 "UpdateRectangleSelection called when not active");
152 return;
153 }
154
155 rect_end_x_ = canvas_x;
156 rect_end_y_ = canvas_y;
157}
158
160 const std::vector<zelda3::RoomObject>& objects, SelectionMode mode,
161 std::function<bool(const zelda3::RoomObject&)> is_object_visible) {
163 LOG_ERROR("ObjectSelection",
164 "EndRectangleSelection called when not active");
165 return;
166 }
167
168 // Convert canvas coordinates to room coordinates
169 auto [start_room_x, start_room_y] =
171 auto [end_room_x, end_room_y] =
173
174 // Visibility is view state, while PassesLayerFilter is the user's explicit
175 // stored-stream selection filter. Apply both without rewriting either one.
176 if (!is_object_visible) {
177 SelectObjectsInRect(start_room_x, start_room_y, end_room_x, end_room_y,
178 objects, mode);
179 } else {
180 if (mode == SelectionMode::Single) {
181 selected_indices_.clear();
182 }
183 const int min_x = std::min(start_room_x, end_room_x);
184 const int max_x = std::max(start_room_x, end_room_x);
185 const int min_y = std::min(start_room_y, end_room_y);
186 const int max_y = std::max(start_room_y, end_room_y);
187 for (size_t index = 0; index < objects.size(); ++index) {
188 if (!is_object_visible(objects[index]) ||
189 !IsObjectInRectangle(objects[index], min_x, min_y, max_x, max_y)) {
190 continue;
191 }
192 if (mode == SelectionMode::Toggle && selected_indices_.contains(index)) {
193 selected_indices_.erase(index);
194 } else {
195 selected_indices_.insert(index);
196 }
197 }
199 }
200
202}
203
207
208std::tuple<int, int, int, int> ObjectSelection::GetRectangleSelectionBounds()
209 const {
210 int min_x = std::min(rect_start_x_, rect_end_x_);
211 int max_x = std::max(rect_start_x_, rect_end_x_);
212 int min_y = std::min(rect_start_y_, rect_end_y_);
213 int max_y = std::max(rect_start_y_, rect_end_y_);
214 return {min_x, min_y, max_x, max_y};
215}
216
217bool ObjectSelection::IsRectangleLargeEnough(int min_pixels) const {
219 return false;
220 }
221
222 auto [min_x, min_y, max_x, max_y] = GetRectangleSelectionBounds();
223 const int width = std::abs(max_x - min_x);
224 const int height = std::abs(max_y - min_y);
225 return (width >= min_pixels) || (height >= min_pixels);
226}
227
228// ============================================================================
229// Visual Rendering
230// ============================================================================
231
233 gui::Canvas* canvas, const std::vector<zelda3::RoomObject>& objects,
234 std::function<std::tuple<int, int, int, int>(const zelda3::RoomObject&)>
235 bounds_calculator) {
236 if (selected_indices_.empty() || !canvas) {
237 return;
238 }
239
240 ImDrawList* draw_list = ImGui::GetWindowDrawList();
241 const DungeonCanvasTransform transform(
242 canvas->zero_point(), canvas->scrolling(), canvas->global_scale());
243 const float scale = transform.scale();
244
245 for (size_t index : selected_indices_) {
246 if (index >= objects.size()) {
247 continue;
248 }
249
250 const auto& object = objects[index];
251
252 // Calculate object position in canvas coordinates
253 auto [obj_x, obj_y] = RoomToCanvasCoordinates(object.x_, object.y_);
254
255 int offset_x = 0;
256 int offset_y = 0;
257 int pixel_width = 0;
258 int pixel_height = 0;
259 if (bounds_calculator) {
260 auto [dx, dy, w, h] = bounds_calculator(object);
261 offset_x = dx;
262 offset_y = dy;
263 pixel_width = w;
264 pixel_height = h;
265 } else {
266 // Fallback to old logic if no calculator provided
267 auto [tile_x, tile_y, tile_width, tile_height] = GetObjectBounds(object);
268 offset_x = (tile_x - object.x_) * 8;
269 offset_y = (tile_y - object.y_) * 8;
270 pixel_width = tile_width * 8;
271 pixel_height = tile_height * 8;
272 }
273
274 obj_x += offset_x;
275 obj_y += offset_y;
276
277 ImVec2 obj_start = transform.RoomPixelsToScreen(
278 ImVec2(static_cast<float>(obj_x), static_cast<float>(obj_y)));
279 const ImVec2 obj_size = transform.RoomSizeToScreen(ImVec2(
280 static_cast<float>(pixel_width), static_cast<float>(pixel_height)));
281 ImVec2 obj_end(obj_start.x + obj_size.x, obj_start.y + obj_size.y);
282
283 // Expand selection box slightly for visibility
284 constexpr float margin = 2.0f;
285 obj_start.x -= margin;
286 obj_start.y -= margin;
287 obj_end.x += margin;
288 obj_end.y += margin;
289
290 // Get color based on theme
291 const auto& theme = AgentUI::GetTheme();
292
293 // Draw pulsing animated border
294 float pulse =
295 0.6f + 0.4f * std::sin(static_cast<float>(ImGui::GetTime()) * 6.0f);
296 ImVec4 pulsing_color = theme.selection_primary;
297 pulsing_color.w = 0.4f + 0.4f * pulse;
298
299 ImU32 border_color = ImGui::GetColorU32(theme.selection_primary);
300 ImU32 pulse_color_u32 = ImGui::GetColorU32(pulsing_color);
301
302 draw_list->AddRect(obj_start, obj_end, border_color, 0.0f, 0, 2.0f);
303 draw_list->AddRect(obj_start, obj_end, pulse_color_u32, 0.0f, 0, 4.0f);
304
305 // Draw corner handles with theme handle color
306 float handle_size = 6.0f * std::max(1.0f, scale * 0.5f);
307 ImU32 handle_color = ImGui::GetColorU32(theme.selection_handle);
308 ImU32 handle_outline = ImGui::GetColorU32(ImVec4(0, 0, 0, 0.8f));
309
310 auto draw_handle = [&](ImVec2 pos) {
311 draw_list->AddRectFilled(
312 ImVec2(pos.x - handle_size / 2, pos.y - handle_size / 2),
313 ImVec2(pos.x + handle_size / 2, pos.y + handle_size / 2),
314 handle_color, 2.0f);
315 draw_list->AddRect(
316 ImVec2(pos.x - handle_size / 2, pos.y - handle_size / 2),
317 ImVec2(pos.x + handle_size / 2, pos.y + handle_size / 2),
318 handle_outline, 2.0f, 0, 1.0f);
319 };
320
321 draw_handle(obj_start);
322 draw_handle(ImVec2(obj_end.x, obj_start.y));
323 draw_handle(ImVec2(obj_start.x, obj_end.y));
324 draw_handle(obj_end);
325 }
326}
327
329 const zelda3::RoomObject& object) const {
330 const auto& theme = AgentUI::GetTheme();
331 int layer = object.GetLayerValue();
332
333 switch (layer) {
334 case 0:
335 return theme.dungeon_outline_layer0;
336 case 1:
337 return theme.dungeon_outline_layer1;
338 case 2:
339 return theme.dungeon_outline_layer2;
340 default:
341 return theme.status_inactive;
342 }
343}
344
346 if (!rectangle_selection_active_ || !canvas) {
347 return;
348 }
349
350 const auto& theme = AgentUI::GetTheme();
351 ImDrawList* draw_list = ImGui::GetWindowDrawList();
352 const DungeonCanvasTransform transform(
353 canvas->zero_point(), canvas->scrolling(), canvas->global_scale());
354
355 // Get normalized bounds
356 auto [min_x, min_y, max_x, max_y] = GetRectangleSelectionBounds();
357
358 const ImVec2 box_start = transform.RoomPixelsToScreen(
359 ImVec2(static_cast<float>(min_x), static_cast<float>(min_y)));
360 const ImVec2 box_end = transform.RoomPixelsToScreen(
361 ImVec2(static_cast<float>(max_x), static_cast<float>(max_y)));
362
363 // Draw selection box with theme selection color
364 // Border: High-contrast at 0.85f alpha
365 ImU32 border_color = ImGui::ColorConvertFloat4ToU32(
366 ImVec4(theme.selection_secondary.x, theme.selection_secondary.y,
367 theme.selection_secondary.z, 0.85f));
368 // Fill: Subtle at 0.15f alpha
369 ImU32 fill_color = ImGui::ColorConvertFloat4ToU32(
370 ImVec4(theme.selection_secondary.x, theme.selection_secondary.y,
371 theme.selection_secondary.z, 0.15f));
372
373 draw_list->AddRectFilled(box_start, box_end, fill_color);
374 draw_list->AddRect(box_start, box_end, border_color, 0.0f, 0, 2.0f);
375}
376
377// ============================================================================
378// Utility Functions
379// ============================================================================
380
381std::pair<int, int> ObjectSelection::RoomToCanvasCoordinates(int room_x,
382 int room_y) {
383 // Dungeon tiles are 8x8 pixels
384 return {room_x * 8, room_y * 8};
385}
386
387std::pair<int, int> ObjectSelection::CanvasToRoomCoordinates(int canvas_x,
388 int canvas_y) {
389 // Convert pixels back to tiles (round down)
390 return {canvas_x / 8, canvas_y / 8};
391}
392
393std::tuple<int, int, int, int> ObjectSelection::GetObjectBounds(
394 const zelda3::RoomObject& object) {
396}
397
398// ============================================================================
399// Private Helper Functions
400// ============================================================================
401
407
409 int min_x, int min_y, int max_x,
410 int max_y) const {
411 // Check layer filter first
412 if (!PassesLayerFilter(object)) {
413 return false;
414 }
415
416 // Get object bounds
417 auto [obj_x, obj_y, obj_width, obj_height] = GetObjectBounds(object);
418
419 // Check if object's bounding box intersects with selection rectangle
420 // Object is selected if ANY part of it is within the rectangle
421 int obj_min_x = obj_x;
422 int obj_max_x = obj_x + obj_width - 1;
423 int obj_min_y = obj_y;
424 int obj_max_y = obj_y + obj_height - 1;
425
426 // Rectangle intersection test
427 bool x_overlap = (obj_min_x <= max_x) && (obj_max_x >= min_x);
428 bool y_overlap = (obj_min_y <= max_y) && (obj_max_y >= min_y);
429
430 return x_overlap && y_overlap;
431}
432
434 const zelda3::RoomObject& object) const {
435 // If no layer filter is active, all objects pass
437 return true;
438 }
439
440 // Mask mode: only allow BG2/Layer 1 objects (overlay content like platforms)
442 return object.GetLayerValue() == kLayer2; // Layer 1 = BG2 = overlay
443 }
444
445 // Check if the object's layer matches the filter
446 return object.GetLayerValue() == static_cast<uint8_t>(active_layer_filter_);
447}
448
449} // namespace yaze::editor
ImVec2 RoomSizeToScreen(ImVec2 room_size) const
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_
void EndRectangleSelection(const std::vector< zelda3::RoomObject > &objects, SelectionMode mode=SelectionMode::Single, std::function< bool(const zelda3::RoomObject &)> is_object_visible={})
Complete rectangle selection operation.
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 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:64
auto global_scale() const
Definition canvas.h:403
auto zero_point() const
Definition canvas.h:354
auto scrolling() const
Definition canvas.h:356
static DimensionService & Get()
std::tuple< int, int, int, int > GetHitTestBounds(const RoomObject &obj) const
#define LOG_ERROR(category, format,...)
Definition log.h:110
const AgentUITheme & GetTheme()
Editors are the view controllers for the application.