13#include "absl/strings/str_format.h"
14#include "imgui/imgui.h"
30bool Intersects(
int ax,
int ay,
int aw,
int ah,
int bx,
int by,
int bw,
32 return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
37 switch (entity.
type) {
40 if (entity.
index >= doors.size()) {
43 return doors[entity.
index].GetEditorBounds();
47 if (entity.
index >= sprites.size()) {
50 const auto& sprite = sprites[entity.
index];
52 return std::make_tuple(sprite.x() * kSize, sprite.y() * kSize, kSize,
57 if (entity.
index >= items.size()) {
60 const auto& item = items[entity.
index];
61 return std::make_tuple(item.GetPixelX(), item.GetPixelY(), 16, 16);
86 return io.KeyAlt && (io.KeyCtrl || io.KeySuper);
176 const ImGuiIO& io = ImGui::GetIO();
177 const bool cycle_modifier = IsCycleModifierHeld(io);
178 if (io.KeyAlt && !cycle_modifier) {
180 const bool had_object_selection =
189 if ((had_entity_selection || had_object_selection) &&
ctx_) {
194 if (cycle_modifier) {
210 const auto entity = hits.front();
211 const bool additive = io.KeyShift || io.KeyCtrl || io.KeySuper;
221 const bool has_multi_object_selection =
223 if (!additive && hit_already_selected &&
236 const bool toggle = io.KeyCtrl || io.KeySuper;
254 switch (entity.type) {
298 std::vector<SelectedEntity> entities) {
304 for (
const auto entity : entities) {
317 switch (selected.
type) {
356 if (had_selection &&
ctx_) {
369 int canvas_x,
int canvas_y)
const {
378 int canvas_x,
int canvas_y)
const {
379 std::vector<SelectedEntity> hits;
558 int delta_x,
int delta_y,
bool defer_drag_notifications) {
564 bool doors_changed =
false;
565 bool sprites_changed =
false;
566 bool items_changed =
false;
567 bool door_mutation_notified =
false;
568 bool sprite_mutation_notified =
false;
569 bool item_mutation_notified =
false;
571 auto notify_once = [&](
MutationDomain domain,
bool& immediate_flag,
576 if (defer_drag_notifications) {
583 if (!immediate_flag) {
585 immediate_flag =
true;
590 switch (entity.type) {
592 auto& doors = room->GetDoors();
593 if (entity.index >= doors.size()) {
596 auto& door = doors[entity.index];
597 int position_delta = 0;
598 switch (door.direction) {
601 position_delta = delta_x;
605 position_delta = delta_y;
608 if (position_delta == 0) {
611 const int next_position =
612 std::clamp(
static_cast<int>(door.position) + position_delta, 0,
614 if (next_position == door.position ||
616 static_cast<uint8_t
>(next_position), door.direction)) {
621 door.position =
static_cast<uint8_t
>(next_position);
622 auto [b1, b2] = door.EncodeBytes();
625 doors_changed =
true;
629 auto& sprites = room->GetSprites();
630 if (entity.index >= sprites.size()) {
633 auto& sprite = sprites[entity.index];
634 const int next_x = std::clamp(
static_cast<int>(sprite.x()) + delta_x, 0,
636 const int next_y = std::clamp(
static_cast<int>(sprite.y()) + delta_y, 0,
638 if (next_x == sprite.x() && next_y == sprite.y()) {
643 sprite.set_x(next_x);
644 sprite.set_y(next_y);
645 sprites_changed =
true;
649 auto& items = room->GetPotItems();
650 if (entity.index >= items.size()) {
653 auto& item = items[entity.index];
654 constexpr int kRoomPixelMax = 511;
655 constexpr int kItemHorizontalNudgePixels = 8;
656 constexpr int kItemVerticalNudgePixels = 16;
657 const int next_pixel_x =
658 std::clamp(item.GetPixelX() + delta_x * kItemHorizontalNudgePixels,
660 const int next_pixel_y =
661 std::clamp(item.GetPixelY() + delta_y * kItemVerticalNudgePixels, 0,
663 const int encoded_x = std::clamp(next_pixel_x / 4, 0, 255);
664 const int encoded_y = std::clamp(next_pixel_y / 16, 0, 255);
665 const uint16_t next_position =
666 static_cast<uint16_t
>((encoded_y << 8) | encoded_x);
667 if (next_position == item.position) {
672 item.position = next_position;
673 items_changed =
true;
683 if (!doors_changed && !sprites_changed && !items_changed) {
688 room->MarkObjectsDirty();
689 if (defer_drag_notifications) {
695 if (sprites_changed) {
696 room->MarkSpritesDirty();
697 if (defer_drag_notifications) {
704 room->MarkPotItemsDirty();
705 if (defer_drag_notifications) {
711 if (!defer_drag_notifications) {
730 constexpr int kItemHorizontalNudgePixels = 8;
731 constexpr int kItemVerticalNudgePixels = 16;
733 delta_y * kItemVerticalNudgePixels);
758 std::vector<size_t> doors;
759 std::vector<size_t> sprites;
760 std::vector<size_t> items;
762 switch (entity.type) {
764 if (entity.index < room->GetDoors().size()) {
765 doors.push_back(entity.index);
769 if (entity.index < room->GetSprites().size()) {
770 sprites.push_back(entity.index);
774 if (entity.index < room->GetPotItems().size()) {
775 items.push_back(entity.index);
785 auto sort_unique_desc = [](std::vector<size_t>& indices) {
786 std::sort(indices.begin(), indices.end(), std::greater<size_t>());
787 indices.erase(std::unique(indices.begin(), indices.end()), indices.end());
789 sort_unique_desc(doors);
790 sort_unique_desc(sprites);
791 sort_unique_desc(items);
793 if (!doors.empty()) {
795 auto& room_doors = room->GetDoors();
796 for (
size_t index : doors) {
797 room_doors.erase(room_doors.begin() +
static_cast<ptrdiff_t
>(index));
799 room->MarkObjectStreamDirty();
802 if (!sprites.empty()) {
804 auto& room_sprites = room->GetSprites();
805 for (
size_t index : sprites) {
806 room_sprites.erase(room_sprites.begin() +
807 static_cast<ptrdiff_t
>(index));
809 room->MarkSpritesDirty();
812 if (!items.empty()) {
814 auto& room_items = room->GetPotItems();
815 for (
size_t index : items) {
816 room_items.erase(room_items.begin() +
static_cast<ptrdiff_t
>(index));
818 room->MarkPotItemsDirty();
822 if (!doors.empty() || !sprites.empty() || !items.empty()) {
897 switch (entity.
type) {
918 bool additive,
bool toggle) {
927 if (!additive && !toggle) {
931 const auto existing =
945 switch (selected.type) {
979 const bool has_object_selection =
1010 const ImVec2 drag_delta(
1015 const int drag_dx =
static_cast<int>(drag_delta.x) / kEntityDragStepPixels;
1016 const int drag_dy =
static_cast<int>(drag_delta.y) / kEntityDragStepPixels;
1019 if (inc_dx == 0 && inc_dy == 0) {
1031 const std::tuple<int, int, int, int>& bounds,
bool additive,
bool toggle) {
1037 const auto [raw_min_x, raw_min_y, raw_max_x, raw_max_y] = bounds;
1038 const int min_x = std::min(raw_min_x, raw_max_x);
1039 const int max_x = std::max(raw_min_x, raw_max_x);
1040 const int min_y = std::min(raw_min_y, raw_max_y);
1041 const int max_y = std::max(raw_min_y, raw_max_y);
1042 const int rect_w = std::max(1, max_x - min_x);
1043 const int rect_h = std::max(1, max_y - min_y);
1045 std::vector<SelectedEntity> hits;
1046 for (
size_t i = 0; i < room->GetDoors().size(); ++i) {
1048 if (
auto entity_bounds = GetEntityBounds(*room, entity)) {
1049 auto [x, y, w, h] = *entity_bounds;
1050 if (Intersects(x, y, w, h, min_x, min_y, rect_w, rect_h)) {
1051 hits.push_back(entity);
1055 for (
size_t i = 0; i < room->GetSprites().size(); ++i) {
1057 if (
auto entity_bounds = GetEntityBounds(*room, entity)) {
1058 auto [x, y, w, h] = *entity_bounds;
1059 if (Intersects(x, y, w, h, min_x, min_y, rect_w, rect_h)) {
1060 hits.push_back(entity);
1064 for (
size_t i = 0; i < room->GetPotItems().size(); ++i) {
1066 if (
auto entity_bounds = GetEntityBounds(*room, entity)) {
1067 auto [x, y, w, h] = *entity_bounds;
1068 if (Intersects(x, y, w, h, min_x, min_y, rect_w, rect_h)) {
1069 hits.push_back(entity);
1077 if (!additive && !toggle) {
1081 for (
const auto entity : hits) {
1104 int canvas_x,
int canvas_y,
const std::vector<SelectedEntity>& hits)
const {
1105 constexpr int kSameSpotTolerancePx = 3;
1106 if (std::abs(canvas_x -
cycle_last_x_) > kSameSpotTolerancePx ||
1107 std::abs(canvas_y -
cycle_last_y_) > kSameSpotTolerancePx ||
1112 for (
size_t i = 0; i < hits.size(); ++i) {
1121 const std::vector<SelectedEntity>& hits)
const {
1122 for (
size_t i = 0; i < hits.size(); ++i) {
1138 if (selected == hit) {
1143 return std::nullopt;
1151 const ImGuiIO& io = ImGui::GetIO();
1152 if (!IsCycleModifierHeld(io)) {
1159 const auto [canvas_x, canvas_y] =
1162 if (hits.size() < 2) {
1186 if (ImGui::GetCurrentContext()) {
1196 const ImGuiIO& io = ImGui::GetIO();
1197 const bool cycle_modifier_held = IsCycleModifierHeld(io);
1198 if (!cycle_modifier_held && elapsed > kCycleHudHoldSeconds) {
1206 1.0f -
static_cast<float>(elapsed / kCycleHudHoldSeconds));
1208 ImVec4 bg = theme.panel_bg_darker;
1209 bg.w *= 0.92f * alpha;
1210 ImVec4 border = theme.panel_border_color;
1212 ImVec4 text = theme.text_primary;
1214 ImVec4 secondary = theme.text_secondary_color;
1215 secondary.w *= alpha;
1216 ImVec4 active = theme.accent_color;
1219 ImGui::SetNextWindowPos(
1222 ImGui::SetNextWindowBgAlpha(bg.w);
1223 ImGui::PushStyleColor(ImGuiCol_WindowBg, bg);
1224 ImGui::PushStyleColor(ImGuiCol_Border, border);
1225 ImGui::PushStyleColor(ImGuiCol_Text, text);
1226 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.0f, 6.0f));
1227 ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
1228 constexpr ImGuiWindowFlags kFlags =
1229 ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings |
1230 ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoNav |
1231 ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoInputs;
1232 if (ImGui::Begin(
"##DungeonSelectionCycleHud",
nullptr, kFlags)) {
1233 ImGui::TextColored(secondary, tr(
"Cycle"));
1238 ImGui::SameLine(0.0f, 5.0f);
1239 ImGui::TextUnformatted(
1244 ImGui::PopStyleVar(2);
1245 ImGui::PopStyleColor(3);
1255 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1259 const float scale = transform.
scale();
1261 0.6f + 0.4f * std::sin(
static_cast<float>(ImGui::GetTime()) * 6.0f);
1265 const auto bounds = GetEntityBounds(*room, entity);
1266 if (!bounds.has_value()) {
1270 auto [x, y, w, h] = *bounds;
1272 ImVec2(
static_cast<float>(x),
static_cast<float>(y)));
1274 ImVec2(
static_cast<float>(w),
static_cast<float>(h)));
1275 ImVec2 end(start.x + size.x, start.y + size.y);
1276 constexpr float kMargin = 2.0f;
1282 ImVec4 base = EntitySelectionColor(theme, entity.type);
1284 fill.w = 0.14f + 0.10f * pulse;
1285 ImVec4 border = base;
1286 border.w = (i == 0) ? 0.95f : 0.72f;
1288 draw_list->AddRectFilled(start, end, ImGui::GetColorU32(fill));
1289 draw_list->AddRect(start, end, ImGui::GetColorU32(border), 0.0f, 0,
1290 (i == 0) ? 2.2f : 1.6f);
1291 draw_list->AddText(ImVec2(start.x, start.y - 14.0f * scale),
1292 ImGui::GetColorU32(theme.text_primary),
1300 switch (entity.
type) {
1304 const std::string direction_name(
1306 return absl::StrFormat(
"Door (%s)", direction_name);
1313 return absl::StrFormat(
"Sprite (0x%02X - %s)", sprite.id(),
1320 return absl::StrFormat(
"Item (0x%02X)", item.item);
1326 return absl::StrFormat(
"Object (0x%03X - %s)",
object.id_,
Abstract base class for entity interaction handlers.
void DrawPostPlacementToast()
virtual bool HandleMouseWheel(float delta)
void SetContext(InteractionContext *ctx)
Set the interaction context.
bool HandleOverlayClick(int canvas_x, int canvas_y)
bool NudgeSelected(int delta_x, int delta_y)
void DrawSelectionHighlight() override
Draw selection highlight for selected entities.
void DrawSnapIndicators()
Draw snap position indicators during door drag.
void SelectDoor(size_t index)
Select door at index.
void DrawGhostPreview() override
Draw ghost preview during placement.
void ClearSelection()
Clear door selection.
void HandleDrag(ImVec2 current_pos, ImVec2 delta) override
Handle mouse drag.
void DeleteSelected()
Delete selected door.
void CancelPlacement() override
Cancel current placement.
void BeginPlacement() override
Begin placement mode.
void HandleRelease() override
Handle mouse release.
bool HandleClick(int canvas_x, int canvas_y) override
Handle mouse click at canvas position.
std::optional< size_t > GetSelectedIndex() const
Get selected door index.
bool IsPlacementActive() const override
Check if placement mode is active.
std::optional< size_t > GetEntityAtPosition(int canvas_x, int canvas_y) const override
Get entity at canvas position.
bool HasSelection() const
Check if a door is selected.
bool ApplySelection(SelectedEntity entity)
std::vector< SelectedEntity > GetEntitiesAtPosition(int canvas_x, int canvas_y) const
bool IsSelectionHitSelected(SelectedEntity entity) const
bool entity_group_drag_active_
bool entity_group_drag_sprites_changed_
SelectedEntity GetSelectedEntity() const
void DrawSelectionCycleHud()
bool TrySelectEntityAtCursor(int canvas_x, int canvas_y)
Try to select entity at cursor position.
void UpdateSelectionCycleHudPreview()
bool HasGroupDragSelection() const
ImVec2 entity_group_drag_start_
void HandleRelease()
Handle mouse release.
bool entity_group_drag_doors_changed_
Mode GetSelectedEntityType() const
Get the type of currently selected entity.
int entity_group_drag_last_dx_
std::string DescribeCycleHudEntity(SelectedEntity entity) const
InteractionContext * ctx_
ItemInteractionHandler item_handler_
void SelectEntitiesInRect(const std::tuple< int, int, int, int > &bounds, bool additive, bool toggle)
void ClearEntitySelection()
void CancelCurrentMode()
Cancel current mode and return to select mode.
void SetContext(InteractionContext *ctx)
Set the shared interaction context.
void FinishEntityGroupDrag()
void BeginSelectionDrag(ImVec2 start_pos)
std::vector< SelectedEntity > selected_entities_
bool UpdateEntitySelection(SelectedEntity entity, bool additive, bool toggle)
std::optional< size_t > FindSelectedCycleIndex(const std::vector< SelectedEntity > &hits) const
BaseEntityHandler * GetActiveHandler()
Get active handler based on current mode.
void DeleteSelectedEntity()
Delete currently selected entity.
ImVec2 entity_group_drag_current_
bool IsPlacementActive() const
Check if any placement mode is active.
bool HandleClick(int canvas_x, int canvas_y)
Handle click at canvas position.
bool entity_group_drag_items_mutation_started_
void SetSelectedEntities(std::vector< SelectedEntity > entities)
std::string DescribeEntity(SelectedEntity entity) const
size_t cycle_active_index_
DoorInteractionHandler door_handler_
void ResetEntityGroupDragState()
void SelectEntity(EntityType type, size_t index)
void DrawMultiEntitySelectionHighlights()
int entity_group_drag_last_dy_
double cycle_hud_start_time_
void DrawPostPlacementOverlays()
Draw post-placement success toasts for all handlers (unconditional)
ImVec2 cycle_hud_screen_pos_
bool NudgeSelected(int delta_x, int delta_y)
bool HandleMouseWheel(float delta)
TileObjectHandler tile_handler_
void HandleEntityGroupDrag(ImVec2 current_pos)
void DrawSelectionHighlights()
Draw selection highlights for all entity types.
bool entity_group_drag_sprites_mutation_started_
void SetMode(Mode mode)
Set interaction mode.
Mode
Available interaction modes.
void ClearAllEntitySelections()
Clear all entity selections.
bool entity_group_drag_doors_mutation_started_
bool entity_group_drag_items_changed_
void DrawGhostPreviews()
Draw ghost previews for active placement mode.
bool SameCycleTarget(int canvas_x, int canvas_y, const std::vector< SelectedEntity > &hits) const
bool HasEntitySelection() const
bool NudgeSelectedEntities(int delta_x, int delta_y, bool defer_drag_notifications)
SpriteInteractionHandler sprite_handler_
std::vector< SelectedEntity > cycle_last_hits_
std::optional< SelectedEntity > GetEntityAtPosition(int canvas_x, int canvas_y) const
void HandleDrag(ImVec2 current_pos, ImVec2 delta)
Handle drag operation.
void DrawSelectionHighlight() override
Draw selection highlight for selected entities.
bool HandleClick(int canvas_x, int canvas_y) override
Handle mouse click at canvas position.
bool IsPlacementActive() const override
Check if placement mode is active.
void DrawGhostPreview() override
Draw ghost preview during placement.
void BeginPlacement() override
Begin placement mode.
void HandleDrag(ImVec2 current_pos, ImVec2 delta) override
Handle mouse drag.
void ClearSelection()
Clear item selection.
std::optional< size_t > GetEntityAtPosition(int canvas_x, int canvas_y) const override
Get entity at canvas position.
void DeleteSelected()
Delete selected item.
bool HasSelection() const
Check if an item is selected.
bool NudgeSelected(int delta_pixel_x, int delta_pixel_y)
void HandleRelease() override
Handle mouse release.
std::optional< size_t > GetSelectedIndex() const
Get selected item index.
void SelectItem(size_t index)
Select item at index.
void CancelPlacement() override
Cancel current placement.
bool IsObjectSelected(size_t index) const
Check if an object is selected.
size_t GetSelectionCount() const
Get the number of selected objects.
void SelectObject(size_t index, SelectionMode mode=SelectionMode::Single)
Select a single object by index.
void ClearSelection()
Clear all selections.
bool HasSelection() const
Check if any objects are selected.
bool IsPlacementActive() const override
Check if placement mode is active.
bool NudgeSelected(int delta_x, int delta_y)
void ClearSelection()
Clear sprite selection.
void CancelPlacement() override
Cancel current placement.
void HandleRelease() override
Handle mouse release.
void SelectSprite(size_t index)
Select sprite at index.
bool HasSelection() const
Check if a sprite is selected.
std::optional< size_t > GetEntityAtPosition(int canvas_x, int canvas_y) const override
Get entity at canvas position.
void DrawGhostPreview() override
Draw ghost preview during placement.
void DrawSelectionHighlight() override
Draw selection highlight for selected entities.
void HandleDrag(ImVec2 current_pos, ImVec2 delta) override
Handle mouse drag.
bool HandleClick(int canvas_x, int canvas_y) override
Handle mouse click at canvas position.
std::optional< size_t > GetSelectedIndex() const
Get selected sprite index.
void BeginPlacement() override
Begin placement mode.
void DeleteSelected()
Delete selected sprite.
void DrawGhostPreview() override
Draw ghost preview during placement.
bool HandleMouseWheel(float delta) override
void HandleRelease() override
Handle mouse release.
void HandleDrag(ImVec2 current_pos, ImVec2 delta) override
Handle mouse drag.
bool IsPlacementActive() const override
Check if placement mode is active.
bool HandleClick(int canvas_x, int canvas_y) override
Handle mouse click at canvas position.
void CancelPlacement() override
Cancel current placement.
std::optional< size_t > GetEntityAtPosition(int canvas_x, int canvas_y) const override
Get entity at canvas position.
auto global_scale() const
bool IsMouseHovering() const
static constexpr int kMaxDoorPositions
static bool IsValidPosition(uint8_t position, DoorDirection direction)
Check if a position is valid for door placement.
const std::vector< Door > & GetDoors() const
const std::vector< zelda3::Sprite > & GetSprites() const
const std::vector< RoomObject > & GetTileObjects() const
const std::vector< PotItem > & GetPotItems() const
const AgentUITheme & GetTheme()
std::optional< std::tuple< int, int, int, int > > GetEntityBounds(const zelda3::Room &room, SelectedEntity entity)
ImVec4 EntitySelectionColor(const AgentUITheme &theme, EntityType type)
constexpr size_t kCycleHudMaxLabelChars
constexpr double kCycleHudHoldSeconds
bool Intersects(int ax, int ay, int aw, int ah, int bx, int by, int bw, int bh)
bool IsCycleModifierHeld(const ImGuiIO &io)
std::string TruncateCycleHudLabel(std::string label)
constexpr int kSpriteGridMax
bool IsWithinBounds(int canvas_x, int canvas_y, int margin=0)
Check if coordinates are within room bounds.
constexpr int kSpriteTileSize
ImVec2 SnapToTileGrid(const ImVec2 &point)
Snap a point to the 8px tile grid.
Editors are the view controllers for the application.
MutationDomain
Domain/type of mutation for undo + invalidation routing.
EntityType
Type of entity that can be selected in the dungeon editor.
constexpr std::string_view GetDoorDirectionName(DoorDirection dir)
Get human-readable name for door direction.
std::string GetObjectName(int object_id)
@ South
Bottom wall (horizontal door, 4x3 tiles)
@ North
Top wall (horizontal door, 4x3 tiles)
@ East
Right wall (vertical door, 3x4 tiles)
@ West
Left wall (vertical door, 3x4 tiles)
const char * ResolveSpriteName(uint16_t id)
Centralized theme colors for Agent UI components.
ImVec4 dungeon_selection_primary
Shared context for all interaction handlers.
const zelda3::Room * GetCurrentRoomConst() const
Get const pointer to current room.
void NotifyEntityChanged() const
Notify that entity has changed.
void NotifyInvalidateCache(MutationDomain domain=MutationDomain::kUnknown) const
Notify that cache invalidation is needed.
ObjectSelection * selection
void NotifyMutation(MutationDomain domain=MutationDomain::kUnknown) const
Notify that a mutation is about to happen.
zelda3::Room * GetCurrentRoom() const
Get pointer to current room.
Represents a selected entity in the dungeon editor.