feat(editor): select all, recent zones, minimap selection highlights

- Ctrl+A selects all placed objects, context menu has Select All item
- selectAll() added to ObjectPlacer, works with multi-select transforms
- Recent Zones submenu in File menu (last 8 loaded zones, deduplicated)
- Minimap: selected objects shown as white dots with gold ring outline
  vs yellow dots for unselected objects
- Help panel updated with Ctrl+A and Ctrl+Shift+Click documentation
This commit is contained in:
Kelsi 2026-05-05 13:52:02 -07:00
parent ddf97e9b8a
commit 533c218983
5 changed files with 49 additions and 3 deletions

View file

@ -302,6 +302,10 @@ void EditorApp::processEvents() {
ui_.openNewTerrainDialog();
if (sc == SDL_SCANCODE_O && (event.key.keysym.mod & KMOD_CTRL))
ui_.openLoadDialog();
if (sc == SDL_SCANCODE_A && (event.key.keysym.mod & KMOD_CTRL)) {
objectPlacer_.selectAll();
showToast("Selected " + std::to_string(objectPlacer_.selectionCount()) + " objects");
}
// Ctrl+Y = Redo (alternate binding)
if (sc == SDL_SCANCODE_Y && (event.key.keysym.mod & KMOD_CTRL)) {
if (terrainEditor_.history().canRedo()) {
@ -699,6 +703,13 @@ void EditorApp::loadADT(const std::string& mapName, int tileX, int tileY) {
loadedTileX_ = tileX;
loadedTileY_ = tileY;
// Track recent zones (deduplicate, max 8)
recentZones_.erase(std::remove_if(recentZones_.begin(), recentZones_.end(),
[&](const RecentZone& rz) { return rz.mapName == mapName && rz.tileX == tileX && rz.tileY == tileY; }),
recentZones_.end());
recentZones_.insert(recentZones_.begin(), {mapName, tileX, tileY});
if (recentZones_.size() > 8) recentZones_.resize(8);
// Position camera at terrain center using actual chunk positions
if (mesh.validChunkCount > 0) {
auto& firstChunk = mesh.chunks[0];

View file

@ -138,12 +138,17 @@ private:
bool autoSaveEnabled_ = true;
bool showQuitConfirm_ = false;
// Recent zones
struct RecentZone { std::string mapName; int tileX; int tileY; };
std::vector<RecentZone> recentZones_;
// Toast notifications
struct Toast { std::string msg; float timer; };
std::vector<Toast> toasts_;
public:
void showToast(const std::string& msg, float duration = 3.0f);
const std::vector<Toast>& getToasts() const { return toasts_; }
const std::vector<RecentZone>& getRecentZones() const { return recentZones_; }
void updateToasts(float dt);
private:
size_t lastObjCount_ = 0;

View file

@ -254,6 +254,16 @@ void EditorUI::renderMenuBar(EditorApp& app) {
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Recent Zones", !app.getRecentZones().empty())) {
for (const auto& rz : app.getRecentZones()) {
char label[128];
std::snprintf(label, sizeof(label), "%s [%d, %d]",
rz.mapName.c_str(), rz.tileX, rz.tileY);
if (ImGui::MenuItem(label))
app.loadADT(rz.mapName, rz.tileX, rz.tileY);
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Content Packs (.wcp)")) {
static char wcpImportPath[256] = "content.wcp";
ImGui::InputText("Path##wcp", wcpImportPath, sizeof(wcpImportPath));
@ -489,7 +499,9 @@ void EditorUI::renderMenuBar(EditorApp& app) {
ImGui::Text("Quick Actions:");
ImGui::BulletText("Ctrl+N — new terrain");
ImGui::BulletText("Ctrl+O — load map tile");
ImGui::BulletText("Ctrl+A — select all objects");
ImGui::BulletText("Alt+Click — eyedropper (paint mode)");
ImGui::BulletText("Ctrl+Shift+Click — add to selection");
ImGui::BulletText("Middle-drag — orbit camera");
ImGui::Separator();
ImGui::Text("View:");
@ -2044,6 +2056,9 @@ void EditorUI::renderContextMenu(EditorApp& app) {
else { app.getNpcSpawner().removeCreature(app.getNpcSpawner().getSelectedIndex()); }
app.markObjectsDirty();
}
if (ImGui::MenuItem("Select All", "Ctrl+A")) {
app.getObjectPlacer().selectAll();
}
if (ImGui::MenuItem("Deselect")) {
app.getObjectPlacer().clearSelection();
app.getNpcSpawner().clearSelection();
@ -2110,7 +2125,7 @@ void EditorUI::renderMinimap(EditorApp& app) {
}
}
// Draw objects as yellow dots
// Draw objects (yellow=normal, white+ring=selected)
float tileNW_X = (32.0f - static_cast<float>(terrain->coord.y)) * 533.33333f;
float tileNW_Y = (32.0f - static_cast<float>(terrain->coord.x)) * 533.33333f;
for (const auto& obj : app.getObjectPlacer().getObjects()) {
@ -2118,7 +2133,12 @@ void EditorUI::renderMinimap(EditorApp& app) {
float v = (tileNW_Y - obj.position.y) / 533.33333f;
if (u >= 0 && u <= 1 && v >= 0 && v <= 1) {
ImVec2 pt(origin.x + v * avail.x, origin.y + u * (16 * cellH));
dl->AddCircleFilled(pt, 2.0f, IM_COL32(255, 220, 50, 200));
if (obj.selected) {
dl->AddCircleFilled(pt, 3.5f, IM_COL32(255, 255, 255, 230));
dl->AddCircle(pt, 5.0f, IM_COL32(255, 200, 50, 200), 0, 1.5f);
} else {
dl->AddCircleFilled(pt, 2.0f, IM_COL32(255, 220, 50, 200));
}
}
}
// Draw NPCs as red dots

View file

@ -111,6 +111,15 @@ PlacedObject* ObjectPlacer::getSelected() {
return &objects_[selectedIdx_];
}
void ObjectPlacer::selectAll() {
clearSelection();
for (int i = 0; i < static_cast<int>(objects_.size()); i++) {
objects_[i].selected = true;
selectedIndices_.push_back(i);
}
if (!objects_.empty()) selectedIdx_ = 0;
}
void ObjectPlacer::moveSelected(const glm::vec3& delta) {
if (selectedIndices_.size() > 1) {
for (int idx : selectedIndices_) objects_[idx].position += delta;

View file

@ -60,7 +60,8 @@ public:
const std::vector<PlacedObject>& getObjects() const { return objects_; }
std::vector<PlacedObject>& getObjects() { return objects_; }
void clearAll() { objects_.clear(); undoStack_.clear(); selectedIdx_ = -1; }
void selectAll();
void clearAll() { objects_.clear(); undoStack_.clear(); selectedIdx_ = -1; selectedIndices_.clear(); }
size_t objectCount() const { return objects_.size(); }
float getPlacementRotationY() const { return placementRotY_; }