14#include "absl/strings/str_format.h"
15#include "imgui/imgui.h"
32bool Intersects(
int ax,
int ay,
int aw,
int ah,
int bx,
int by,
int bw,
34 return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
39 switch (entity.
type) {
40 case EntityType::Door: {
42 if (entity.
index >= doors.size()) {
45 return doors[entity.
index].GetEditorBounds();
47 case EntityType::Sprite: {
49 if (entity.
index >= sprites.size()) {
52 const auto& sprite = sprites[entity.
index];
54 return std::make_tuple(sprite.x() * kSize, sprite.y() * kSize, kSize,
57 case EntityType::Item: {
59 if (entity.
index >= items.size()) {
62 const auto& item = items[entity.
index];
63 return std::make_tuple(item.GetPixelX(), item.GetPixelY(), 16, 16);
65 case EntityType::Object:
66 case EntityType::None:
74 case EntityType::Door:
76 case EntityType::Sprite:
78 case EntityType::Item:
80 case EntityType::Object:
81 case EntityType::None:
88 return io.KeyAlt && (io.KeyCtrl || io.KeySuper);
178 const ImGuiIO& io = ImGui::GetIO();
179 const bool cycle_modifier = IsCycleModifierHeld(io);
180 if (io.KeyAlt && !cycle_modifier) {
182 const bool had_object_selection =
191 if ((had_entity_selection || had_object_selection) &&
ctx_) {
196 if (cycle_modifier) {
212 const auto entity = hits.front();
213 const bool additive = io.KeyShift || io.KeyCtrl || io.KeySuper;
222 if (entity.type == EntityType::Object) {
223 const bool has_multi_object_selection =
225 if (!additive && hit_already_selected &&
238 const bool toggle = io.KeyCtrl || io.KeySuper;
256 switch (entity.type) {
257 case EntityType::Door:
259 case EntityType::Sprite:
261 case EntityType::Item:
270 if (type == EntityType::Object || type == EntityType::None) {
283 case EntityType::Door:
286 case EntityType::Sprite:
289 case EntityType::Item:
292 case EntityType::Object:
293 case EntityType::None:
300 std::vector<SelectedEntity> entities) {
306 for (
const auto entity : entities) {
307 if (entity.type != EntityType::Door && entity.type != EntityType::Sprite &&
308 entity.type != EntityType::Item) {
319 switch (selected.
type) {
320 case EntityType::Door:
323 case EntityType::Sprite:
326 case EntityType::Item:
329 case EntityType::Object:
330 case EntityType::None:
358 if (had_selection &&
ctx_) {
371 int canvas_x,
int canvas_y)
const {
380 int canvas_x,
int canvas_y)
const {
381 std::vector<SelectedEntity> hits;
548 return selected.type == EntityType::None
549 ? std::vector<SelectedEntity>{}
550 : std::vector<SelectedEntity>{selected};
555 return std::any_of(entities.begin(), entities.end(),
556 [](
const auto entity) {
557 return entity.type == EntityType::Sprite;
564 absl::Status status) {
578 "No room is available for selection editing"));
584 if (!planned->changed()) {
597 for (
const auto [mask, domain] :
602 if (planned->domains & mask)
612 for (
const auto index : planned->after.selected_objects) {
620 for (
const auto [mask, domain] :
625 if (planned->domains & mask)
642 request.
entities.front().type == EntityType::Door) {
646 const size_t index = request.
entities.front().index;
647 if (!room || index >= room->GetDoors().size()) {
650 const auto& door = room->GetDoors()[index];
653 const int64_t next_position =
654 static_cast<int64_t
>(door.position) + (horizontal ? delta_x : delta_y);
655 if (next_position == door.position)
657 if (next_position < 0 ||
660 static_cast<uint8_t
>(next_position), door.direction)) {
662 absl::OutOfRangeError(
"No door slot in that direction"));
666 next.position =
static_cast<uint8_t
>(next_position);
667 const auto [old_x, old_y] = door.GetPixelCoords();
668 const auto [new_x, new_y] = next.GetPixelCoords();
674 const int64_t dx =
static_cast<int64_t
>(delta_x) * step;
675 const int64_t dy =
static_cast<int64_t
>(delta_y) * step;
676 if (dx < -512 || dx > 512 || dy < -512 || dy > 512) {
678 absl::OutOfRangeError(
"Selection movement leaves the room"));
710 case EntityType::Door:
712 case EntityType::Sprite:
714 case EntityType::Item:
716 case EntityType::Object:
717 case EntityType::None:
751 if (entity.
type == EntityType::Object) {
765 switch (entity.
type) {
766 case EntityType::Door:
770 case EntityType::Sprite:
774 case EntityType::Item:
778 case EntityType::Object:
779 case EntityType::None:
786 bool additive,
bool toggle) {
787 if (entity.
type == EntityType::Object || entity.
type == EntityType::None) {
795 if (!additive && !toggle) {
799 const auto existing =
813 switch (selected.type) {
814 case EntityType::Door:
817 case EntityType::Sprite:
820 case EntityType::Item:
823 case EntityType::Object:
824 case EntityType::None:
837 if (entity.
type == EntityType::Object) {
847 const bool has_object_selection =
872 const ImVec2 drag_delta(
877 const int drag_dx =
static_cast<int>(drag_delta.x) / step * step;
878 const int drag_dy =
static_cast<int>(drag_delta.y) / step * step;
881 if (inc_dx == 0 && inc_dy == 0)
898 const std::tuple<int, int, int, int>& bounds,
bool additive,
bool toggle) {
904 const auto [raw_min_x, raw_min_y, raw_max_x, raw_max_y] = bounds;
905 const int min_x = std::min(raw_min_x, raw_max_x);
906 const int max_x = std::max(raw_min_x, raw_max_x);
907 const int min_y = std::min(raw_min_y, raw_max_y);
908 const int max_y = std::max(raw_min_y, raw_max_y);
909 const int rect_w = std::max(1, max_x - min_x);
910 const int rect_h = std::max(1, max_y - min_y);
912 std::vector<SelectedEntity> hits;
913 for (
size_t i = 0; i < room->GetDoors().size(); ++i) {
915 if (
auto entity_bounds = GetEntityBounds(*room, entity)) {
916 auto [x, y, w, h] = *entity_bounds;
917 if (Intersects(x, y, w, h, min_x, min_y, rect_w, rect_h)) {
918 hits.push_back(entity);
922 for (
size_t i = 0; i < room->GetSprites().size(); ++i) {
924 if (
auto entity_bounds = GetEntityBounds(*room, entity)) {
925 auto [x, y, w, h] = *entity_bounds;
926 if (Intersects(x, y, w, h, min_x, min_y, rect_w, rect_h)) {
927 hits.push_back(entity);
931 for (
size_t i = 0; i < room->GetPotItems().size(); ++i) {
933 if (
auto entity_bounds = GetEntityBounds(*room, entity)) {
934 auto [x, y, w, h] = *entity_bounds;
935 if (Intersects(x, y, w, h, min_x, min_y, rect_w, rect_h)) {
936 hits.push_back(entity);
944 if (!additive && !toggle) {
948 for (
const auto entity : hits) {
971 int canvas_x,
int canvas_y,
const std::vector<SelectedEntity>& hits)
const {
972 constexpr int kSameSpotTolerancePx = 3;
973 if (std::abs(canvas_x -
cycle_last_x_) > kSameSpotTolerancePx ||
979 for (
size_t i = 0; i < hits.size(); ++i) {
988 const std::vector<SelectedEntity>& hits)
const {
989 for (
size_t i = 0; i < hits.size(); ++i) {
991 if (hit.
type == EntityType::Object) {
1005 if (selected == hit) {
1010 return std::nullopt;
1018 const ImGuiIO& io = ImGui::GetIO();
1019 if (!IsCycleModifierHeld(io)) {
1026 const auto [canvas_x, canvas_y] =
1029 if (hits.size() < 2) {
1053 if (ImGui::GetCurrentContext()) {
1063 const ImGuiIO& io = ImGui::GetIO();
1064 const bool cycle_modifier_held = IsCycleModifierHeld(io);
1065 if (!cycle_modifier_held && elapsed > kCycleHudHoldSeconds) {
1073 1.0f -
static_cast<float>(elapsed / kCycleHudHoldSeconds));
1075 ImVec4 bg = theme.panel_bg_darker;
1076 bg.w *= 0.92f * alpha;
1077 ImVec4 border = theme.panel_border_color;
1079 ImVec4 text = theme.text_primary;
1081 ImVec4 secondary = theme.text_secondary_color;
1082 secondary.w *= alpha;
1083 ImVec4 active = theme.accent_color;
1086 ImGui::SetNextWindowPos(
1089 ImGui::SetNextWindowBgAlpha(bg.w);
1091 {ImGuiCol_Border, border},
1092 {ImGuiCol_Text, text}});
1094 {{ImGuiStyleVar_WindowPadding, ImVec2(8.0f, 6.0f)},
1095 {ImGuiStyleVar_WindowRounding, 4.0f}});
1096 constexpr ImGuiWindowFlags kFlags =
1097 ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings |
1098 ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoNav |
1099 ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoInputs;
1100 if (ImGui::Begin(
"##DungeonSelectionCycleHud",
nullptr, kFlags)) {
1101 ImGui::TextColored(secondary, tr(
"Cycle"));
1106 ImGui::SameLine(0.0f, 5.0f);
1107 ImGui::TextUnformatted(
1121 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1125 const float scale = transform.
scale();
1127 0.6f + 0.4f * std::sin(
static_cast<float>(ImGui::GetTime()) * 6.0f);
1131 const auto bounds = GetEntityBounds(*room, entity);
1132 if (!bounds.has_value()) {
1136 auto [x, y, w, h] = *bounds;
1138 ImVec2(
static_cast<float>(x),
static_cast<float>(y)));
1140 ImVec2(
static_cast<float>(w),
static_cast<float>(h)));
1141 ImVec2 end(start.x + size.x, start.y + size.y);
1142 constexpr float kMargin = 2.0f;
1148 ImVec4 base = EntitySelectionColor(theme, entity.type);
1150 fill.w = 0.14f + 0.10f * pulse;
1151 ImVec4 border = base;
1152 border.w = (i == 0) ? 0.95f : 0.72f;
1154 draw_list->AddRectFilled(start, end, ImGui::GetColorU32(fill));
1155 draw_list->AddRect(start, end, ImGui::GetColorU32(border), 0.0f, 0,
1156 (i == 0) ? 2.2f : 1.6f);
1157 draw_list->AddText(ImVec2(start.x, start.y - 14.0f * scale),
1158 ImGui::GetColorU32(theme.text_primary),
1166 switch (entity.
type) {
1167 case EntityType::Door: {
1170 const std::string direction_name(
1172 return absl::StrFormat(
"Door (%s)", direction_name);
1176 case EntityType::Sprite:
1179 return absl::StrFormat(
"Sprite (0x%02X - %s)", sprite.id(),
1183 case EntityType::Item:
1186 return absl::StrFormat(
"Item (0x%02X)", item.item);
1189 case EntityType::Object:
1192 return absl::StrFormat(
"Object (0x%03X - %s)",
object.id_,
1196 case EntityType::None:
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)
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 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.
std::vector< SelectedEntity > SelectedEntitiesForEdit() const
absl::Status ReportSelectionEditStatus(absl::Status status)
bool ApplySelection(SelectedEntity entity)
std::vector< SelectedEntity > GetEntitiesAtPosition(int canvas_x, int canvas_y) const
void FinishSelectionGesture()
bool IsSelectionHitSelected(SelectedEntity entity) const
bool entity_group_drag_active_
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.
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.
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.
void SetSelectedEntities(std::vector< SelectedEntity > entities)
std::string DescribeEntity(SelectedEntity entity) const
size_t cycle_active_index_
absl::Status selection_edit_status_
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.
absl::Status DeleteSelectedEntity()
Delete currently selected entity.
void SetMode(Mode mode)
Set interaction mode.
Mode
Available interaction modes.
void ClearAllEntitySelections()
Clear all entity selections.
absl::Status CommitSelectionEdit(const DungeonSelectionEditRequest &request, bool continuous=false)
void DrawGhostPreviews()
Draw ghost previews for active placement mode.
int SelectionMoveStepPixels() const
bool SameCycleTarget(int canvas_x, int canvas_y, const std::vector< SelectedEntity > &hits) const
bool HasEntitySelection() const
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.
bool HasSelection() const
Check if an item is selected.
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.
std::vector< size_t > GetSelectedIndices() const
Get all selected object indices.
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.
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 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
RAII guard for ImGui style colors.
RAII guard for ImGui style vars.
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)
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.
absl::StatusOr< DungeonSelectionEditPlan > PlanDungeonSelectionEdit(const zelda3::Room &room, const DungeonSelectionEditRequest &request)
constexpr uint8_t kSelectionSprites
constexpr uint8_t kSelectionObjects
constexpr uint8_t kSelectionDoors
void ApplyDungeonSelectionEditState(zelda3::Room &room, const DungeonSelectionEditState &state, uint8_t domains)
EntityType
Type of entity that can be selected in the dungeon editor.
constexpr uint8_t kSelectionItems
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)
const char * ResolveSpriteName(uint16_t id)
Centralized theme colors for Agent UI components.
ImVec4 dungeon_selection_primary
DungeonSelectionEditKind kind
std::vector< SelectedEntity > entities
std::vector< size_t > objects
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.
std::function< void(const absl::Status &) on_selection_edit_error)
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.
std::function< void()> on_selection_edit_finished
zelda3::Room * GetCurrentRoom() const
Get pointer to current room.
std::function< absl::Status(const DungeonSelectionEditPlan &, bool continuous) on_selection_edit)
Represents a selected entity in the dungeon editor.