extract SelectionController
This commit is contained in:
@@ -16,6 +16,7 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/TunnelCompletion.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.h
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
@@ -29,6 +30,7 @@ SET(SRCS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/TunnelCompletion.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.cpp
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
|
||||
165
src/lib/core/SelectionController.cpp
Normal file
165
src/lib/core/SelectionController.cpp
Normal file
@@ -0,0 +1,165 @@
|
||||
#include "SelectionController.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
|
||||
#include "DebrisSelectionChangedEvent.h"
|
||||
#include "EntitySelectionChangedEvent.h"
|
||||
#include "EventManager.h"
|
||||
#include "SelectionChangedEvent.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
template <typename T>
|
||||
bool contains(const std::vector<T>& items, const T& item)
|
||||
{
|
||||
return std::find(items.begin(), items.end(), item) != items.end();
|
||||
}
|
||||
|
||||
// Applies `hits` to `selection` per `mode`. Replace is handled by the caller so
|
||||
// that an empty hit list can mean "clear this category" there but "leave this
|
||||
// category alone" here.
|
||||
template <typename T>
|
||||
void combine(std::vector<T>& selection, const std::vector<T>& hits, SelectionMode mode)
|
||||
{
|
||||
for (const T& hit : hits)
|
||||
{
|
||||
const typename std::vector<T>::iterator it =
|
||||
std::find(selection.begin(), selection.end(), hit);
|
||||
if (it == selection.end())
|
||||
{
|
||||
selection.push_back(hit);
|
||||
}
|
||||
else if (mode == SelectionMode::Toggle)
|
||||
{
|
||||
// Only a toggle removes; an additive box drag never deselects.
|
||||
selection.erase(it);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const std::vector<BuildingId>& SelectionController::getSelectedBuildings() const
|
||||
{
|
||||
return m_buildings;
|
||||
}
|
||||
|
||||
const std::vector<entt::entity>& SelectionController::getSelectedActors() const
|
||||
{
|
||||
return m_actors;
|
||||
}
|
||||
|
||||
const std::vector<entt::entity>& SelectionController::getSelectedDebris() const
|
||||
{
|
||||
return m_debris;
|
||||
}
|
||||
|
||||
bool SelectionController::isActorSelected(entt::entity actor) const
|
||||
{
|
||||
return contains(m_actors, actor);
|
||||
}
|
||||
|
||||
bool SelectionController::isDebrisSelected(entt::entity debris) const
|
||||
{
|
||||
return contains(m_debris, debris);
|
||||
}
|
||||
|
||||
void SelectionController::selectBuildings(const std::vector<BuildingId>& ids,
|
||||
SelectionMode mode)
|
||||
{
|
||||
// Buildings win over field objects (REQ-UI-SELECTION-CATEGORIES).
|
||||
if (clearActorsQuietly()) { publishActors(); }
|
||||
if (clearDebrisQuietly()) { publishDebris(); }
|
||||
|
||||
if (mode == SelectionMode::Replace)
|
||||
{
|
||||
m_buildings = ids;
|
||||
}
|
||||
else
|
||||
{
|
||||
combine(m_buildings, ids, mode);
|
||||
}
|
||||
publishBuildings();
|
||||
}
|
||||
|
||||
void SelectionController::selectFieldObjects(const std::vector<entt::entity>& actors,
|
||||
const std::vector<entt::entity>& debris,
|
||||
SelectionMode mode)
|
||||
{
|
||||
if (clearBuildingsQuietly()) { publishBuildings(); }
|
||||
|
||||
if (mode == SelectionMode::Replace)
|
||||
{
|
||||
m_actors = actors;
|
||||
m_debris = debris;
|
||||
}
|
||||
else
|
||||
{
|
||||
combine(m_actors, actors, mode);
|
||||
combine(m_debris, debris, mode);
|
||||
}
|
||||
publishActors();
|
||||
publishDebris();
|
||||
}
|
||||
|
||||
void SelectionController::clearAll()
|
||||
{
|
||||
if (clearBuildingsQuietly()) { publishBuildings(); }
|
||||
if (clearActorsQuietly()) { publishActors(); }
|
||||
if (clearDebrisQuietly()) { publishDebris(); }
|
||||
}
|
||||
|
||||
void SelectionController::setSelectedActors(std::vector<entt::entity> actors)
|
||||
{
|
||||
if (actors == m_actors) { return; }
|
||||
m_actors = std::move(actors);
|
||||
publishActors();
|
||||
}
|
||||
|
||||
void SelectionController::setSelectedDebris(std::vector<entt::entity> debris)
|
||||
{
|
||||
if (debris == m_debris) { return; }
|
||||
m_debris = std::move(debris);
|
||||
publishDebris();
|
||||
}
|
||||
|
||||
bool SelectionController::clearBuildingsQuietly()
|
||||
{
|
||||
if (m_buildings.empty()) { return false; }
|
||||
m_buildings.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SelectionController::clearActorsQuietly()
|
||||
{
|
||||
if (m_actors.empty()) { return false; }
|
||||
m_actors.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SelectionController::clearDebrisQuietly()
|
||||
{
|
||||
if (m_debris.empty()) { return false; }
|
||||
m_debris.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
void SelectionController::publishBuildings() const
|
||||
{
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<SelectionChangedEvent>(m_buildings));
|
||||
}
|
||||
|
||||
void SelectionController::publishActors() const
|
||||
{
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<EntitySelectionChangedEvent>(m_actors));
|
||||
}
|
||||
|
||||
void SelectionController::publishDebris() const
|
||||
{
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<DebrisSelectionChangedEvent>(m_debris));
|
||||
}
|
||||
75
src/lib/core/SelectionController.h
Normal file
75
src/lib/core/SelectionController.h
Normal file
@@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "BuildingId.h"
|
||||
#include "entt/entity/entity.hpp"
|
||||
|
||||
// How a new hit combines with what is already selected.
|
||||
enum class SelectionMode
|
||||
{
|
||||
Replace, // plain click or drag: the hit becomes the whole selection
|
||||
Toggle, // Ctrl + click: the hit joins the selection, or leaves it if present
|
||||
Add // Ctrl + box drag: the hits join the selection, never leave it
|
||||
};
|
||||
|
||||
// The player's current selection across the three categories, and the rules that
|
||||
// govern moving between them (REQ-UI-SELECTION-CATEGORIES).
|
||||
//
|
||||
// The rules were previously written out once for point-clicks and once for box
|
||||
// drags, which is why they live here now: buildings win over field objects, so
|
||||
// selecting a building clears actors and debris, and selecting either of those
|
||||
// clears buildings — but actors and debris coexist with each other
|
||||
// (REQ-UI-ENTITY-CLICK-SELECT, REQ-UI-DEBRIS-CLICK-SELECT, REQ-UI-MULTI-SELECT,
|
||||
// REQ-UI-DEBRIS-MULTI-SELECT). A point-click and a box drag then differ only in
|
||||
// the SelectionMode they pass and in how many hits they pass.
|
||||
//
|
||||
// Every mutator publishes the change events, so callers never emit them by hand.
|
||||
// Hit-testing is not done here: callers resolve what was hit and pass the result
|
||||
// in, which keeps this free of any simulation dependency.
|
||||
class SelectionController
|
||||
{
|
||||
public:
|
||||
const std::vector<BuildingId>& getSelectedBuildings() const;
|
||||
const std::vector<entt::entity>& getSelectedActors() const;
|
||||
const std::vector<entt::entity>& getSelectedDebris() const;
|
||||
|
||||
bool isActorSelected(entt::entity actor) const;
|
||||
bool isDebrisSelected(entt::entity debris) const;
|
||||
|
||||
// Selects buildings and/or construction sites, clearing any field selection.
|
||||
// Always publishes SelectionChangedEvent, even when the result is unchanged,
|
||||
// so a click on an already-selected building still refreshes its panel.
|
||||
void selectBuildings(const std::vector<BuildingId>& ids, SelectionMode mode);
|
||||
|
||||
// Selects field objects, clearing any building selection. Actors and debris are
|
||||
// set together so a Replace can express "these actors and no debris" — which is
|
||||
// what a plain click on an actor means — while a Toggle or Add leaves the
|
||||
// category whose vector is empty untouched. Always publishes both field events.
|
||||
void selectFieldObjects(const std::vector<entt::entity>& actors,
|
||||
const std::vector<entt::entity>& debris,
|
||||
SelectionMode mode);
|
||||
|
||||
// Empties all three categories, publishing only for those that were non-empty.
|
||||
void clearAll();
|
||||
|
||||
// Replace one category outright, publishing only if it actually changed. For
|
||||
// the per-frame prune of entities that despawned or died: the liveness query
|
||||
// belongs to the caller, which is the one that can see the simulation.
|
||||
void setSelectedActors(std::vector<entt::entity> actors);
|
||||
void setSelectedDebris(std::vector<entt::entity> debris);
|
||||
|
||||
private:
|
||||
// Each returns whether anything changed, without publishing.
|
||||
bool clearBuildingsQuietly();
|
||||
bool clearActorsQuietly();
|
||||
bool clearDebrisQuietly();
|
||||
|
||||
void publishBuildings() const;
|
||||
void publishActors() const;
|
||||
void publishDebris() const;
|
||||
|
||||
std::vector<BuildingId> m_buildings;
|
||||
std::vector<entt::entity> m_actors;
|
||||
std::vector<entt::entity> m_debris;
|
||||
};
|
||||
Reference in New Issue
Block a user