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_) {
370 switch (selected.type) {
371 case EntityType::Door:
374 case EntityType::Sprite:
377 case EntityType::Item:
380 case EntityType::Object:
381 case EntityType::None:
389 int canvas_x,
int canvas_y)
const {
398 int canvas_x,
int canvas_y)
const {
399 std::vector<SelectedEntity> hits;
566 return selected.type == EntityType::None
567 ? std::vector<SelectedEntity>{}
568 : std::vector<SelectedEntity>{selected};
573 return std::any_of(entities.begin(), entities.end(),
574 [](
const auto entity) {
575 return entity.type == EntityType::Sprite;
582 absl::Status status) {
596 "No room is available for selection editing"));
610 absl::FailedPreconditionError(
"No room is available for door trim"));
626 if (!room || room->id() != plan.
room_id) {
628 absl::FailedPreconditionError(
"Selection room is no longer available"));
643 for (
const auto [mask, domain] :
666 for (
const auto [mask, domain] :
688 request.
entities.front().type == EntityType::Door) {
692 const size_t index = request.
entities.front().index;
693 if (!room || index >= room->GetDoors().size()) {
696 const auto& door = room->GetDoors()[index];
699 const int64_t next_position =
700 static_cast<int64_t
>(door.position) + (horizontal ? delta_x : delta_y);
701 if (next_position == door.position)
703 if (next_position < 0 ||
706 static_cast<uint8_t
>(next_position), door.direction)) {
708 absl::OutOfRangeError(
"No door slot in that direction"));
712 next.position =
static_cast<uint8_t
>(next_position);
713 const auto [old_x, old_y] = door.GetPixelCoords();
714 const auto [new_x, new_y] = next.GetPixelCoords();
720 const int64_t dx =
static_cast<int64_t
>(delta_x) * step;
721 const int64_t dy =
static_cast<int64_t
>(delta_y) * step;
722 if (dx < -512 || dx > 512 || dy < -512 || dy > 512) {
724 absl::OutOfRangeError(
"Selection movement leaves the room"));
756 case EntityType::Door:
758 case EntityType::Sprite:
760 case EntityType::Item:
762 case EntityType::Object:
763 case EntityType::None:
797 if (entity.
type == EntityType::Object) {
811 switch (entity.
type) {
812 case EntityType::Door:
816 case EntityType::Sprite:
820 case EntityType::Item:
824 case EntityType::Object:
825 case EntityType::None:
832 bool additive,
bool toggle) {
833 if (entity.
type == EntityType::Object || entity.
type == EntityType::None) {
841 if (!additive && !toggle) {
845 const auto existing =
859 switch (selected.type) {
860 case EntityType::Door:
863 case EntityType::Sprite:
866 case EntityType::Item:
869 case EntityType::Object:
870 case EntityType::None:
883 if (entity.
type == EntityType::Object) {
893 const bool has_object_selection =
918 const ImVec2 drag_delta(
923 const int drag_dx =
static_cast<int>(drag_delta.x) / step * step;
924 const int drag_dy =
static_cast<int>(drag_delta.y) / step * step;
927 if (inc_dx == 0 && inc_dy == 0)
944 const std::tuple<int, int, int, int>& bounds,
bool additive,
bool toggle) {
950 const auto [raw_min_x, raw_min_y, raw_max_x, raw_max_y] = bounds;
951 const int min_x = std::min(raw_min_x, raw_max_x);
952 const int max_x = std::max(raw_min_x, raw_max_x);
953 const int min_y = std::min(raw_min_y, raw_max_y);
954 const int max_y = std::max(raw_min_y, raw_max_y);
955 const int rect_w = std::max(1, max_x - min_x);
956 const int rect_h = std::max(1, max_y - min_y);
958 std::vector<SelectedEntity> hits;
959 for (
size_t i = 0; i < room->GetDoors().size(); ++i) {
961 if (
auto entity_bounds = GetEntityBounds(*room, entity)) {
962 auto [x, y, w, h] = *entity_bounds;
963 if (Intersects(x, y, w, h, min_x, min_y, rect_w, rect_h)) {
964 hits.push_back(entity);
968 for (
size_t i = 0; i < room->GetSprites().size(); ++i) {
970 if (
auto entity_bounds = GetEntityBounds(*room, entity)) {
971 auto [x, y, w, h] = *entity_bounds;
972 if (Intersects(x, y, w, h, min_x, min_y, rect_w, rect_h)) {
973 hits.push_back(entity);
977 for (
size_t i = 0; i < room->GetPotItems().size(); ++i) {
979 if (
auto entity_bounds = GetEntityBounds(*room, entity)) {
980 auto [x, y, w, h] = *entity_bounds;
981 if (Intersects(x, y, w, h, min_x, min_y, rect_w, rect_h)) {
982 hits.push_back(entity);
990 if (!additive && !toggle) {
994 for (
const auto entity : hits) {
1017 int canvas_x,
int canvas_y,
const std::vector<SelectedEntity>& hits)
const {
1018 constexpr int kSameSpotTolerancePx = 3;
1019 if (std::abs(canvas_x -
cycle_last_x_) > kSameSpotTolerancePx ||
1020 std::abs(canvas_y -
cycle_last_y_) > kSameSpotTolerancePx ||
1025 for (
size_t i = 0; i < hits.size(); ++i) {
1034 const std::vector<SelectedEntity>& hits)
const {
1035 for (
size_t i = 0; i < hits.size(); ++i) {
1037 if (hit.
type == EntityType::Object) {
1051 if (selected == hit) {
1056 return std::nullopt;
1064 const ImGuiIO& io = ImGui::GetIO();
1065 if (!IsCycleModifierHeld(io)) {
1072 const auto [canvas_x, canvas_y] =
1075 if (hits.size() < 2) {
1099 if (ImGui::GetCurrentContext()) {
1109 const ImGuiIO& io = ImGui::GetIO();
1110 const bool cycle_modifier_held = IsCycleModifierHeld(io);
1111 if (!cycle_modifier_held && elapsed > kCycleHudHoldSeconds) {
1119 1.0f -
static_cast<float>(elapsed / kCycleHudHoldSeconds));
1121 ImVec4 bg = theme.panel_bg_darker;
1122 bg.w *= 0.92f * alpha;
1123 ImVec4 border = theme.panel_border_color;
1125 ImVec4 text = theme.text_primary;
1127 ImVec4 secondary = theme.text_secondary_color;
1128 secondary.w *= alpha;
1129 ImVec4 active = theme.accent_color;
1132 ImGui::SetNextWindowPos(
1135 ImGui::SetNextWindowBgAlpha(bg.w);
1137 {ImGuiCol_Border, border},
1138 {ImGuiCol_Text, text}});
1140 {{ImGuiStyleVar_WindowPadding, ImVec2(8.0f, 6.0f)},
1141 {ImGuiStyleVar_WindowRounding, 4.0f}});
1142 constexpr ImGuiWindowFlags kFlags =
1143 ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings |
1144 ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoNav |
1145 ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoInputs;
1146 if (ImGui::Begin(
"##DungeonSelectionCycleHud",
nullptr, kFlags)) {
1147 ImGui::TextColored(secondary, tr(
"Cycle"));
1152 ImGui::SameLine(0.0f, 5.0f);
1153 ImGui::TextUnformatted(
1167 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1171 const float scale = transform.
scale();
1173 0.6f + 0.4f * std::sin(
static_cast<float>(ImGui::GetTime()) * 6.0f);
1177 const auto bounds = GetEntityBounds(*room, entity);
1178 if (!bounds.has_value()) {
1182 auto [x, y, w, h] = *bounds;
1184 ImVec2(
static_cast<float>(x),
static_cast<float>(y)));
1186 ImVec2(
static_cast<float>(w),
static_cast<float>(h)));
1187 ImVec2 end(start.x + size.x, start.y + size.y);
1188 constexpr float kMargin = 2.0f;
1194 ImVec4 base = EntitySelectionColor(theme, entity.type);
1196 fill.w = 0.14f + 0.10f * pulse;
1197 ImVec4 border = base;
1198 border.w = (i == 0) ? 0.95f : 0.72f;
1200 draw_list->AddRectFilled(start, end, ImGui::GetColorU32(fill));
1201 draw_list->AddRect(start, end, ImGui::GetColorU32(border), 0.0f, 0,
1202 (i == 0) ? 2.2f : 1.6f);
1203 draw_list->AddText(ImVec2(start.x, start.y - 14.0f * scale),
1204 ImGui::GetColorU32(theme.text_primary),
1212 switch (entity.
type) {
1213 case EntityType::Door: {
1216 const std::string direction_name(
1218 return absl::StrFormat(
"Door (%s)", direction_name);
1222 case EntityType::Sprite:
1225 return absl::StrFormat(
"Sprite (0x%02X - %s)", sprite.id(),
1229 case EntityType::Item:
1232 return absl::StrFormat(
"Item (0x%02X)", item.item);
1235 case EntityType::Object:
1238 return absl::StrFormat(
"Object (0x%03X - %s)",
object.id_,
1242 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.
absl::Status CompleteDoorTrim(size_t door_index)
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.
absl::Status CommitPlannedSelectionEdit(const DungeonSelectionEditPlan &plan, bool continuous)
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)
absl::StatusOr< DungeonSelectionEditPlan > PlanDungeonDoorTrim(const zelda3::Room &room, size_t door_index)
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
DungeonSelectionEditState after
DungeonSelectionEditKind kind
std::vector< SelectedEntity > entities
std::vector< size_t > objects
std::vector< SelectedEntity > selected_entities
std::vector< size_t > selected_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.