diff --git a/include/imap.h b/include/imap.h index 8e04cef3a3..aa715a6236 100644 --- a/include/imap.h +++ b/include/imap.h @@ -8,6 +8,8 @@ #include "ikeyvaluestore.h" #include +#include + // Registry setting for suppressing the map load progress dialog const char* const RKEY_MAP_SUPPRESS_LOAD_STATUS_DIALOG = "user/ui/map/suppressMapLoadDialog"; @@ -29,8 +31,8 @@ class ITargetManager; // see ilayer.h class ILayerManager; -namespace selection -{ +namespace selection +{ class ISelectionSetManager; class ISelectionGroupManager; } @@ -174,7 +176,7 @@ class IMap : // Create a MapExporter instance which can be used to export a scene, // including the necessary preparation, info-file handling, etc. // This is mainly a service method for external code, like the gameconnection. - virtual map::IMapExporter::Ptr createMapExporter(map::IMapWriter& writer, + virtual map::IMapExporter::Ptr createMapExporter(map::IMapWriter& writer, const scene::IMapRootNodePtr& root, std::ostream& mapStream) = 0; // Exports the current selection to the given output stream, using the map's format @@ -191,6 +193,29 @@ class IMap : // Returns the currently active merge operation (or an empty reference if no merge is ongoing) virtual scene::merge::IMergeOperation::Ptr getActiveMergeOperation() = 0; + + /* POINTFILE MANAGEMENT */ + + /// Functor to receive pointfile paths + using PointfileFunctor = std::function; + + /// Enumerate pointfiles associated with the current map + virtual void forEachPointfile(PointfileFunctor func) const = 0; + + /** + * \brief Show the point trace contained in the specified file. + * + * \param filePath + * Filesystem path of the file to parse for point coordinates, or an empty + * path to hide any current point trace. + * + * \exception std::runtime_error + * Thrown if filePath is not empty but the file is inaccessible. + */ + virtual void showPointFile(const fs::path& filePath) = 0; + + /// Return true if a point trace is currently visible + virtual bool isPointTraceVisible() const = 0; }; typedef std::shared_ptr IMapPtr; diff --git a/install/menu.xml b/install/menu.xml index c712aaada9..0bf3001b6c 100644 --- a/install/menu.xml +++ b/install/menu.xml @@ -30,7 +30,7 @@ - + diff --git a/libs/scene/PointTrace.h b/libs/scene/PointTrace.h new file mode 100644 index 0000000000..3d2a5d91cd --- /dev/null +++ b/libs/scene/PointTrace.h @@ -0,0 +1,36 @@ +#pragma once + +#include "math/Vector3.h" + +namespace map +{ + +/// Parsed point positions from a .lin file +class PointTrace +{ +public: + using Points = std::vector; + +private: + + // List of point positions + Points _points; + +public: + + /// Construct a PointTrace to read point data from the given stream + explicit PointTrace(std::istream& stream) + { + // Point file consists of one point per line, with three components + double x, y, z; + while (stream >> x >> y >> z) + { + _points.push_back(Vector3(x, y, z)); + } + } + + /// Return points parsed + const Points& points() const { return _points; } +}; + +} \ No newline at end of file diff --git a/radiant/CMakeLists.txt b/radiant/CMakeLists.txt index f6bbe4ad42..fee68a89a6 100644 --- a/radiant/CMakeLists.txt +++ b/radiant/CMakeLists.txt @@ -149,6 +149,7 @@ add_executable(darkradiant ui/patch/PatchCreateDialog.cpp ui/patch/PatchInspector.cpp ui/patch/PatchThickenDialog.cpp + ui/PointFileChooser.cpp ui/prefabselector/PrefabSelector.cpp ui/prefdialog/GameSetupDialog.cpp ui/prefdialog/GameSetupPage.cpp diff --git a/radiant/ui/PointFileChooser.cpp b/radiant/ui/PointFileChooser.cpp new file mode 100644 index 0000000000..4de9d7dfc8 --- /dev/null +++ b/radiant/ui/PointFileChooser.cpp @@ -0,0 +1,77 @@ +#include "PointFileChooser.h" + +#include "imainframe.h" +#include "imap.h" +#include "icommandsystem.h" +#include "os/fs.h" +#include "command/ExecutionFailure.h" + +#include +#include +#include +#include + +namespace ui +{ + +PointFileChooser::PointFileChooser(const wxArrayString& files) +: wxDialog(GlobalMainFrame().getWxTopLevelWindow(), wxID_ANY, + "Choose pointfile") +{ + SetSizer(new wxBoxSizer(wxVERTICAL)); + + // Construct and populate the dropdown list + _pfChoice.reset( + new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, files) + ); + _pfChoice->SetSelection(0); + GetSizer()->Add(_pfChoice.get(), 0, wxEXPAND | wxALL, 12); + + // Add dialog buttons + auto btnSizer = CreateStdDialogButtonSizer(wxOK | wxCANCEL); + GetSizer()->Add(btnSizer, 0, wxALIGN_RIGHT | wxALIGN_BOTTOM | wxBOTTOM, 12); + + Fit(); +} + +void PointFileChooser::chooseAndToggle() +{ + // If the point trace is currently visible, toggle it to invisible and we're + // done. + if (GlobalMapModule().isPointTraceVisible()) + { + GlobalMapModule().showPointFile({}); + return; + } + + // Determine the list of pointfiles + std::vector pointfiles; + GlobalMapModule().forEachPointfile([&](const fs::path& p) + { pointfiles.push_back(p); }); + if (pointfiles.empty()) + throw cmd::ExecutionFailure("No pointfiles found for current map."); + + // If there is a choice to make, show the dialog + std::size_t chosenPointfile = 0; + if (pointfiles.size() > 1) + { + // Construct list of wxString filenames + wxArrayString filenames; + for (const fs::path& p: pointfiles) + filenames.Add(static_cast(p.filename())); + + // Show dialog with list of pointfiles + PointFileChooser chooser(filenames); + if (chooser.ShowModal() == wxID_OK) + chosenPointfile = chooser._pfChoice->GetSelection(); + else + // Dialog cancelled, don't proceed to showing point trace + return; + } + + // Show the chosen (or only) pointfile + if (chosenPointfile >= 0 && chosenPointfile < pointfiles.size()) + GlobalMapModule().showPointFile(pointfiles[chosenPointfile]); +} + +} \ No newline at end of file diff --git a/radiant/ui/PointFileChooser.h b/radiant/ui/PointFileChooser.h new file mode 100644 index 0000000000..2fa12b8af6 --- /dev/null +++ b/radiant/ui/PointFileChooser.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include + +namespace ui +{ + +/// Selector dialog for pointfiles associated with the current map +class PointFileChooser: public wxDialog +{ + // wxChoice containing the chosen pointfile + wxWindowPtr _pfChoice; + + // Dialog constructor + PointFileChooser(const wxArrayString& files); + +public: + + /** + * \brief Toggle pointfile visibility, showing the chooser if necessary. + * + * If there is only a single pointfile available, this method acts as a + * simple visibility toggle. If there is more than one pointfile, the + * chooser dialog will be shown before toggling visibility on. If there are + * no pointfiles available, an error dialog will be shown. + */ + static void chooseAndToggle(); +}; + +} \ No newline at end of file diff --git a/radiant/ui/UserInterfaceModule.cpp b/radiant/ui/UserInterfaceModule.cpp index 6855484b4c..4e9340673b 100644 --- a/radiant/ui/UserInterfaceModule.cpp +++ b/radiant/ui/UserInterfaceModule.cpp @@ -62,6 +62,7 @@ #include "ui/mousetool/RegistrationHelper.h" #include "ui/mapselector/MapSelector.h" #include "ui/merge/MergeControlDialog.h" +#include "ui/PointFileChooser.h" #include @@ -131,19 +132,19 @@ void UserInterfaceModule::initialiseModule(const IApplicationContext& ctx) // Add the orthocontext menu's layer actions GlobalOrthoContextMenu().addItem( - std::make_shared(_(ADD_TO_LAYER_TEXT), + std::make_shared(_(ADD_TO_LAYER_TEXT), LayerOrthoContextMenuItem::AddToLayer), IOrthoContextMenu::SECTION_LAYER ); GlobalOrthoContextMenu().addItem( - std::make_shared(_(MOVE_TO_LAYER_TEXT), + std::make_shared(_(MOVE_TO_LAYER_TEXT), LayerOrthoContextMenuItem::MoveToLayer), IOrthoContextMenu::SECTION_LAYER ); GlobalOrthoContextMenu().addItem( - std::make_shared(_(REMOVE_FROM_LAYER_TEXT), + std::make_shared(_(REMOVE_FROM_LAYER_TEXT), LayerOrthoContextMenuItem::RemoveFromLayer), IOrthoContextMenu::SECTION_LAYER ); @@ -240,7 +241,7 @@ void UserInterfaceModule::shutdownModule() _editStopwatchStatus.reset(); _manipulatorToggle.reset(); _selectionModeToggle.reset(); - + _mruMenu.reset(); } @@ -266,17 +267,17 @@ void UserInterfaceModule::HandleNotificationMessage(radiant::NotificationMessage switch (msg.getType()) { case radiant::NotificationMessage::Information: - wxutil::Messagebox::Show(msg.hasTitle() ? msg.getTitle() : _("Notification"), + wxutil::Messagebox::Show(msg.hasTitle() ? msg.getTitle() : _("Notification"), msg.getMessage(), IDialog::MessageType::MESSAGE_CONFIRM, parentWindow); break; case radiant::NotificationMessage::Warning: - wxutil::Messagebox::Show(msg.hasTitle() ? msg.getTitle() : _("Warning"), + wxutil::Messagebox::Show(msg.hasTitle() ? msg.getTitle() : _("Warning"), msg.getMessage(), IDialog::MessageType::MESSAGE_WARNING, parentWindow); break; case radiant::NotificationMessage::Error: - wxutil::Messagebox::Show(msg.hasTitle() ? msg.getTitle() : _("Error"), + wxutil::Messagebox::Show(msg.hasTitle() ? msg.getTitle() : _("Error"), msg.getMessage(), IDialog::MessageType::MESSAGE_ERROR, parentWindow); break; }; @@ -301,9 +302,9 @@ void UserInterfaceModule::initialiseEntitySettings() applyPatchVertexColours(); _coloursUpdatedConn = ColourSchemeEditor::signal_ColoursChanged().connect( - [this]() { - applyEntityVertexColours(); - applyBrushVertexColours(); + [this]() { + applyEntityVertexColours(); + applyBrushVertexColours(); applyPatchVertexColours(); } ); @@ -344,7 +345,7 @@ void UserInterfaceModule::refreshShadersCmd(const cmd::ArgumentList& args) // Disable screen updates for the scope of this function auto blocker = GlobalMainFrame().getScopedScreenUpdateBlocker(_("Processing..."), _("Loading Shaders")); - // Reload the Shadersystem, this will also trigger an + // Reload the Shadersystem, this will also trigger an // OpenGLRenderSystem unrealise/realise sequence as the rendersystem // is attached to this class as Observer // We can't do this refresh() operation in a thread it seems due to context binding @@ -367,8 +368,11 @@ void UserInterfaceModule::registerUICommands() GlobalCommandSystem().addCommand("MergeControlDialog", MergeControlDialog::ShowDialog); GlobalCommandSystem().addCommand("OverlayDialog", OverlayDialog::toggle); GlobalCommandSystem().addCommand("TransformDialog", TransformDialog::toggle); + GlobalCommandSystem().addCommand("ChooseAndTogglePointfile", + [](const cmd::ArgumentList&) + { PointFileChooser::chooseAndToggle(); }); - GlobalCommandSystem().addCommand("FindBrush", FindBrushDialog::Show); + GlobalCommandSystem().addCommand("FindBrush", FindBrushDialog::Show); GlobalCommandSystem().addCommand("AnimationPreview", MD5AnimationViewer::Show); GlobalCommandSystem().addCommand("EditColourScheme", ColourSchemeEditor::DisplayDialog); diff --git a/radiant/ui/einspector/EntityInspector.cpp b/radiant/ui/einspector/EntityInspector.cpp index 9295b40567..7fb2c1cb4c 100644 --- a/radiant/ui/einspector/EntityInspector.cpp +++ b/radiant/ui/einspector/EntityInspector.cpp @@ -1235,7 +1235,7 @@ void EntityInspector::addClassAttribute(const EntityClassAttribute& a) wxutil::TreeModel::Row row = _kvStore->AddItem(); wxDataViewItemAttr grey; - grey.SetColour(wxColor(112, 112, 112)); + grey.SetItalic(true); row[_columns.name] = wxVariant(wxDataViewIconText(a.getName(), _emptyIcon)); row[_columns.value] = a.getValue(); diff --git a/radiantcore/map/Map.cpp b/radiantcore/map/Map.cpp index 1f0f0ec270..b2b30aa8dd 100644 --- a/radiantcore/map/Map.cpp +++ b/radiantcore/map/Map.cpp @@ -61,10 +61,10 @@ #include "scene/merge/GraphComparer.h" #include "scene/merge/MergeOperation.h" -namespace map +namespace map { -namespace +namespace { const char* const MAP_UNNAMED_STRING = N_("unnamed.map"); } @@ -101,7 +101,7 @@ void Map::loadMapResourceFromArchive(const std::string& archive, const std::stri void Map::loadMapResourceFromLocation(const MapLocation& location) { - rMessage() << "Loading map from " << location.path << + rMessage() << "Loading map from " << location.path << (location.isArchive ? " [" + location.archiveRelativePath + "]" : "") << std::endl; // Map loading started @@ -110,7 +110,7 @@ void Map::loadMapResourceFromLocation(const MapLocation& location) // Abort any ongoing merge abortMergeOperation(); - _resource = location.isArchive ? + _resource = location.isArchive ? GlobalMapResourceManager().createFromArchiveFile(location.path, location.archiveRelativePath) : GlobalMapResourceManager().createFromPath(location.path); @@ -263,6 +263,64 @@ bool Map::isUnnamed() const { return _mapName == _(MAP_UNNAMED_STRING); } +namespace +{ + bool pointfileNameMatch(const std::string& candidate, + const std::string& mapStem) + { + // A matching point file either has an identical stem to the map file, + // or the map file stem with an underscore suffix (e.g. + // "mapfile_portal_123_456.lin") + if (candidate == mapStem) + return true; + else if (candidate.rfind(mapStem + "_", 0) == 0) + return true; + else + return false; + } +} + +void Map::forEachPointfile(PointfileFunctor func) const +{ + static const char* LIN_EXT = ".lin"; + + const fs::path map(getMapName()); + const fs::path mapDir = map.parent_path(); + const fs::path mapStem = map.stem(); + + // Don't bother trying to iterate over a missing map directory, this will + // just throw an exception. + if (!fs::is_directory(mapDir)) + return; + + // Iterate over files in the map directory, putting them in a sorted set + std::set paths; + for (const auto& entry: fs::directory_iterator(mapDir)) + { + // Ignore anything which isn't a .lin file + auto entryPath = entry.path(); + if (entryPath.extension() == LIN_EXT + && pointfileNameMatch(entryPath.stem(), mapStem)) + { + paths.insert(entryPath); + } + } + + // Call functor on paths in order + for (const fs::path& p: paths) + func(p); +} + +void Map::showPointFile(const fs::path& filePath) +{ + _pointTrace->show(filePath); +} + +bool Map::isPointTraceVisible() const +{ + return _pointTrace->isVisible(); +} + void Map::onSceneNodeErase(const scene::INodePtr& node) { // Detect when worldspawn is removed from the map @@ -326,12 +384,12 @@ MapFormatPtr Map::getFormat() } // free all map elements, reinitialize the structures that depend on them -void Map::freeMap() +void Map::freeMap() { // Abort any ongoing merge abortMergeOperation(); - // Fire the map unloading event, + // Fire the map unloading event, // This will de-select stuff, clear the pointfile, etc. emitMapEvent(MapUnloading); @@ -389,7 +447,7 @@ scene::INodePtr Map::findWorldspawn() // Traverse the scenegraph and search for the worldspawn GlobalSceneGraph().root()->foreachNode([&](const scene::INodePtr& node) { - if (Node_isWorldspawn(node)) + if (Node_isWorldspawn(node)) { worldspawn = node; return false; // done traversing @@ -478,7 +536,7 @@ bool Map::save(const MapFormatPtr& mapFormat) _saveInProgress = false; - // Redraw the views, sometimes the backbuffer containing + // Redraw the views, sometimes the backbuffer containing // the previous frame will remain visible SceneChangeNotify(); @@ -660,7 +718,7 @@ bool Map::saveAs() { if (_saveInProgress) return false; // safeguard - auto fileInfo = MapFileManager::getMapFileSelection(false, + auto fileInfo = MapFileManager::getMapFileSelection(false, _("Save Map"), filetype::TYPE_MAP, getMapName()); if (fileInfo.fullPath.empty()) @@ -674,7 +732,7 @@ bool Map::saveAs() // Create a new resource pointing to the given path... _resource = GlobalMapResourceManager().createFromPath(fileInfo.fullPath); - + // ...and import the existing root node into that resource _resource->setRootNode(oldResource->getRootNode()); @@ -703,7 +761,7 @@ void Map::saveCopyAs() _lastCopyMapName = getMapName(); } - auto fileInfo = MapFileManager::getMapFileSelection(false, + auto fileInfo = MapFileManager::getMapFileSelection(false, _("Save Copy As..."), filetype::TYPE_MAP, _lastCopyMapName); if (!fileInfo.fullPath.empty()) @@ -731,7 +789,7 @@ void Map::loadPrefabAt(const cmd::ArgumentList& args) { if (args.size() < 2 || args.size() > 4) { - rWarning() << "Usage: " << LOAD_PREFAB_AT_CMD << + rWarning() << "Usage: " << LOAD_PREFAB_AT_CMD << " [insertAsGroup:0|1] [recalculatePrefabOrigin:0|1]" << std::endl; return; } @@ -818,7 +876,7 @@ void Map::registerCommands() GlobalCommandSystem().addCommand("SaveSelected", Map::exportSelection); GlobalCommandSystem().addCommand("ReloadSkins", map::algorithm::reloadSkins); GlobalCommandSystem().addCommand("ExportSelectedAsModel", map::algorithm::exportSelectedAsModelCmd, - { cmd::ARGTYPE_STRING, + { cmd::ARGTYPE_STRING, cmd::ARGTYPE_STRING, cmd::ARGTYPE_INT | cmd::ARGTYPE_OPTIONAL, cmd::ARGTYPE_INT | cmd::ARGTYPE_OPTIONAL, @@ -1161,7 +1219,7 @@ const std::string& Map::getName() const return _name; } -const StringSet& Map::getDependencies() const +const StringSet& Map::getDependencies() const { static StringSet _dependencies; @@ -1172,6 +1230,7 @@ const StringSet& Map::getDependencies() const _dependencies.insert(MODULE_MAPINFOFILEMANAGER); _dependencies.insert(MODULE_FILETYPES); _dependencies.insert(MODULE_MAPRESOURCEMANAGER); + _dependencies.insert(MODULE_COMMANDSYSTEM); } return _dependencies; @@ -1192,6 +1251,11 @@ void Map::initialiseModule(const IApplicationContext& ctx) _scaledModelExporter.initialise(); _modelScalePreserver.reset(new ModelScalePreserver); + // Construct point trace and connect it to map signals + _pointTrace.reset(new PointFile()); + signal_mapEvent().connect([this](IMap::MapEvent e) + { _pointTrace->onMapEvent(e); }); + MapFileManager::registerFileTypes(); // Register an info file module to save the map property bag diff --git a/radiantcore/map/Map.h b/radiantcore/map/Map.h index dc42add985..3ad6dc3f77 100644 --- a/radiantcore/map/Map.h +++ b/radiantcore/map/Map.h @@ -13,6 +13,7 @@ #include "model/export/ScaledModelExporter.h" #include "model/export/ModelScalePreserver.h" #include "MapPositionManager.h" +#include "PointFile.h" #include "messages/ApplicationShutdownRequest.h" #include @@ -64,12 +65,14 @@ class Map : util::StopWatch _mapSaveTimer; MapEventSignal _mapEvent; - std::size_t _shutdownListener; scene::merge::MergeOperation::Ptr _mergeOperation; std::list _mergeActionNodes; + // Point trace for leak detection + std::unique_ptr _pointTrace; + private: std::string getSaveConfirmationText() const; @@ -94,20 +97,14 @@ class Map : // Gets called when a node is removed from the scenegraph void onSceneNodeErase(const scene::INodePtr& node) override; - /** greebo: Returns true if the map has not been named yet. - */ + // IMap implementation bool isUnnamed() const override; - - /** greebo: Updates the name of the map (and triggers an update - * of the mainframe window title) - */ void setMapName(const std::string& newName); - - /** greebo: Returns the name of this map - */ std::string getMapName() const override; - sigc::signal& signal_mapNameChanged() override; + void forEachPointfile(PointfileFunctor func) const override; + void showPointFile(const fs::path& filePath) override; + bool isPointTraceVisible() const override; /** * greebo: Saves the current map, doesn't ask for any filenames, @@ -138,7 +135,7 @@ class Map : void saveCopyAs(); /** - * Saves a copy of the current map to the given path, using the + * Saves a copy of the current map to the given path, using the * given format (which may be an empty reference, in which case the map format * will be guessed from the filename). */ @@ -157,7 +154,7 @@ class Map : */ bool import(const std::string& filename); - /** + /** * greebo: Exports the current map directly to the given filename. * This skips any "modified" or "unnamed" checks, it just dumps * the current scenegraph content to the file. @@ -226,7 +223,7 @@ class Map : static void saveSelectedAsPrefab(const cmd::ArgumentList& args); private: - /** + /** * greebo: Asks the user if the current changes should be saved. * * @returns: true, if the user gave clearance (map was saved, had no @@ -241,7 +238,7 @@ class Map : void loadPrefabAt(const cmd::ArgumentList& args); /** - * greebo: Tries to locate the worldspawn in the global scenegraph and + * greebo: Tries to locate the worldspawn in the global scenegraph and * stores it into the local member variable. * Returns the node that was found (can be an empty ptr). */ diff --git a/radiantcore/map/PointFile.cpp b/radiantcore/map/PointFile.cpp index 02c3c2b018..a2c0a310e2 100644 --- a/radiantcore/map/PointFile.cpp +++ b/radiantcore/map/PointFile.cpp @@ -12,6 +12,7 @@ #include "math/Matrix4.h" #include "math/Vector3.h" #include "map/Map.h" +#include "scene/PointTrace.h" #include #include "module/StaticModule.h" @@ -20,17 +21,33 @@ namespace map { +namespace +{ + const Colour4b RED(255, 0, 0, 1); +} + // Constructor PointFile::PointFile() : _points(GL_LINE_STRIP), _curPos(0) -{} +{ + GlobalCommandSystem().addCommand( + "NextLeakSpot", sigc::mem_fun(*this, &PointFile::nextLeakSpot) + ); + GlobalCommandSystem().addCommand( + "PrevLeakSpot", sigc::mem_fun(*this, &PointFile::prevLeakSpot) + ); +} + +PointFile::~PointFile() +{ +} void PointFile::onMapEvent(IMap::MapEvent ev) { if (ev == IMap::MapUnloading || ev == IMap::MapSaved) { - clear(); + show({}); } } @@ -39,17 +56,23 @@ bool PointFile::isVisible() const return !_points.empty(); } -void PointFile::show(bool show) +void PointFile::show(const fs::path& pointfile) { // Update the status if required - if (show) + if (!pointfile.empty()) { // Parse the pointfile from disk - parse(); + parse(pointfile); + + // Construct shader if needed, and activate rendering + if (!_shader) + _shader = GlobalRenderSystem().capture("$POINTFILE"); + GlobalRenderSystem().attachRenderable(*this); } - else - { - _points.clear(); + else if (isVisible()) + { + _points.clear(); + GlobalRenderSystem().detachRenderable(*this); } // Regardless whether hide or show, we reset the current position @@ -59,11 +82,11 @@ void PointFile::show(bool show) SceneChangeNotify(); } -void PointFile::renderSolid(RenderableCollector& collector, const VolumeTest& volume) const +void PointFile::renderSolid(RenderableCollector& collector, const VolumeTest& volume) const { if (isVisible()) { - collector.addRenderable(*_renderstate, _points, Matrix4::getIdentity()); + collector.addRenderable(*_shader, _points, Matrix4::getIdentity()); } } @@ -73,33 +96,25 @@ void PointFile::renderWireframe(RenderableCollector& collector, const VolumeTest } // Parse the current pointfile and read the vectors into the point list -void PointFile::parse() +void PointFile::parse(const fs::path& pointfile) { - // Pointfile is the same as the map file but with a .lin extension - // instead of .map - std::string mapName = GlobalMapModule().getMapName(); - std::string pfName = mapName.substr(0, mapName.rfind(".")) + ".lin"; - - // Open the pointfile and get its input stream if possible - std::ifstream inFile(pfName); - - if (!inFile) + // Open the first pointfile and get its input stream if possible + std::ifstream inFile(pointfile); + if (!inFile) { - throw cmd::ExecutionFailure(fmt::format(_("Could not open pointfile: {0}"), pfName)); - } + throw cmd::ExecutionFailure( + fmt::format(_("Could not open pointfile: {0}"), std::string(pointfile)) + ); + } - // Pointfile is a list of float vectors, one per line, with components - // separated by spaces. - while (inFile.good()) - { - float x, y, z; - inFile >> x; inFile >> y; inFile >> z; - _points.push_back(VertexCb(Vertex3f(x, y, z), Colour4b(255,0,0,1))); - } + // Construct vertices from parsed point data + PointTrace trace(inFile); + for (auto pos: trace.points()) + _points.push_back(VertexCb(pos, RED)); } // advance camera to previous point -void PointFile::advance(bool forward) +void PointFile::advance(bool forward) { if (!isVisible()) { @@ -108,7 +123,7 @@ void PointFile::advance(bool forward) if (forward) { - if (_curPos + 2 >= _points.size()) + if (_curPos + 2 >= _points.size()) { rMessage() << "End of pointfile" << std::endl; return; @@ -167,63 +182,4 @@ void PointFile::prevLeakSpot(const cmd::ArgumentList& args) advance(false); } -void PointFile::clear() -{ - show(false); -} - -void PointFile::toggle(const cmd::ArgumentList& args) -{ - show(!isVisible()); -} - -void PointFile::registerCommands() -{ - GlobalCommandSystem().addCommand("TogglePointfile", sigc::mem_fun(*this, &PointFile::toggle)); - GlobalCommandSystem().addCommand("NextLeakSpot", sigc::mem_fun(*this, &PointFile::nextLeakSpot)); - GlobalCommandSystem().addCommand("PrevLeakSpot", sigc::mem_fun(*this, &PointFile::prevLeakSpot)); -} - -// RegisterableModule implementation -const std::string& PointFile::getName() const -{ - static std::string _name("PointFile"); - return _name; -} - -const StringSet& PointFile::getDependencies() const -{ - static StringSet _dependencies; - - if (_dependencies.empty()) - { - _dependencies.insert(MODULE_COMMANDSYSTEM); - _dependencies.insert(MODULE_RENDERSYSTEM); - _dependencies.insert(MODULE_MAP); - } - - return _dependencies; -} - -void PointFile::initialiseModule(const IApplicationContext& ctx) -{ - rMessage() << getName() << "::initialiseModule called" << std::endl; - - registerCommands(); - - _renderstate = GlobalRenderSystem().capture("$POINTFILE"); - - GlobalRenderSystem().attachRenderable(*this); - - GlobalMap().signal_mapEvent().connect(sigc::mem_fun(*this, &PointFile::onMapEvent)); -} - -void PointFile::shutdownModule() -{ - GlobalRenderSystem().detachRenderable(*this); - _renderstate.reset(); -} - -module::StaticModule pointFileModule; - } // namespace map diff --git a/radiantcore/map/PointFile.h b/radiantcore/map/PointFile.h index f46e715cdf..a1585200d2 100644 --- a/radiantcore/map/PointFile.h +++ b/radiantcore/map/PointFile.h @@ -3,34 +3,32 @@ #include #include "irender.h" #include "imap.h" -#include "imodule.h" #include "icommandsystem.h" #include "irenderable.h" #include "math/Vector3.h" #include "render.h" -namespace map +namespace map { -class PointFile : - public RegisterableModule, - public Renderable +/// Renderable point trace to identify leak positions +class PointFile: public Renderable { -private: // Vector of point coordinates RenderablePointVector _points; - + // Holds the current position in the point file chain std::size_t _curPos; - ShaderPtr _renderstate; + // The shader for rendering the line + ShaderPtr _shader; public: // Constructor PointFile(); // Destructor - virtual ~PointFile() {} + virtual ~PointFile(); // Query whether the point path is currently visible bool isVisible() const; @@ -53,27 +51,14 @@ class PointFile : return Highlight::NoHighlight; } - const std::string& getName() const override; - const StringSet& getDependencies() const override; - void initialiseModule(const IApplicationContext& ctx) override; - void shutdownModule() override; + void onMapEvent(IMap::MapEvent ev); -private: - // Registers the events to the EventManager - void registerCommands(); + /// Show the specified pointfile, or hide if the path is empty + void show(const fs::path& pointfile); - /* - * Toggle the status of the pointfile rendering. If the pointfile must be - * shown, the file is parsed automatically. - */ - void show(bool show); +private: /** - * greebo: Clears the point file vector, which is the same as hiding it. - */ - void clear(); - - /** * greebo: This sets the camera position to the next/prev leak spot. * @forward: pass true to set to the next leak spot, false for the previous */ @@ -81,14 +66,11 @@ class PointFile : // command targets // Toggles visibility of the point file line - void toggle(const cmd::ArgumentList& args); void nextLeakSpot(const cmd::ArgumentList& args); void prevLeakSpot(const cmd::ArgumentList& args); - // Parse the current pointfile and read the vectors into the point list - void parse(); - - void onMapEvent(IMap::MapEvent ev); + // Parse the specified pointfile and read the vectors into the point list + void parse(const fs::path& pointfile); }; } // namespace map diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 8427145c8a..05e892f144 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -27,6 +27,7 @@ add_executable(drtest Models.cpp PatchIterators.cpp PatchWelding.cpp + PointTrace.cpp Prefabs.cpp Renderer.cpp SelectionAlgorithm.cpp diff --git a/test/PointTrace.cpp b/test/PointTrace.cpp new file mode 100644 index 0000000000..bdaa11fa68 --- /dev/null +++ b/test/PointTrace.cpp @@ -0,0 +1,82 @@ +#include "RadiantTest.h" +#include "scene/PointTrace.h" + +#include + +namespace test +{ + +using PointTraceTest = RadiantTest; + +const std::string LIN_DATA = "544.000000 64.000000 112.000000\n" + "544.000000 64.000000 240.000000\n" + "512.000000 64.000000 240.000000\n" + "512.000000 64.000000 112.000000\n" + "544.000000 64.000000 112.000000\n"; + +TEST_F(PointTraceTest, ConstructPointTraceEmpty) +{ + std::string s(""); + std::istringstream ss(s); + + // Constructing with empty data should not crash, or add any undefined or + // [0, 0, 0] points. + map::PointTrace trace(ss); + EXPECT_EQ(trace.points().size(), 0); +} + +TEST_F(PointTraceTest, ConstructPointTraceWithData) +{ + // Construct a stream to read the data + std::istringstream iss(LIN_DATA); + + // Construct the PointTrace to read the stream and confirm the expected + // number of points are parsed + map::PointTrace trace(iss); + auto ps = trace.points(); + ASSERT_EQ(ps.size(), 5); + EXPECT_EQ(ps[0], Vector3(544, 64, 112)); + EXPECT_EQ(ps[1], Vector3(544, 64, 240)); + EXPECT_EQ(ps[2], Vector3(512, 64, 240)); + EXPECT_EQ(ps[3], Vector3(512, 64, 112)); + EXPECT_EQ(ps[4], Vector3(544, 64, 112)); +} + +namespace +{ + +using Paths = std::vector; + +// Get pointfile names in a list +Paths pointfiles() +{ + Paths result; + GlobalMapModule().forEachPointfile([&](const fs::path& pf) + { result.push_back(pf); }); + return result; +} + +} + +TEST_F(PointTraceTest, IdentifyMapPointfiles) +{ + GlobalCommandSystem().executeCommand("OpenMap", std::string("altar.map")); + + // Check the pointfiles for this map + auto pfs = pointfiles(); + ASSERT_EQ(pfs.size(), 2); + EXPECT_EQ(pfs[0].filename(), "altar.lin"); + EXPECT_EQ(pfs[1].filename(), "altar_portalL_544_64_112.lin"); +} + +TEST_F(PointTraceTest, PointFilesAssociatedWithCorrectMap) +{ + std::string modRelativePath = "maps/altar_in_pk4.map"; + GlobalCommandSystem().executeCommand("OpenMap", modRelativePath); + + // No pointfiles should be associated with this map, even though it also + // starts with "altar_" + EXPECT_EQ(pointfiles().size(), 0); +} + +} \ No newline at end of file diff --git a/test/resources/tdm/maps/altar.lin b/test/resources/tdm/maps/altar.lin new file mode 100644 index 0000000000..f6212fb863 --- /dev/null +++ b/test/resources/tdm/maps/altar.lin @@ -0,0 +1,5 @@ +544.000000 64.000000 112.000000 +544.000000 64.000000 240.000000 +512.000000 64.000000 240.000000 +512.000000 64.000000 112.000000 +544.000000 64.000000 112.000000 diff --git a/test/resources/tdm/maps/altar_portalL_544_64_112.lin b/test/resources/tdm/maps/altar_portalL_544_64_112.lin new file mode 100644 index 0000000000..f6212fb863 --- /dev/null +++ b/test/resources/tdm/maps/altar_portalL_544_64_112.lin @@ -0,0 +1,5 @@ +544.000000 64.000000 112.000000 +544.000000 64.000000 240.000000 +512.000000 64.000000 240.000000 +512.000000 64.000000 112.000000 +544.000000 64.000000 112.000000