diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 59fb9a66..5bb244d2 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -197,6 +197,12 @@ if(ARMADILLO_FOUND) target_link_libraries(grid_border GLEW::GLEW) endif() + add_executable(grid_border2 grid_border2.cpp) + target_link_libraries(grid_border2 OpenGL::GL glfw Freetype::Freetype) + if(USE_GLEW) + target_link_libraries(grid_border2 GLEW::GLEW) + endif() + # Like HexGrid::resampleImage, this one uses an omp call that only works on Mac with # libomp, so avoid compiling this example on a Mac. @@ -358,6 +364,12 @@ if(USE_GLEW) target_link_libraries(graph1 GLEW::GLEW) endif() +add_executable(graph_line graph_line.cpp) +target_link_libraries(graph_line OpenGL::GL glfw Freetype::Freetype) +if(USE_GLEW) + target_link_libraries(graph_line GLEW::GLEW) +endif() + add_executable(graph_dynamic_x2 graph_dynamic_x2.cpp) target_link_libraries(graph_dynamic_x2 OpenGL::GL glfw Freetype::Freetype) if(USE_GLEW) @@ -748,3 +760,15 @@ target_link_libraries(healpix OpenGL::GL glfw Freetype::Freetype) if(USE_GLEW) target_link_libraries(healpix GLEW::GLEW) endif() + +add_executable(lines lines.cpp) +target_link_libraries(lines OpenGL::GL glfw Freetype::Freetype) +if(USE_GLEW) + target_link_libraries(lines GLEW::GLEW) +endif() + +add_executable(line line.cpp) +target_link_libraries(line OpenGL::GL glfw Freetype::Freetype) +if(USE_GLEW) + target_link_libraries(line GLEW::GLEW) +endif() diff --git a/examples/graph_line.cpp b/examples/graph_line.cpp new file mode 100644 index 00000000..aa8dafaf --- /dev/null +++ b/examples/graph_line.cpp @@ -0,0 +1,34 @@ +// A line graph showing how line segments work nicely + +#include +#include +#include + +int main() +{ + // Set up a morph::Visual 'scene environment'. + morph::Visual v(1024, 768, "Made with morph::GraphVisual"); + // Create a GraphVisual object (obtaining a unique_ptr to the object) with a spatial offset within the scene of 0,0,0 + auto gv = std::make_unique> (morph::vec({0,0,0})); + // This mandatory line of boilerplate code sets the parent pointer in GraphVisual and binds some functions + v.bindmodel (gv); + // Data for the x axis. A vvec is like std::vector, but with built-in maths methods + morph::vvec x; + // This works like numpy's linspace() (the 3 args are "start", "end" and "num"): + x.linspace (0, 10, 11); + // Hand chosen numbers + morph::vvec y = { 5, 8, 2, 9, 1, 2, 4, 5, 8, 0, 1 }; + // Choose a line graph by crezating a lines stylepolicy datasetstyle. + morph::DatasetStyle ds (morph::stylepolicy::lines); + ds.linecolour = morph::colour::crimson; + // Now set the data + gv->setdata (x, y, ds); + // finalize() makes the GraphVisual compute the vertices of the OpenGL model + gv->finalize(); + // Add the GraphVisual OpenGL model to the Visual scene, transferring ownership of the unique_ptr + v.addVisualModel (gv); + // Render the scene on the screen until user quits with 'Ctrl-q' + v.keepOpen(); + // Because v owns the unique_ptr to the GraphVisual, its memory will be deallocated when v goes out of scope. + return 0; +} diff --git a/examples/grid_border2.cpp b/examples/grid_border2.cpp new file mode 100644 index 00000000..316a6444 --- /dev/null +++ b/examples/grid_border2.cpp @@ -0,0 +1,64 @@ +/* + * An example morph::Visual scene, containing a Grid, and using GridVisual. This is for + * debugging/demonstrating grid borders. see aso grid_border.cpp + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +int main() +{ + morph::Visual v(1600, 1000, "GridVisual borders"); + + // Create a grid to show in the scene + constexpr unsigned int Nside = 4; // You can change this + constexpr morph::vec grid_spacing = {0.5f, 0.5f}; + morph::Grid grid(Nside, Nside, grid_spacing); + std::cout << "Number of pixels in grid:" << grid.n() << std::endl; + + // Make some dummy data (a sine wave) to make an interesting surface + std::vector data(grid.n(), 0.0); + + // Set data + constexpr float length = morph::mathconst::pi_over_4; + for (unsigned int ri=0; ri1 + } + + float step = 0.6f; + // Add a GridVisual to display the Grid within the morph::Visual scene + morph::vec offset = { -step * grid.width(), -step * grid.width(), 0.0f }; + + auto gv = std::make_unique>(&grid, offset); + v.bindmodel (gv); + gv->gridVisMode = morph::GridVisMode::RectInterp; + gv->setScalarData (&data); + gv->cm.setType (morph::ColourMapType::Cork); + gv->zScale.do_autoscale = false; + gv->zScale.setParams (0, 0); + gv->colourScale.do_autoscale = false; + gv->colourScale.compute_scaling (-1, 1); + + // Border specific parameters + gv->showborder = true; + gv->border_thickness = 0.15f; // of a pixel + gv->border_z_offset = 0.0f; + gv->border_colour = morph::colour::black; + + gv->finalize(); + v.addVisualModel (gv); + + v.keepOpen(); + + return 0; +} diff --git a/examples/line.cpp b/examples/line.cpp new file mode 100644 index 00000000..014aa38a --- /dev/null +++ b/examples/line.cpp @@ -0,0 +1,57 @@ +/* + * Draw a line (made of a few segments) + */ + +#include +#include + +namespace morph { + + // A test VisualModel which draws some lines + template + struct LinestestVisual : public VisualModel + { + LinestestVisual() : morph::VisualModel() {} + + void initializeVertices() + { + float lth = 0.1f; // line thickness + + // Draw 3 lines with a different angle and one extra line + morph::vvec> p(5); + p[0] = {-0.5, -0.5, 0}; + p[1] = {0, 0, 0}; + p[2] = {1, 0, 0}; + p[3] = {1.5, 0.5, 0}; + p[4] = {2, 0, 0}; + + this->computeFlatLine (p[0], p[1], + p[0], p[2], + this->uz, morph::colour::black, lth); +#if 1 + this->computeFlatLine (p[1], p[2], + p[0], p[3], + this->uz, morph::colour::crimson, lth); + this->computeFlatLine (p[2], p[3], + p[1], p[4], + this->uz, morph::colour::goldenrod1, lth); + this->computeFlatLine (p[3], p[4], + p[2], p[4], + this->uz, morph::colour::dodgerblue2, lth); +#endif + } + }; + +} // morph + +// Main program simply creates a Visual and places a LinestestVisual object in it. +int main() +{ + morph::Visual v(1024, 768, "Lines"); + auto vm = std::make_unique>(); + v.bindmodel (vm); + vm->finalize(); + v.addVisualModel (vm); + v.keepOpen(); + return 0; +} diff --git a/examples/lines.cpp b/examples/lines.cpp new file mode 100644 index 00000000..f3200efe --- /dev/null +++ b/examples/lines.cpp @@ -0,0 +1,111 @@ +/* + * Demonstrates the VisualModel line-drawing primitives. Make sure they're visually + * correct. + */ + +#include +#include + +namespace morph { + + // A test VisualModel which draws some lines + template + struct LinestestVisual : public VisualModel + { + LinestestVisual() : morph::VisualModel() {} + + void initializeVertices() + { + float lth = 0.1f; // line thickness + + // A black horizontal line, length 1, width 0.1 + this->computeFlatLine ({0, 0, 0}, {1, 0, 0}, this->uz, morph::colour::black, lth, 0.0f); + + // Moving up, a black horz line with rounded ends + this->computeFlatLineRnd ({0, 0.5, 0}, {1, 0.5, 0}, this->uz, morph::colour::black, lth, 0.0f); + + // Draw 3 lines to see if the line-joining build in to this overload of + // VisualModel::computeFlatLine is doing the right thing + this->computeFlatLine ({-0.5, 1-0.2, 0}, {0, 1, 0}, + {-0.5, 1-0.2, 0}, {1, 1, 0}, + this->uz, + morph::colour::black, + lth); + this->computeFlatLine ({0, 1, 0}, {1, 1, 0}, + {-0.5, 1-0.2, 0}, {1+0.5, 1+0.2, 0}, + this->uz, + morph::colour::crimson, + lth); + this->computeFlatLine ({1, 1, 0}, {1+0.5, 1+0.2, 0}, + {0, 1, 0}, {1+0.5, 1+0.2, 0}, + this->uz, + morph::colour::goldenrod1, + lth); + + // Draw 3 lines with a different angle and one extra line + vec p1 = {-0.5, 1.5, 0}; + vec p2 = {0, 2, 0}; + vec p3 = {1, 2, 0}; + vec p4 = {1.5, 2.5, 0}; + vec p5 = {2, 2, 0}; + + this->computeFlatLine (p1, p2, + p1, p3, + this->uz, morph::colour::black, lth); + this->computeFlatLine (p2, p3, + p1, p4, + this->uz, morph::colour::crimson, lth); + this->computeFlatLine (p3, p4, + p2, p5, + this->uz, morph::colour::goldenrod1, lth); + this->computeFlatLine (p4, p5, + p3, p5, + this->uz, morph::colour::dodgerblue2, lth); + + // Draw a square with computeFlatLine + float left = 0.0f; + float right = 1.0f; + float bot = 3.0f; + float top = 4.0f; + morph::vec lb = { left, bot, 0.0f }; + morph::vec lt = { left, top, 0.0f }; + morph::vec rt = { right, top, 0.0f }; + morph::vec rb = { right, bot, 0.0f }; + // draw the vertical from bottom left to top left + this->computeFlatLine (lb, lt, rb, rt, this->uz, morph::colour::black, lth); + // draw the horizontal from bottom left to bottom right + this->computeFlatLine (rb, lb, rt, lt, this->uz, morph::colour::crimson, lth); + // complete the last right border (from bottom right to top right) + this->computeFlatLine (rt, rb, lt, lb, this->uz, morph::colour::goldenrod1, lth); + // complete the last top border (from top left to top right) + this->computeFlatLine (lt, rt, lb, rb, this->uz, morph::colour::dodgerblue1, lth); + + // Test a straight line + morph::vec p0 = { 0, 4.5, 0 }; + p1 = { 1, 4.5, 0 }; + p2 = { 2, 4.5, 0 }; + this->computeFlatLine (p0, p1, p0, p2, this->uz, morph::colour::black, lth); + this->computeFlatLine (p1, p2, p0, p2, this->uz, morph::colour::black, lth); + + // Different dirn vector (uy, not uz). This fails to work as expected. + p0 = { 0, 5, 0 }; + p1 = { 1, 5, 1 }; + p2 = { 2, 5, 0 }; + this->computeFlatLine (p0, p1, p0, p2, this->uy, morph::colour::black, lth); + this->computeFlatLine (p1, p2, p0, p2, this->uy, morph::colour::black, lth); + } + }; + +} // morph + +// Main program simply creates a Visual and places a LinestestVisual object in it. +int main() +{ + morph::Visual v(1024, 768, "Lines"); + auto vm = std::make_unique>(); + v.bindmodel (vm); + vm->finalize(); + v.addVisualModel (vm); + v.keepOpen(); + return 0; +} diff --git a/morph/GridVisual.h b/morph/GridVisual.h index 5973adec..dc94ba7f 100644 --- a/morph/GridVisual.h +++ b/morph/GridVisual.h @@ -94,20 +94,12 @@ namespace morph { morph::vec cg_extents = this->grid->extents(); // {xmin, xmax, ymin, ymax} morph::vec dx = this->grid->get_dx(); float bthick = this->border_thickness_fixed ? this->border_thickness_fixed : dx[0] * this->border_thickness; - float bz = 0.02f; float left = cg_extents[0] - (dx[0]/2.0f) + this->centering_offset[0]; float right = cg_extents[1] + (dx[0]/2.0f) + this->centering_offset[0]; float bot = cg_extents[2] - (dx[1]/2.0f) + this->centering_offset[1]; float top = cg_extents[3] + (dx[1]/2.0f) + this->centering_offset[1]; - morph::vec lb = {{left, bot, bz}}; // z? - morph::vec lt = {{left, top, bz}}; - morph::vec rt = {{right, top, bz}}; - morph::vec rb = {{right, bot, bz}}; - - this->computeFlatLine(lb, lt, rb, rt, this->uz, this->border_colour, bthick); - this->computeFlatLine(lt, rt, lb, rb, this->uz, this->border_colour, bthick); - this->computeFlatLine(rt, rb, lt, lb, this->uz, this->border_colour, bthick); - this->computeFlatLine(rb, lb, rt, lt, this->uz, this->border_colour, bthick); + morph::vec r_extents = { left, right, bot, top }; + this->rectangularBorder (r_extents, this->border_z_offset, bthick, this->border_colour); } //! function to draw the grid (border around each pixel) @@ -152,46 +144,104 @@ namespace morph { // Draw around all pixels morph::vec cg_extents = this->grid->extents(); // {xmin, xmax, ymin, ymax} morph::vec dx = this->grid->get_dx(); - float gridthick = this->grid_thickness_fixed ? this->grid_thickness_fixed : dx[0] * this->grid_thickness; - float bz = 0.05f; + float gridthick = this->grid_thickness_fixed ? this->grid_thickness_fixed : dx[0] * this->grid_thickness; unsigned int pix_width = static_cast(std::round((cg_extents[1] - cg_extents[0] + dx[0])/dx[0])); - // check if the size of selected_pix_border_colour is the same as the size of selected_pix_indexes - if (selected_pix_indexes.size()>selected_pix_border_colour.size()){ - std::cerr << "[GridVisual::drawSelectedPixBorder] the number of pixel indices is higher than the number of colours," - << " the last color will be used for the remaining pixels" << std::endl; - while(selected_pix_border_colour.size() < selected_pix_indexes.size()) { - selected_pix_border_colour.push_back(selected_pix_border_colour.back()); - } + // If user has NOT resized and populated selected_pix_border_colour AND + // selected_pix_indexes, resize and default the colour here. + if (this->selected_pix_border_colour.size() < this->selected_pix_indexes.size()) { + this->selected_pix_border_colour.resize (this->selected_pix_indexes.size(), this->border_colour); } float grid_left = cg_extents[0] - (dx[0]/2.0f) + this->centering_offset[0]; float grid_bot = cg_extents[2] - (dx[1]/2.0f) + this->centering_offset[1]; // loop through each pixel - for (unsigned int i=0; i < selected_pix_indexes.size(); ++i ) { + for (std::size_t i=0; i < selected_pix_indexes.size(); ++i ) { unsigned int r = selected_pix_indexes[i] % pix_width; unsigned int c = selected_pix_indexes[i] / pix_width; + // {xmin, xmax, ymin, ymax} + morph::vec r_extents = { (grid_left + r * dx[0]), (grid_left + (r+1) * dx[0]), (grid_bot + c * dx[1]), (grid_bot + (c+1) * dx[1]) }; + this->rectangularBorder (r_extents, this->grid_z_offset, gridthick, this->selected_pix_border_colour[i]); + } + } + + // Draw a GridVisual border + // r_extents: rectangular extents of the Grid + void rectangularBorder (const morph::vec& r_extents, + const float bz, const float linethickness, + const std::array& clr) + { + morph::vec lb = { r_extents[0], r_extents[2], bz }; + morph::vec lt = { r_extents[0], r_extents[3], bz }; + morph::vec rt = { r_extents[1], r_extents[3], bz }; + morph::vec rb = { r_extents[1], r_extents[2], bz }; +#if 1 + morph::vec lbout = { r_extents[0] - linethickness, r_extents[2] - linethickness, bz }; + morph::vec ltout = { r_extents[0] - linethickness, r_extents[3] + linethickness, bz }; + morph::vec rtout = { r_extents[1] + linethickness, r_extents[3] + linethickness, bz }; + morph::vec rbout = { r_extents[1] + linethickness, r_extents[2] - linethickness, bz }; + + this->computeFlatQuad (lbout, ltout, lt, lb, clr); + this->computeFlatQuad (lt, ltout, rtout, rt, clr); + this->computeFlatQuad (rt, rtout, rbout, rb, clr); + this->computeFlatQuad (rb, rbout, lbout, lb, clr); +#else + // draw the vertical from bottom left to top left + this->computeFlatLine(lb, lt, rb, rt, this->uz, clr, linethickness); + // draw the horizontal from bottom left to bottom right + this->computeFlatLine(rb, lb, rt, lt, this->uz, clr, linethickness); + // draw the vertical from bottom right to top right + this->computeFlatLine(rt, rb, lt, lb, this->uz, clr, linethickness); + // draw the horizontal from top left to top right + this->computeFlatLine(lt, rt, lb, rb, this->uz, clr, linethickness); +#endif + } + + //! Draw a border around the selected pixels, using the first selected pix colour + void drawSelectedPixBorderEnclosing() + { + // Draw around all pixels + morph::vec cg_extents = this->grid->extents(); // {xmin, xmax, ymin, ymax} + morph::vec dx = this->grid->get_dx(); + float gridthick = this->grid_thickness_fixed ? this->grid_thickness_fixed : dx[0] * this->grid_thickness; + + unsigned int pix_width = static_cast(std::round((cg_extents[1] - cg_extents[0] + dx[0])/dx[0])); + + if (this->selected_pix_border_colour.empty()) { + this->selected_pix_border_colour.push_back (this->border_colour); + } + + float grid_left = cg_extents[0] - (dx[0]/2.0f) + this->centering_offset[0]; + float grid_bot = cg_extents[2] - (dx[1]/2.0f) + this->centering_offset[1]; + morph::range l_r; // left extent range + l_r.search_init(); + morph::range r_r; + r_r.search_init(); + morph::range b_r; + b_r.search_init(); + morph::range t_r; + t_r.search_init(); + + // Find extents of our selected pixels + for (std::size_t i = 0; i < this->selected_pix_indexes.size(); ++i) { + I r = this->selected_pix_indexes[i] % pix_width; + I c = this->selected_pix_indexes[i] / pix_width; float left = grid_left + (r * dx[0]); float right = left + dx[0]; float bot = grid_bot + (c * dx[1]); float top = bot + dx[1]; - morph::vec lb = {{left, bot, bz}}; // z? - morph::vec lt = {{left, top, bz}}; - morph::vec rt = {{right, top, bz}}; - morph::vec rb = {{right, bot, bz}}; - - // draw the vertical from bottom left to top left - this->computeFlatLine(lb, lt, rb, rt, this->uz, this->selected_pix_border_colour[i], gridthick); - // draw the horizontal from bottom left to bottom right - this->computeFlatLine(rb, lb, rt, lt, this->uz, this->selected_pix_border_colour[i], gridthick); - // draw the vertical from bottom right to top right - this->computeFlatLine(rt, rb, lt, lb, this->uz, this->selected_pix_border_colour[i], gridthick); - // draw the horizontal from top left to top right - this->computeFlatLine(lt, rt, lb, rb, this->uz, this->selected_pix_border_colour[i], gridthick); + l_r.update (left); + r_r.update (right); + b_r.update (bot); + t_r.update (top); } + + // xmin xmax ymin ymax + morph::vec r_extents = { l_r.min, r_r.max, b_r.min, t_r.max }; + this->rectangularBorder (r_extents, this->grid_z_offset, gridthick, this->selected_pix_border_colour[0]); } // Common function to setup scaling. Called by all initializeVertices subroutines. Also @@ -286,7 +336,10 @@ namespace morph { this->drawGrid(); } if (this->showselectedpixborder == true) { - this->drawSelectedPixBorder(); + this->drawSelectedPixBorder(); + } + if (this->showselectedpixborder_enclosing == true) { + this->drawSelectedPixBorderEnclosing(); } if (this->showorigin == true) { this->computeSphere (morph::vec{0, 0, 0}, morph::colour::crimson, 0.25f * this->grid->get_dx()[0]); @@ -807,6 +860,9 @@ namespace morph { //! If you need to override the pixels-relationship to the grid thickness, set it here float grid_thickness_fixed = 0.0f; + //! How far in z to locate the grid lines? + float grid_z_offset = 0.02f; + //! Set true to draw a border around the outside bool showborder = false; @@ -819,16 +875,25 @@ namespace morph { //! If you need to override the pixels-relationship to the border thickness, set it here float border_thickness_fixed = 0.0f; - //! new option for border around selected pixels + //! Where in z to locate the border lines? + float border_z_offset = 0.02f; + + /*! + * If true, draw a border around selected pixels (with a full border around each selected + * pixel). The selected pixels are chosen by the client code, which should populate + * selected_pix_indexes. + */ bool showselectedpixborder = false; - //! list of the pixel to have a border - std::vector selected_pix_indexes; + //! If true, draw a rectangular border enclosing the selected pixels + bool showselectedpixborder_enclosing = false; + + //! list of those pixel indices that should be drawn with a border + std::vector selected_pix_indexes; //! The colour for the border std::vector> selected_pix_border_colour; - // If true, interpolate the colour of the sides of columns on a column grid bool interpolate_colour_sides = false; diff --git a/morph/MathAlgo.h b/morph/MathAlgo.h index 3623c599..70d49cdd 100644 --- a/morph/MathAlgo.h +++ b/morph/MathAlgo.h @@ -282,7 +282,7 @@ namespace morph { /*! * Find the coordinate of the crossing point of the two line segments p1-q1 and p2-q2, * *assuming* the segments intersect. Call this *after* you have used - * MathAlgo::segments_intesect! + * MathAlgo::segments_intersect! * * \param p1 Start of line segment 1 * \param q1 End of line segment 1 diff --git a/morph/VisualModel.h b/morph/VisualModel.h index a78f0a67..5fc5430b 100644 --- a/morph/VisualModel.h +++ b/morph/VisualModel.h @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -1982,7 +1983,7 @@ namespace morph { vend = end - v * shorten; } - // vv is normal to v and uz + // vv is normal to v and _uz vec vv = v.cross(_uz); vv.renormalize(); @@ -2005,11 +2006,11 @@ namespace morph { angles[6] = morph::mathconst::two_pi - angles[0]; angles[7] = angles[6]; // The normals for the vertices around the line - std::array, 8> norms = { vv, uz, uz, -vv, -vv, -uz, -uz, vv }; + std::array, 8> norms = { vv, _uz, _uz, -vv, -vv, -_uz, -_uz, vv }; // Start cap vertices (a triangle fan) for (int j = 0; j < segments; j++) { - vec c = uz * sin(angles[j]) * r + vv * cos(angles[j]) * r; + vec c = _uz * sin(angles[j]) * r + vv * cos(angles[j]) * r; this->vertex_push (vstart+c, this->vertexPositions); this->vertex_push (-v, this->vertexNormals); this->vertex_push (colStart, this->vertexColors); @@ -2017,7 +2018,7 @@ namespace morph { // Intermediate, near start cap. Normals point outwards. Need Additional vertices for (int j = 0; j < segments; j++) { - vec c = uz * sin(angles[j]) * r + vv * cos(angles[j]) * r; + vec c = _uz * sin(angles[j]) * r + vv * cos(angles[j]) * r; this->vertex_push (vstart+c, this->vertexPositions); this->vertex_push (norms[j], this->vertexNormals); this->vertex_push (colStart, this->vertexColors); @@ -2025,7 +2026,7 @@ namespace morph { // Intermediate, near end cap. Normals point in direction c for (int j = 0; j < segments; j++) { - vec c = uz * sin(angles[j]) * r + vv * cos(angles[j]) * r; + vec c = _uz * sin(angles[j]) * r + vv * cos(angles[j]) * r; this->vertex_push (vend+c, this->vertexPositions); this->vertex_push (norms[j], this->vertexNormals); this->vertex_push (colEnd, this->vertexColors); @@ -2033,7 +2034,7 @@ namespace morph { // Bottom cap vertices for (int j = 0; j < segments; j++) { - vec c = uz * sin(angles[j]) * r + vv * cos(angles[j]) * r; + vec c = _uz * sin(angles[j]) * r + vv * cos(angles[j]) * r; this->vertex_push (vend+c, this->vertexPositions); this->vertex_push (v, this->vertexNormals); this->vertex_push (colEnd, this->vertexColors); @@ -2106,7 +2107,7 @@ namespace morph { // Like computeLine, but this line has no thickness. void computeFlatLine (vec start, vec end, - vec uz, + vec _uz, std::array col, float w = 0.1f, float shorten = 0.0f) { @@ -2122,8 +2123,8 @@ namespace morph { vend = end - v * shorten; } - // vv is normal to v and uz - vec vv = v.cross(uz); + // vv is normal to v and _uz + vec vv = v.cross(_uz); vv.renormalize(); // corners of the line, and the start angle is determined from vv and w @@ -2134,19 +2135,19 @@ namespace morph { vec c4 = vend + ww; this->vertex_push (c1, this->vertexPositions); - this->vertex_push (uz, this->vertexNormals); + this->vertex_push (_uz, this->vertexNormals); this->vertex_push (col, this->vertexColors); this->vertex_push (c2, this->vertexPositions); - this->vertex_push (uz, this->vertexNormals); + this->vertex_push (_uz, this->vertexNormals); this->vertex_push (col, this->vertexColors); this->vertex_push (c3, this->vertexPositions); - this->vertex_push (uz, this->vertexNormals); + this->vertex_push (_uz, this->vertexNormals); this->vertex_push (col, this->vertexColors); this->vertex_push (c4, this->vertexPositions); - this->vertex_push (uz, this->vertexNormals); + this->vertex_push (_uz, this->vertexNormals); this->vertex_push (col, this->vertexColors); // Number of vertices = segments * 4 + 2. @@ -2170,7 +2171,7 @@ namespace morph { // draw a whole circle around start/end to achieve this, rather than figuring // out a semi-circle). void computeFlatLineRnd (vec start, vec end, - vec uz, + vec _uz, std::array col, float w = 0.1f, float shorten = 0.0f, bool startcaps = true, bool endcaps = true) { @@ -2186,8 +2187,8 @@ namespace morph { vend = end - v * shorten; } - // vv is normal to v and uz - vec vv = v.cross(uz); + // vv is normal to v and _uz + vec vv = v.cross(_uz); vv.renormalize(); // corners of the line, and the start angle is determined from vv and w @@ -2203,7 +2204,7 @@ namespace morph { if (startcaps) { // Push the central point of the start cap - this is at location vstart this->vertex_push (vstart, this->vertexPositions); - this->vertex_push (uz, this->vertexNormals); + this->vertex_push (_uz, this->vertexNormals); this->vertex_push (col, this->vertexColors); ++startvertices; // Start cap vertices (a triangle fan) @@ -2211,33 +2212,33 @@ namespace morph { float t = j * morph::mathconst::two_pi / static_cast(segments); morph::vec c = { sin(t) * r, cos(t) * r, 0.0f }; this->vertex_push (vstart+c, this->vertexPositions); - this->vertex_push (uz, this->vertexNormals); + this->vertex_push (_uz, this->vertexNormals); this->vertex_push (col, this->vertexColors); ++startvertices; } } this->vertex_push (c1, this->vertexPositions); - this->vertex_push (uz, this->vertexNormals); + this->vertex_push (_uz, this->vertexNormals); this->vertex_push (col, this->vertexColors); this->vertex_push (c2, this->vertexPositions); - this->vertex_push (uz, this->vertexNormals); + this->vertex_push (_uz, this->vertexNormals); this->vertex_push (col, this->vertexColors); this->vertex_push (c3, this->vertexPositions); - this->vertex_push (uz, this->vertexNormals); + this->vertex_push (_uz, this->vertexNormals); this->vertex_push (col, this->vertexColors); this->vertex_push (c4, this->vertexPositions); - this->vertex_push (uz, this->vertexNormals); + this->vertex_push (_uz, this->vertexNormals); this->vertex_push (col, this->vertexColors); unsigned int endvertices = 0u; if (endcaps) { // Push the central point of the end cap - this is at location vend this->vertex_push (vend, this->vertexPositions); - this->vertex_push (uz, this->vertexNormals); + this->vertex_push (_uz, this->vertexNormals); this->vertex_push (col, this->vertexColors); ++endvertices; // End cap vertices (a triangle fan) @@ -2245,7 +2246,7 @@ namespace morph { float t = j * morph::mathconst::two_pi / static_cast(segments); morph::vec c = { sin(t) * r, cos(t) * r, 0.0f }; this->vertex_push (vend+c, this->vertexPositions); - this->vertex_push (uz, this->vertexNormals); + this->vertex_push (_uz, this->vertexNormals); this->vertex_push (col, this->vertexColors); ++endvertices; } @@ -2288,60 +2289,197 @@ namespace morph { } } // end computeFlatLine - //! Like computeFlatLine, but this line has no thickness and you can provide the - //! previous and next data points so that this line, the previous line and the - //! next line can line up perfectly without drawing a circular rounded 'end cap'! + /*! + * Like computeFlatLine, but this line has no thickness and you can provide the + * previous and next data points so that this line, the previous line and the + * next line can line up perfectly without drawing a circular rounded 'end cap'! + * + * This code assumes that the coordinates prev, start, end, next all lie on a 2D + * plane normal to _uz. In fact, the 3D coordinates start, end, prev and next + * will all be projected onto the plane defined by _uz, so that they can be + * reduced to 2D coordinates. This then allows crossing points of lines to be + * computed. + * + * If you want to make a ribbon between points that do *not* lie on a 2D plane, + * you'll need to write another graphics primitive function. + */ void computeFlatLine (vec start, vec end, vec prev, vec next, - vec uz, + vec _uz, std::array col, float w = 0.1f) { - // The vector from start to end defines direction of the tube - vec vstart = start; - vec vend = end; - - // line segment vectors - vec v = vend - vstart; - v.renormalize(); - vec vp = vstart - prev; - vp.renormalize(); - vec vn = next - vend; - vn.renormalize(); - - // vv is normal to v and uz - vec vv = v.cross(uz); - vv.renormalize(); - vec vvp = vp.cross(uz); - vvp.renormalize(); - vec vvn = vn.cross(uz); - vvn.renormalize(); - - // corners of the line, and the start angle is determined from vv and w - vec ww = ( (vv+vvp) * 0.5f * w * 0.5f ); + // Corner coordinates for this line section + vec c1 = { 0.0f }; + vec c2 = { 0.0f }; + vec c3 = { 0.0f }; + vec c4 = { 0.0f }; + + // std::cout << "prev/start/end/next: " << prev << "/" << start << "/" << end << "/" << next << "\n"; + // std::cout << "w = " << w << std::endl; + // This is the new code + vec __uz = _uz; + __uz.renormalize(); // Ensure _uz was normalized + + // Transform so that start is the origin + vec s_o = { 0.0f }; // by defn + vec e_o = end - start; + vec p_o = prev - start; + vec n_o = next - start; + + // Use the vector from start to end as the in-plane x dirn + vec plane_x = e_o; + plane_x.renormalize(); + vec plane_y = __uz.cross (plane_x); + plane_y.renormalize(); + + // std::cout << "in-plane axes: plane_x: " << plane_x << " and y " << plane_y << std::endl; + + // Find the in-plane coordinates in the rotated coordinate system + vec e_p = { plane_x.dot (e_o), plane_y.dot (e_o), __uz.dot (e_o) }; + std::cout << "end coord: " << end << ", offset e_o: " << e_o << " in-plane e_p: " << e_p << std::endl; + + // One epsilon is exacting + if (std::abs(e_p[2]) > std::numeric_limits::epsilon()) { + throw std::runtime_error ("uz not orthogonal to the line start -> end?"); + } + + // From e_p and e_o could now figure out what angle of rotation this was + // with e_p.angle (e_o); BUT NB: This returns an angle magnitude; it does + // not give rotn dirn So use 2D version of angle function: + // float basis_rotn_angle = e_p.less_one_dim().angle() - e_o.less_one_dim().angle(); + // BUT.... that's no good unless __uz == this->uz + // What's angle/rotn transform between __uz and uz?? Need this to recapitulate + // simples: + morph::vec basis_rotn_axis = __uz.cross (this->uz); + if (basis_rotn_axis.length() == 0.0f) { basis_rotn_axis = this->uz; } + + float basis_rotn_angle = plane_x.angle (this->ux, basis_rotn_axis); + // Now use basis_rotn_axis to determine the sign of the angle? + std::cout << "rotation axis: " << basis_rotn_axis << std::endl; + std::cout << "rotation angle: " << basis_rotn_angle << std::endl; + // std::cout << "Basis coordinates rotated " << basis_rotn_angle << " radians/" + // << basis_rotn_angle * morph::mathconst::rad2deg << " deg\n"; + + // A rotation quaternion to rotate our coordinates back at the end + morph::quaternion basis_rotn (basis_rotn_axis, basis_rotn_angle); + morph::quaternion basis_rotn_inv (basis_rotn_axis, -basis_rotn_angle); + +#if 1 + vec p_p = basis_rotn * p_o; + vec n_p = basis_rotn * n_o; + vec s_p = basis_rotn * s_o; // not necessary, s_p = (0,0,0) by defn +#else + // *should* be equivalent + vec p_p = { plane_x.dot (p_o), plane_y.dot (p_o), __uz.dot (s_o) }; + vec n_p = { plane_x.dot (n_o), plane_y.dot (n_o), __uz.dot (s_o) }; + vec s_p = { plane_x.dot (s_o), plane_y.dot (s_o), __uz.dot (s_o) }; +#endif + // Line crossings time. + vec c1_p = { 0.0f }; // 2D crossing coords that we're going to find + vec c2_p = { 0.0f }; + vec c3_p = e_p.less_one_dim(); + vec c4_p = e_p.less_one_dim(); + + // 3 lines on each side. l_p, l_c (current) and l_n. Each has two ends. l_p_1, l_p_2 etc. + + std::cout << "s_p (sanity check): " << s_p << " should be (0,0,0)\n"; + // 'prev' 'cur' and 'next' vectors + vec p_vec = (s_p - p_p).less_one_dim(); + vec c_vec = e_p.less_one_dim(); + vec n_vec = (n_p - e_p).less_one_dim(); + + vec p_ortho = (s_p - p_p).cross (this->uz).less_one_dim(); + p_ortho.renormalize(); + vec c_ortho = (e_p - s_p).cross (this->uz).less_one_dim(); + c_ortho.renormalize(); + vec n_ortho = (n_p - e_p).cross (this->uz).less_one_dim(); + n_ortho.renormalize(); + + const float hw = w / 2.0f; + + std::cout << "prev in-plane: " << p_p << " and start in-plane: " << s_p << std::endl; + vec l_p_1 = p_p.less_one_dim() + (p_ortho * hw) - p_vec; // makes it 3 times as long as the line. + vec l_p_2 = s_p.less_one_dim() + (p_ortho * hw) + p_vec; + vec l_c_1 = s_p.less_one_dim() + (c_ortho * hw) - c_vec; + vec l_c_2 = e_p.less_one_dim() + (c_ortho * hw) + c_vec; + vec l_n_1 = e_p.less_one_dim() + (n_ortho * hw) - n_vec; + vec l_n_2 = n_p.less_one_dim() + (n_ortho * hw) + n_vec; +#if 1 + std::cout << "l_p_1 -> l_p_2 => " << l_p_1 << " -> " << l_p_2 << std::endl; + std::cout << "l_c_1 -> l_c_2 => " << l_c_1 << " -> " << l_c_2 << std::endl; + std::cout << "l_n_1 -> l_n_2 => " << l_n_1 << " -> " << l_n_2 << std::endl; +#endif + std::bitset<2> isect = morph::MathAlgo::segments_intersect (l_p_1, l_p_2, l_c_1, l_c_2); + if (isect.test(0) == true && isect.test(1) == false) { // test for intersection but not colinear + c1_p = morph::MathAlgo::crossing_point (l_p_1, l_p_2, l_c_1, l_c_2); + } else if (isect.test(0) == true && isect.test(1) == true) { + c1_p = s_p.less_one_dim() + (c_ortho * hw); + } else { // no intersection. prev could have been start + c1_p = s_p.less_one_dim() + (c_ortho * hw); + } + isect = morph::MathAlgo::segments_intersect (l_c_1, l_c_2, l_n_1, l_n_2); + if (isect.test(0) == true && isect.test(1) == false) { + c4_p = morph::MathAlgo::crossing_point (l_c_1, l_c_2, l_n_1, l_n_2); + } else if (isect.test(0) == true && isect.test(1) == true) { + c4_p = e_p.less_one_dim() + (c_ortho * hw); + } else { // no intersection, prev could have been end + c4_p = e_p.less_one_dim() + (c_ortho * hw); + } + std::cout << "c1_p: " << c1_p << " and c_4p: " << c4_p << std::endl; + // o for 'other side'. Could re-use vars in future version. Or just subtract (*_ortho * w) from each. + vec o_l_p_1 = p_p.less_one_dim() - (p_ortho * hw) - p_vec; // makes it 3 times as long as the line. + vec o_l_p_2 = s_p.less_one_dim() - (p_ortho * hw) + p_vec; + vec o_l_c_1 = s_p.less_one_dim() - (c_ortho * hw) - c_vec; + vec o_l_c_2 = e_p.less_one_dim() - (c_ortho * hw) + c_vec; + vec o_l_n_1 = e_p.less_one_dim() - (n_ortho * hw) - n_vec; + vec o_l_n_2 = n_p.less_one_dim() - (n_ortho * hw) + n_vec; +#if 1 + std::cout << "o_l_p_1 -> o_l_p_2 => " << o_l_p_1 << " -> " << o_l_p_2 << std::endl; + std::cout << "o_l_c_1 -> o_l_c_2 => " << o_l_c_1 << " -> " << o_l_c_2 << std::endl; + std::cout << "o_l_n_1 -> o_l_n_2 => " << o_l_n_1 << " -> " << o_l_n_2 << std::endl; +#endif + isect = morph::MathAlgo::segments_intersect (o_l_p_1, o_l_p_2, o_l_c_1, o_l_c_2); + if (isect.test(0) == true && isect.test(1) == false) { // test for intersection but not colinear + c2_p = morph::MathAlgo::crossing_point (o_l_p_1, o_l_p_2, o_l_c_1, o_l_c_2); + } else if (isect.test(0) == true && isect.test(1) == true) { + c2_p = s_p.less_one_dim() - (c_ortho * hw); + } else { // no intersection. prev could have been start + c2_p = s_p.less_one_dim() - (c_ortho * hw); + } - vec c1 = vstart + ww; - vec c2 = vstart - ww; + isect = morph::MathAlgo::segments_intersect (o_l_c_1, o_l_c_2, o_l_n_1, o_l_n_2); + if (isect.test(0) == true && isect.test(1) == false) { + c3_p = morph::MathAlgo::crossing_point (o_l_c_1, o_l_c_2, o_l_n_1, o_l_n_2); + } else if (isect.test(0) == true && isect.test(1) == true) { + c3_p = e_p.less_one_dim() - (c_ortho * hw); + } else { // no intersection. next could have been end + c3_p = e_p.less_one_dim() - (c_ortho * hw); + } - ww = ( (vv+vvn) * 0.5f * w * 0.5f ); + // Transform and rotate back into c1-c4 + c1 = (basis_rotn_inv * c1_p.plus_one_dim()) + start; + c2 = (basis_rotn_inv * c2_p.plus_one_dim()) + start; + c3 = (basis_rotn_inv * c3_p.plus_one_dim()) + start; + c4 = (basis_rotn_inv * c4_p.plus_one_dim()) + start; - vec c3 = vend - ww; - vec c4 = vend + ww; + //std::cout << "c1 to c4: " << c1 << ", " << c2 << ", " << c3 << ", " << c4 << "\n"; + // Now create the vertices from these four corners, c1-c4 this->vertex_push (c1, this->vertexPositions); - this->vertex_push (uz, this->vertexNormals); + this->vertex_push (_uz, this->vertexNormals); this->vertex_push (col, this->vertexColors); this->vertex_push (c2, this->vertexPositions); - this->vertex_push (uz, this->vertexNormals); + this->vertex_push (_uz, this->vertexNormals); this->vertex_push (col, this->vertexColors); this->vertex_push (c3, this->vertexPositions); - this->vertex_push (uz, this->vertexNormals); + this->vertex_push (_uz, this->vertexNormals); this->vertex_push (col, this->vertexColors); this->vertex_push (c4, this->vertexPositions); - this->vertex_push (uz, this->vertexNormals); + this->vertex_push (_uz, this->vertexNormals); this->vertex_push (col, this->vertexColors); this->indices.push_back (this->idx); @@ -2359,132 +2497,28 @@ namespace morph { //! Make a joined up line with previous. void computeFlatLineP (vec start, vec end, vec prev, - vec uz, + vec _uz, std::array col, float w = 0.1f) { - // The vector from start to end defines direction of the tube - vec vstart = start; - vec vend = end; - - // line segment vectors - vec v = vend - vstart; - v.renormalize(); - vec vp = vstart - prev; - vp.renormalize(); - - // vv is normal to v and uz - vec vv = v.cross(uz); - vv.renormalize(); - vec vvp = vp.cross(uz); - vvp.renormalize(); - - // corners of the line, and the start angle is determined from vv and w - vec ww = ( (vv+vvp)*0.5f * w*0.5f ); - - vec c1 = vstart + ww; - vec c2 = vstart - ww; - - ww = vv * w * 0.5f; - - vec c3 = vend - ww; - vec c4 = vend + ww; - - this->vertex_push (c1, this->vertexPositions); - this->vertex_push (uz, this->vertexNormals); - this->vertex_push (col, this->vertexColors); - - this->vertex_push (c2, this->vertexPositions); - this->vertex_push (uz, this->vertexNormals); - this->vertex_push (col, this->vertexColors); - - this->vertex_push (c3, this->vertexPositions); - this->vertex_push (uz, this->vertexNormals); - this->vertex_push (col, this->vertexColors); - - this->vertex_push (c4, this->vertexPositions); - this->vertex_push (uz, this->vertexNormals); - this->vertex_push (col, this->vertexColors); - - this->indices.push_back (this->idx); - this->indices.push_back (this->idx+1); - this->indices.push_back (this->idx+2); - - this->indices.push_back (this->idx); - this->indices.push_back (this->idx+2); - this->indices.push_back (this->idx+3); - - // Update idx - this->idx += 4; + this->computeFlatLine (start, end, prev, end, _uz, col, w); } // end computeFlatLine that joins perfectly with prev //! Flat line, joining up with next void computeFlatLineN (vec start, vec end, vec next, - vec uz, + vec _uz, std::array col, float w = 0.1f) { - // The vector from start to end defines direction of the tube - vec vstart = start; - vec vend = end; - - // line segment vectors - vec v = vend - vstart; - v.renormalize(); - vec vn = next - vend; - vn.renormalize(); - - // vv is normal to v and uz - vec vv = v.cross(uz); - vv.renormalize(); - vec vvn = vn.cross(uz); - vvn.renormalize(); - - // corners of the line, and the start angle is determined from vv and w - vec ww = vv * w * 0.5f; - - vec c1 = vstart + ww; - vec c2 = vstart - ww; - - ww = ( (vv+vvn) * 0.5f * w * 0.5f ); - - vec c3 = vend - ww; - vec c4 = vend + ww; - - this->vertex_push (c1, this->vertexPositions); - this->vertex_push (uz, this->vertexNormals); - this->vertex_push (col, this->vertexColors); - - this->vertex_push (c2, this->vertexPositions); - this->vertex_push (uz, this->vertexNormals); - this->vertex_push (col, this->vertexColors); - - this->vertex_push (c3, this->vertexPositions); - this->vertex_push (uz, this->vertexNormals); - this->vertex_push (col, this->vertexColors); - - this->vertex_push (c4, this->vertexPositions); - this->vertex_push (uz, this->vertexNormals); - this->vertex_push (col, this->vertexColors); - - this->indices.push_back (this->idx); - this->indices.push_back (this->idx+1); - this->indices.push_back (this->idx+2); - - this->indices.push_back (this->idx); - this->indices.push_back (this->idx+2); - this->indices.push_back (this->idx+3); - - // Update idx - this->idx += 4; - } // end computeFlatLine that joins perfectly with next line + this->computeFlatLine (start, end, start, next, _uz, col, w); + } // Like computeLine, but this line has no thickness and it's dashed. // dashlen: the length of dashes // gap prop: The proportion of dash length used for the gap void computeFlatDashedLine (vec start, vec end, - vec uz, + vec _uz, std::array col, float w = 0.1f, float shorten = 0.0f, float dashlen = 0.1f, float gapprop = 0.3f) @@ -2506,8 +2540,8 @@ namespace morph { linelen = v.length() - shorten * 2.0f; } - // vv is normal to v and uz - vec vv = v.cross(uz); + // vv is normal to v and _uz + vec vv = v.cross(_uz); vv.renormalize(); // Loop, creating the dashes @@ -2525,19 +2559,19 @@ namespace morph { vec c4 = dash_e + ww; this->vertex_push (c1, this->vertexPositions); - this->vertex_push (uz, this->vertexNormals); + this->vertex_push (_uz, this->vertexNormals); this->vertex_push (col, this->vertexColors); this->vertex_push (c2, this->vertexPositions); - this->vertex_push (uz, this->vertexNormals); + this->vertex_push (_uz, this->vertexNormals); this->vertex_push (col, this->vertexColors); this->vertex_push (c3, this->vertexPositions); - this->vertex_push (uz, this->vertexNormals); + this->vertex_push (_uz, this->vertexNormals); this->vertex_push (col, this->vertexColors); this->vertex_push (c4, this->vertexPositions); - this->vertex_push (uz, this->vertexNormals); + this->vertex_push (_uz, this->vertexNormals); this->vertex_push (col, this->vertexColors); // Number of vertices = segments * 4 + 2. diff --git a/morph/vec.h b/morph/vec.h index e0319be4..48ba2d99 100644 --- a/morph/vec.h +++ b/morph/vec.h @@ -1109,7 +1109,8 @@ namespace morph { } /*! - * Return this angle between this vector and the other. Works for any N. + * Return the magnitude of the angle between this vector and the other. Works + * for any N. */ constexpr S angle (const vec& other) const { @@ -1117,6 +1118,20 @@ namespace morph { return std::acos (cos_theta); } + /*! + * Return this angle between this vector and the other. Works for any N. + * + * axis is the axis of rotation, so this angle IS signed, positive if 'other' is + * at a positive right-handed angle wrt *this. axis does not need to be + * *exactly* the axis of rotation, though it could be. The exact direction of + * the axis of rotation can be obtained from this->cross (other), but this + */ + constexpr S angle (const vec& other, const vec& axis) const + { + S angle_magn = this->angle (other); + return (this->cross (other).dot (axis) > S{0}) ? angle_magn : -angle_magn; + } + /*! * Two dimensional angle in radians (only for N=2) wrt to the axes */ diff --git a/tests/testvec.cpp b/tests/testvec.cpp index f40167cb..7ba4a4d8 100644 --- a/tests/testvec.cpp +++ b/tests/testvec.cpp @@ -259,5 +259,14 @@ int main() { << (avec1.angle(avec2) * morph::mathconst::rad2deg) << " or " << (avec2.angle(avec1) * morph::mathconst::rad2deg) << std::endl; + morph::vec testvec = {1, 0, 0}; + morph::vec othervec = {0.707, 0.707, 0}; + float tv_ov = testvec.angle(othervec, morph::vec{0, 0, 1}); + std::cout << othervec << " is at angle " << tv_ov << " wrt " << testvec << " around the uz axis" << std::endl; + if (tv_ov < 0.0f) { --rtn; } + float ov_tv = othervec.angle(testvec, morph::vec{0, 0, 1}); + std::cout << testvec << " is at angle " << ov_tv << " wrt " << othervec << " around the uz axis" << std::endl; + if (ov_tv > 0.0f) { --rtn; } + return rtn; }