diff --git a/CHANGELOG b/CHANGELOG index 46abcd48..492b96e1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,6 +9,15 @@ https://glvis.org +Version 4.5.1 (development) +=========================== +- Added an optional point line overlay (useful e.g. for reference curves), which + can be toggled with 'Ctrl+l'. Points can be loaded from a file with the '-pts' + option, sent via socket with the 'pointline' command, or specified in a GLVis + script. The overlay appears as a red line connecting the given points. The + points line format is: number of points, followed by x y z coordinates. + + Version 4.5 released on Feb 6, 2026 =================================== diff --git a/README.md b/README.md index 2a78818e..e74b1a53 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,7 @@ Key commands GLVis will use `SDL` to take screenshots in `bmp` format, which it will then convert to `png` if ImageMagick's `convert` tool is available. - G – 3D scene export to [glTF format](https://www.khronos.org/gltf) +- Ctrl + l – Toggle point line (see `-pts` option) - Ctrl + p – Print to a PDF file using `gl2ps`. Other vector formats (SVG, EPS) are also possible, but keep in mind that the printing takes a while and the generated files are big. diff --git a/glvis.cpp b/glvis.cpp index 18b09126..13db5b78 100644 --- a/glvis.cpp +++ b/glvis.cpp @@ -12,6 +12,7 @@ // GLVis - an OpenGL visualization server based on the MFEM library #include +#include #include #include #include @@ -159,7 +160,8 @@ class Session }; void GLVisServer(int portnum, bool save_stream, bool fix_elem_orient, - bool save_coloring, bool keep_attr, string plot_caption, bool secure, + bool save_coloring, bool keep_attr, string plot_caption, + bool secure, std::vector> point_coords, bool headless = false) { std::vector current_sessions; @@ -312,8 +314,9 @@ void GLVisServer(int portnum, bool save_stream, bool fix_elem_orient, while (1); } - Session new_session(fix_elem_orient, save_coloring, - keep_attr, plot_caption, headless); + Session new_session(fix_elem_orient, save_coloring, keep_attr, + plot_caption, headless); + if (!point_coords.empty()) { new_session.GetState().point_coords = point_coords; } constexpr int tmp_filename_size = 50; char tmp_file[tmp_filename_size]; @@ -389,6 +392,7 @@ int main (int argc, char *argv[]) const char *palette_name = string_none; const char *window_title = string_default; const char *font_name = string_default; + const char *points_file = string_none; int portnum = 19916; bool persistent = true; int multisample = GetMultisample(); @@ -499,6 +503,8 @@ int main (int argc, char *argv[]) args.AddOption(&enable_hidpi, "-hidpi", "--high-dpi", "-nohidpi", "--no-high-dpi", "Enable/disable support for HiDPI at runtime, if supported."); + args.AddOption(&points_file, "-pts", "--points-file", + "Points file: number of points, followed by x y z coordinates."); cout << endl << " _/_/_/ _/ _/ _/ _/" << endl @@ -618,6 +624,21 @@ int main (int argc, char *argv[]) GLVisGeometryRefiner.SetType(geom_ref_type); + // Load points file if specified (Ctrl+l to toggle) + if (points_file != string_none) + { + ifstream ifs(points_file); + if (!ifs) + { + cout << "Cannot open points file: " << points_file << endl; + } + else + { + int num_points = ReadPointLine(ifs, win.data_state.point_coords, cerr); + cout << "Loaded " << num_points << " points from " << points_file << endl; + } + } + string data_type; // check for saved stream file @@ -700,7 +721,9 @@ int main (int argc, char *argv[]) win.data_state.fix_elem_orient, win.data_state.save_coloring, win.data_state.keep_attr, - win.plot_caption, secure, win.headless}; + win.plot_caption, secure, + std::move(win.data_state.point_coords), + win.headless}; // Start message loop in main thread MainThreadLoop(win.headless, persistent); diff --git a/lib/data_state.cpp b/lib/data_state.cpp index 31c222f0..aa161697 100644 --- a/lib/data_state.cpp +++ b/lib/data_state.cpp @@ -10,6 +10,9 @@ // CONTRIBUTING.md for details. #include +#include +#include +#include #include "data_state.hpp" #include "visual.hpp" @@ -36,6 +39,61 @@ class VectorExtrudeCoefficient : public VectorCoefficient QuadratureFunction* Extrude1DQuadFunction(Mesh *mesh, Mesh *mesh2d, QuadratureFunction *qf, int ny); + +int ReadPointLine(std::istream &in, + std::vector> &points, + std::ostream &warn) +{ + points.clear(); + + int num_points = 0; + if (!(in >> num_points)) + { + warn << "Warning: ReadPointLine missing number of points" << std::endl; + return 0; + } + + if (num_points < 2) + { + warn << "Warning: ReadPointLine needs at least 2 points" << std::endl; + return 0; + } + + if (num_points > 0) + { + points.reserve(num_points); + } + + for (int j = 0; j < num_points; j++) + { + double x, y, z; + if (in >> x >> y >> z) + { + points.push_back({x, y, z}); + } + else + { + warn << "Warning: failed reading point " << j << std::endl; + break; + } + } + + const int read_points = (int) points.size(); + if (read_points < 2) + { + warn << "Warning: ReadPointLine needs at least 2 points (got " + << read_points << ")" << std::endl; + } + else if (read_points < num_points) + { + warn << "Warning: ReadPointLine expected " << num_points + << " (got " << read_points << ")" << std::endl; + } + + return read_points; +} + + DataState &DataState::operator=(DataState &&ss) { internal = std::move(ss.internal); @@ -49,6 +107,7 @@ DataState &DataState::operator=(DataState &&ss) save_coloring = ss.save_coloring; keep_attr = ss.keep_attr; cmplx_phase = ss.cmplx_phase; + point_coords = std::move(ss.point_coords); return *this; } diff --git a/lib/data_state.hpp b/lib/data_state.hpp index 18182fc6..550736af 100644 --- a/lib/data_state.hpp +++ b/lib/data_state.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -139,6 +140,7 @@ struct DataState bool save_coloring{false}; bool keep_attr{false}; double cmplx_phase{0.}; + std::vector> point_coords; // point line (from -pts) DataState() = default; DataState(DataState &&ss) { *this = std::move(ss); } @@ -278,4 +280,11 @@ struct DataState { if (cgrid_f) { FindComplexValueRange(minv, maxv, vec2scal, scale); } else { minv = maxv = 0.; } } }; +/// Helper to read a point line file (num_points, followed by x y z coordinates) +/// that is called in glvis.cpp and the 'pointline' commands in threads.cpp and +/// script_controller.cpp. +int ReadPointLine(std::istream &in, + std::vector> &points, + std::ostream &warn); + #endif // GLVIS_DATA_STATE_HPP diff --git a/lib/file_reader.cpp b/lib/file_reader.cpp index 0e5455b9..d046eef3 100644 --- a/lib/file_reader.cpp +++ b/lib/file_reader.cpp @@ -137,18 +137,12 @@ int FileReader::ReadParallel(int np, FileType ft, const char *mesh_file, bool FileReader::CheckStreamIsComplex(std::istream &solin, bool parallel) { - string buff; - auto pos = solin.tellg(); - solin >> ws; - getline(solin, buff); - solin.seekg(pos); - if (!buff.empty() && *buff.rbegin() == '\r') - { - buff.resize(buff.size()-1); - } const char *header = (parallel)?("ParComplexGridFunction"): ("ComplexGridFunction"); - return (buff == header); + solin >> ws; + // Returning characters to ifgzstream is not reliable. The first character + // is enough to distinguish complex headers from FiniteElementSpace. + return (solin.peek() == header[0]); } int FileReader::ReadParMeshAndGridFunction(int np, const char *mesh_prefix, diff --git a/lib/script_controller.cpp b/lib/script_controller.cpp index e8bab160..e283ac53 100644 --- a/lib/script_controller.cpp +++ b/lib/script_controller.cpp @@ -61,6 +61,7 @@ enum class Command Scale, Translate, PlotCaption, + PointLine, Headless, //---------- Max @@ -125,6 +126,7 @@ ScriptCommands::ScriptCommands() (*this)[Command::Scale] = {"scale", "", "Set the scaling factor."}; (*this)[Command::Translate] = {"translate", " ", "Set the translation coordinates."}; (*this)[Command::PlotCaption] = {"plot_caption", "''", "Set the plot caption."}; + (*this)[Command::PointLine] = {"pointline", " ...", "Set point line overlay coordinates."}; (*this)[Command::Headless] = {"headless", "", "Change the session to headless."}; } @@ -843,6 +845,17 @@ bool ScriptController::ExecuteScriptCommand() MyExpose(); } break; + case Command::PointLine: + { + int num_points = ReadPointLine(scr, win.data_state.point_coords, cerr); + + win.vs->SetPointLineVisible(true); + win.vs->PreparePointLine(); + MyExpose(); + + cout << "Script: pointline: " << num_points << " points" << endl; + } + break; case Command::Headless: cout << "The session cannot become headless after initialization" << endl; break; diff --git a/lib/threads.cpp b/lib/threads.cpp index d6c4690d..e6dd8b6c 100644 --- a/lib/threads.cpp +++ b/lib/threads.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -240,6 +241,21 @@ int GLVisCommand::ColorbarNumberFormat(string formatting) return 0; } +int GLVisCommand::PointLine(std::vector> points) +{ + if (lock() < 0) + { + return -1; + } + command = Command::POINT_LINE; + point_coords = std::move(points); + if (signal() < 0) + { + return -2; + } + return 0; +} + int GLVisCommand::Pause() { if (lock() < 0) @@ -839,6 +855,17 @@ int GLVisCommand::Execute() break; } + case Command::POINT_LINE: + { + cout << "Command: pointline: " << point_coords.size() + << " points" << endl; + win.data_state.point_coords = std::move(point_coords); + win.vs->SetPointLineVisible(true); + win.vs->PreparePointLine(); + MyExpose(); + break; + } + case Command::QUIT: { thread_wnd->signalQuit(); @@ -916,6 +943,7 @@ enum class ThreadCommand AxisLabels, Pause, Autopause, + PointLine, //---------- Max }; @@ -968,6 +996,7 @@ ThreadCommands::ThreadCommands() (*this)[ThreadCommand::AxisLabels] = {"axis_labels", "'' '' ''", "Set labels of the axes."}; (*this)[ThreadCommand::Pause] = {"pause", "", "Stop the stream until space is pressed."}; (*this)[ThreadCommand::Autopause] = {"autopause", "<0/off/1/on>", "Turns off or on autopause."}; + (*this)[ThreadCommand::PointLine] = {"pointline", " ...", "Set point line overlay coordinates."}; } communication_thread::communication_thread(StreamCollection _is, @@ -1594,6 +1623,27 @@ bool communication_thread::execute_one(std::string ident) } } break; + case ThreadCommand::PointLine: + { + // in parallel, use the point line data from rank 0 + std::vector> points; + int num_points = ReadPointLine(*is[0], points, cerr); + + // assuming that all ranks have sent the same point data, perform dummy + // reads on the remaining ranks to sync parallel streams + std::vector> dummy_points; + std::ostringstream dummy_stream; + for (size_t i = 1; i < is.size(); i++) + { + *is[i] >> ws >> ident; // 'pointline' + ReadPointLine(*is[i], dummy_points, dummy_stream); + } + + glvis_command->PointLine(std::move(points)); + + cout << "Received " << num_points << " points" << endl; + } + break; case ThreadCommand::Max: //dummy break; } diff --git a/lib/threads.hpp b/lib/threads.hpp index 8b3248ef..4217a32a 100644 --- a/lib/threads.hpp +++ b/lib/threads.hpp @@ -12,6 +12,7 @@ #ifndef GLVIS_THREADS_HPP #define GLVIS_THREADS_HPP +#include #include #include #include @@ -62,6 +63,7 @@ class GLVisCommand LEVELLINES, AXIS_NUMBERFORMAT, COLORBAR_NUMBERFORMAT, + POINT_LINE, QUIT }; @@ -97,6 +99,7 @@ class GLVisCommand std::string autopause_mode; std::string axis_formatting; std::string colorbar_formatting; + std::vector> point_coords; // internal variables int autopause; @@ -137,6 +140,7 @@ class GLVisCommand int Levellines(double minv, double maxv, int number); int AxisNumberFormat(std::string formatting); int ColorbarNumberFormat(std::string formatting); + int PointLine(std::vector> points); int Camera(const double cam[]); int Autopause(const char *mode); int Quit(); diff --git a/lib/vsdata.cpp b/lib/vsdata.cpp index 8b01802a..5853e7b7 100644 --- a/lib/vsdata.cpp +++ b/lib/vsdata.cpp @@ -700,9 +700,16 @@ void VisualizationSceneScalarData::KeyHPressed() cout << vsdata->GetHelpString() << flush; } -void VisualizationSceneScalarData::KeylPressed() +void VisualizationSceneScalarData::KeylPressed(GLenum state) { - vsdata -> ToggleLight(); + if (state & KMOD_CTRL) + { + vsdata->TogglePointLine(); + } + else + { + vsdata -> ToggleLight(); + } SendExposeEvent(); } @@ -1002,6 +1009,7 @@ void VisualizationSceneScalarData::ToggleLogscale(bool print) SetLevelLines(minv, maxv, nl); UpdateLevelLines(); EventUpdateColors(); + PreparePointLine(); if (print) { PrintLogscale(false); @@ -1020,6 +1028,18 @@ void VisualizationSceneScalarData::ToggleRuler() PrepareRuler(); } +void VisualizationSceneScalarData::TogglePointLine() +{ + if (win.data_state.point_coords.empty()) + { + cout << "No points loaded. Use -pts to load coordinates." << endl; + return; + } + show_point_line = !show_point_line; + cout << "Point line: " << (show_point_line ? "ON" : "OFF") << endl; + PreparePointLine(); +} + void VisualizationSceneScalarData::RulerPosition() { cout << "Current ruler position: (" << ruler_x << ',' @@ -1669,6 +1689,56 @@ void VisualizationSceneScalarData::PrepareAxes() updated_bufs.emplace_back(&coord_cross_buf); } +void VisualizationSceneScalarData::PreparePointLine() +{ + point_line_buf.clear(); + if (!show_point_line || win.data_state.point_coords.empty()) + { + return; + } + + const auto& points = win.data_state.point_coords; + // in 2D, elevate the point line above the surface using the value range + // in 3D, use the z-coordinates from the points file. + const bool is_2d = (mesh->SpaceDimension() == 2); + float z_offset = 0.0f; + if (is_2d) + { + const auto range = maxv - minv; + const float dz = (range > 0.0) ? (0.02 * range) + : (0.02 * std::max(1.0, std::abs(maxv))); + z_offset = (float)maxv + dz; + } + + std::vector line_vertices; + line_vertices.reserve((points.size()-1)*2); + for (size_t i = 0; i + 1 < points.size(); i++) + { + float x0 = points[i][0]; + float y0 = points[i][1]; + float x1 = points[i+1][0]; + float y1 = points[i+1][1]; + float z = is_2d ? z_offset : points[i][2]; + float z_next = is_2d ? z_offset : points[i+1][2]; + line_vertices.push_back({x0, y0, z}); + line_vertices.push_back({x1, y1, z_next}); + } + point_line_buf.addLines(line_vertices); + updated_bufs.emplace_back(&point_line_buf); +} + +void VisualizationSceneScalarData::AddPointLineToScene( + gl3::SceneInfo& scene, const gl3::RenderParams& base_params) +{ + if (!show_point_line || win.data_state.point_coords.empty()) { return; } + gl3::RenderParams params = base_params; // use local parameters + params.static_color = {1.0f, 0.0f, 0.0f, 1.0f}; // point line is red + params.use_clip_plane = false; + params.num_pt_lights = 0; + params.contains_translucent = false; + scene.queue.emplace_back(params, &point_line_buf); +} + void VisualizationSceneScalarData::DrawPolygonLevelLines( gl3::GlBuilder& builder, double * point, int n, Array &mesh_level, bool log_vals) diff --git a/lib/vsdata.hpp b/lib/vsdata.hpp index 9ed63cf8..d844cb0a 100644 --- a/lib/vsdata.hpp +++ b/lib/vsdata.hpp @@ -104,6 +104,7 @@ class VisualizationSceneScalarData : public VisualizationScene gl3::GlDrawable color_bar; gl3::GlDrawable ruler_buf; gl3::GlDrawable caption_buf; + gl3::GlDrawable point_line_buf; int caption_w, caption_h; void Init(); @@ -126,6 +127,7 @@ class VisualizationSceneScalarData : public VisualizationScene Autoscale autoscale; bool logscale; + bool show_point_line{false}; bool LogscaleRange() { return (minv > 0.0 && maxv > minv); } void PrintLogscale(bool warn); @@ -175,7 +177,7 @@ class VisualizationSceneScalarData : public VisualizationScene static void KeyaPressed(); static void Key_Mod_a_Pressed(GLenum state); static void KeyHPressed(); - static void KeylPressed(); + static void KeylPressed(GLenum state); static void KeyLPressed(); static void KeyrPressed(); static void KeyRPressed(); @@ -346,6 +348,13 @@ class VisualizationSceneScalarData : public VisualizationScene } } + void PreparePointLine(); + void TogglePointLine(); + void SetPointLineVisible(bool visible) { show_point_line = visible; } + /// Helper for adding point line overlay in derived classes + void AddPointLineToScene(gl3::SceneInfo& scene, + const gl3::RenderParams& params); + void ToggleScaling() { scaling = !scaling; SetNewScalingFromBox(); } diff --git a/lib/vssolution.cpp b/lib/vssolution.cpp index 1c7fc7ff..267dd335 100644 --- a/lib/vssolution.cpp +++ b/lib/vssolution.cpp @@ -87,6 +87,7 @@ std::string VisualizationSceneSolution::GetHelpString() const << "| Alt+a - Axes number format |" << endl << "| Alt+c - Colorbar number format |" << endl << "| Alt+n - Numberings method |" << endl + << "| Ctrl+l - Toggle point line |" << endl << "| Ctrl+o - Element ordering curve |" << endl << "| Ctrl+p - Print to a PDF file |" << endl << "+------------------------------------+" << endl @@ -2437,6 +2438,7 @@ void VisualizationSceneSolution::UpdateValueRange(bool prepare) bb.z[0] = minv; bb.z[1] = maxv; PrepareAxes(); + PreparePointLine(); if (prepare) { UpdateLevelLines(); @@ -2772,6 +2774,7 @@ gl3::SceneInfo VisualizationSceneSolution::GetSceneObjs() { scene.queue.emplace_back(params, &order_buf); } + AddPointLineToScene(scene, params); // point line overlay ProcessUpdatedBufs(scene); return scene; diff --git a/lib/vssolution3d.cpp b/lib/vssolution3d.cpp index 5d917fd8..0173c033 100644 --- a/lib/vssolution3d.cpp +++ b/lib/vssolution3d.cpp @@ -88,6 +88,7 @@ std::string VisualizationSceneSolution3d::GetHelpString() const << "| \\ - Set light source position |" << endl << "| Alt+a - Axes number format |" << endl << "| Alt+c - Colorbar number format |" << endl + << "| Ctrl+l - Toggle point line |" << endl << "| Ctrl+o - Element ordering curve |" << endl << "| Ctrl+p - Print to a PDF file |" << endl << "+------------------------------------+" << endl @@ -4293,6 +4294,7 @@ gl3::SceneInfo VisualizationSceneSolution3d::GetSceneObjs() { scene.queue.emplace_back(params, &order_buf); } + AddPointLineToScene(scene, params); // point line overlay ProcessUpdatedBufs(scene); return scene; } diff --git a/lib/vsvector.cpp b/lib/vsvector.cpp index 1417d67d..ca4a3aa0 100644 --- a/lib/vsvector.cpp +++ b/lib/vsvector.cpp @@ -72,6 +72,7 @@ std::string VisualizationSceneVector::GetHelpString() const << "| \\ - Set light source position |" << endl << "| Alt+a - Axes number format |" << endl << "| Alt+c - Colorbar number format |" << endl + << "| Ctrl+l - Toggle point line |" << endl << "| Ctrl+p - Print to a PDF file |" << endl << "+------------------------------------+" << endl << "| Function keys |" << endl @@ -1131,6 +1132,7 @@ gl3::SceneInfo VisualizationSceneVector::GetSceneObjs() } scene.queue.emplace_back(params, &displine_buf); } + AddPointLineToScene(scene, params); // point line overlay ProcessUpdatedBufs(scene); return scene; diff --git a/lib/vsvector3d.cpp b/lib/vsvector3d.cpp index 789fff5e..a46a5cc8 100644 --- a/lib/vsvector3d.cpp +++ b/lib/vsvector3d.cpp @@ -84,6 +84,7 @@ std::string VisualizationSceneVector3d::GetHelpString() const << "| \\ - Set light source position |" << endl << "| Alt+a - Axes number format |" << endl << "| Alt+c - Colorbar number format |" << endl + << "| Ctrl+l - Toggle point line |" << endl << "| Ctrl+p - Print to a PDF file |" << endl << "+------------------------------------+" << endl << "| Function keys |" << endl @@ -1794,6 +1795,7 @@ gl3::SceneInfo VisualizationSceneVector3d::GetSceneObjs() params.use_clip_plane = false; scene.queue.emplace_back(params, &cplines_buf); } + AddPointLineToScene(scene, params); // point line overlay ProcessUpdatedBufs(scene); return scene; } diff --git a/lib/window.cpp b/lib/window.cpp index eea18226..6930f6f0 100644 --- a/lib/window.cpp +++ b/lib/window.cpp @@ -162,6 +162,11 @@ bool Window::GLVisInitVis(StreamCollection input_streams) vs->SetValueRange(-mesh_range, mesh_range); vs->SetAutoscale(VisualizationSceneScalarData::Autoscale::None); } + if (!data_state.point_coords.empty()) + { + vs->SetPointLineVisible(true); + vs->PreparePointLine(); + } if (data_state.mesh->SpaceDimension() == 2 && field_type == DataState::FieldType::MESH) {