diff --git a/BGL/include/CGAL/draw_face_graph.h b/BGL/include/CGAL/draw_face_graph.h index 11c1da1d6188..72d462a4e031 100644 --- a/BGL/include/CGAL/draw_face_graph.h +++ b/BGL/include/CGAL/draw_face_graph.h @@ -60,6 +60,9 @@ void compute_elements(const FG &fg, if (gs_options.are_faces_enabled()) { + // Colour by value: name the value once, for the legend. + if (!gs_options.face_value_name.empty()) + { graphics_scene.set_value_name(gs_options.face_value_name); } for (auto fh : faces(fg)) { if (fh != boost::graph_traits::null_face() && // face exists @@ -78,6 +81,9 @@ void compute_elements(const FG &fg, hd = next(hd, fg); } while (hd != first_hd); + // Colour by value: attach the face's scalar value before committing it. + if (gs_options.is_face_valued(fg, fh)) + { graphics_scene.set_face_value(gs_options.face_value(fg, fh)); } graphics_scene.face_end(); } } diff --git a/Basic_viewer/doc/Basic_viewer/Concepts/GraphicsSceneOptions.h b/Basic_viewer/doc/Basic_viewer/Concepts/GraphicsSceneOptions.h index f845dc792813..5beef8b82238 100644 --- a/Basic_viewer/doc/Basic_viewer/Concepts/GraphicsSceneOptions.h +++ b/Basic_viewer/doc/Basic_viewer/Concepts/GraphicsSceneOptions.h @@ -75,6 +75,18 @@ class GraphicsSceneOptions /// `nullptr` by default. std::function face_color; + /// `std::function` that returns `true` if the given face carries a scalar value to + /// colour it by, `false` otherwise. `false` by default. + std::function is_face_valued; + + /// `std::function` that returns the scalar value of the given face. Used only when + /// `is_face_valued()` returns `true`. The viewer normalises the values over their range + /// and maps them to a color palette. + std::function face_value; + + /// name of the value, shown in the viewer's color legend (for example "aspect ratio"). + std::string face_value_name; + /// ignores all vertices when `b` is `true`; otherwise ignores only vertices for which `ignore_vertex()` returns `true`. void ignore_all_vertices(bool b); diff --git a/Basic_viewer/include/CGAL/Basic_shaders.h b/Basic_viewer/include/CGAL/Basic_shaders.h index 97c8aa359223..8199971c881c 100644 --- a/Basic_viewer/include/CGAL/Basic_shaders.h +++ b/Basic_viewer/include/CGAL/Basic_shaders.h @@ -75,6 +75,38 @@ uniform highp vec4 u_PointPlane; uniform mediump float u_RenderingMode; uniform mediump float u_RenderingTransparency; +// Colour by value: 0 keeps the vertex colour, otherwise a palette index. The +// value shown is the signed distance from the fragment to the clipping plane, +// normalised to [u_ValueMin, u_ValueMax]. +uniform mediump float u_ColorMapMode; +uniform mediump float u_ValueMin; +uniform mediump float u_ValueMax; +// Per cell: the viewer gives one value for the whole cell (its centre distance or +// its size), so a whole cell takes one flat colour and neighbouring cells do not melt. +uniform int u_ColorPerCell; +uniform highp float u_CellValue; + +vec3 colour_palette(float t, float mode) +{ + if (mode < 1.5) + { return clamp(vec3(t*3.0, t*3.0-1.0, t*3.0-2.0), 0.0, 1.0); } // heat + if (mode < 2.5) + { return clamp(vec3(1.5-abs(4.0*t-3.0), 1.5-abs(4.0*t-2.0), 1.5-abs(4.0*t-1.0)), + 0.0, 1.0); } // jet + if (mode < 3.5) + { return vec3(t); } // grey ramp + // viridis, a perceptually uniform map (polynomial fit by Matt Zucker). The same + // coefficients are mirrored in the viewer's legend so the bar matches the faces. + const vec3 c0=vec3(0.2777273272234177, 0.005407344544966578, 0.3340998053353061); + const vec3 c1=vec3(0.1050930431085774, 1.404613529898575, 1.384590162594685); + const vec3 c2=vec3(-0.3308618287255563, 0.214847559468213, 0.09509516302823659); + const vec3 c3=vec3(-4.634230498983486, -5.799100973351585, -19.33244095627987); + const vec3 c4=vec3(6.228269936347081, 14.17993336680509, 56.69055260068105); + const vec3 c5=vec3(4.776384997670288, -13.74514537774601, -65.35303263337234); + const vec3 c6=vec3(-5.435455855934631, 4.645852612178535, 26.3124352495832); + return clamp(c0+t*(c1+t*(c2+t*(c3+t*(c4+t*(c5+t*c6))))), 0.0, 1.0); +} + void main(void) { highp vec3 L = u_LightPos.xyz - vs_fP.xyz; @@ -84,9 +116,19 @@ void main(void) L = normalize(L); V = normalize(V); + // Base colour is the vertex colour, or a palette applied to the value. + vec3 base = fColor.rgb; + if (u_ColorMapMode > 0.5) + { + float value = (u_ColorPerCell != 0) ? u_CellValue + : dot(ls_fP.xyz-u_PointPlane.xyz, normalize(u_ClipPlane.xyz)); + float t = clamp((value-u_ValueMin)/max(u_ValueMax-u_ValueMin, 1e-6), 0.0, 1.0); + base = colour_palette(t, u_ColorMapMode); + } + highp vec3 R = reflect(-L, a_Normal); - highp vec4 diffuse = vec4(max(dot(a_Normal,L), 0.0) * u_LightDiff.rgb * fColor.rgb, 1.0); - highp vec4 ambient = vec4(u_LightAmb.rgb * fColor.rgb, 1.0); + highp vec4 diffuse = vec4(max(dot(a_Normal,L), 0.0) * u_LightDiff.rgb * base, 1.0); + highp vec4 ambient = vec4(u_LightAmb.rgb * base, 1.0); highp vec4 specular = pow(max(dot(R,V), 0.0), u_SpecPower) * u_LightSpec; // onPlane == 1: inside clipping plane, should be solid; diff --git a/Basic_viewer/include/CGAL/Graphics_scene.h b/Basic_viewer/include/CGAL/Graphics_scene.h index cd2331d17b11..a7a58dfd2661 100644 --- a/Basic_viewer/include/CGAL/Graphics_scene.h +++ b/Basic_viewer/include/CGAL/Graphics_scene.h @@ -252,6 +252,13 @@ class Graphics_scene return m_buffer_for_faces.is_a_face_started(); } + /// sets the scalar value of the face currently being built. The viewer can colour + /// the faces by these values, normalised over their range and mapped to a palette. + void set_face_value(float v) { m_current_face_value=v; m_has_face_values=true; } + + /// sets the name of the value, shown in the viewer's colour legend. + void set_value_name(const std::string &n) { m_value_name=n; } + void face_begin() { if (a_face_started()) @@ -314,6 +321,7 @@ class Graphics_scene const unsigned int idx=static_cast(m_faces.size()); m_faces.emplace_back(m_current_face_start, number_of_elements(POS_FACES)-m_current_face_start); + record_current_face_value(); m_face_dedup.emplace(std::move(key), idx); m_volume_faces.back().push_back(idx); return; @@ -324,6 +332,22 @@ class Graphics_scene // Record this face's vertex range in POS_FACES, for the clip-plane cap. m_faces.emplace_back(m_current_face_start, number_of_elements(POS_FACES) - m_current_face_start); + record_current_face_value(); + } + + // Colour by value: store the value of the face just committed, parallel to + // m_faces, and keep the min and max for the palette range. + void record_current_face_value() + { + if (m_has_face_values) + { + if (m_face_values.empty()) + { m_face_value_min=m_face_value_max=m_current_face_value; } + else + { if (m_current_face_valuem_face_value_max) { m_face_value_max=m_current_face_value; } } + } + m_face_values.push_back(m_current_face_value); } // Clip-plane cap: a volume groups the faces added until volume_end, de-duplicated @@ -366,6 +390,14 @@ class Graphics_scene const std::vector &get_volume_bboxes() const { return m_volume_bboxes; } + // Colour by value: the per-face values set by the drawer, whether any were set, + // the legend name, and the value range. + const std::vector &get_face_values() const { return m_face_values; } + bool has_face_values() const { return m_has_face_values; } + const std::string &value_name() const { return m_value_name; } + float face_value_min() const { return m_face_value_min; } + float face_value_max() const { return m_face_value_max; } + template void add_text(const KPoint &kp, const std::string &txt) { @@ -509,6 +541,16 @@ class Graphics_scene std::vector m_volume_bboxes; unsigned int m_current_face_start = 0; + // Colour by value: an optional scalar per face, set by the drawer, that the viewer + // maps to a palette (like the colour, but any float). Each value is parallel to + // m_faces; m_value_name labels the legend. + std::vector m_face_values; + float m_current_face_value = 0.f; + bool m_has_face_values = false; + std::string m_value_name; + float m_face_value_min = 0.f; + float m_face_value_max = 1.f; + // Clip-plane cap: geometric face de-duplication during volume building. The key // is the sorted face vertex positions, so both sides of a shared wall match. // Hashed so the build stays linear on meshes with many faces. diff --git a/Basic_viewer/include/CGAL/Graphics_scene_options.h b/Basic_viewer/include/CGAL/Graphics_scene_options.h index b1cefd56df24..033c02271267 100644 --- a/Basic_viewer/include/CGAL/Graphics_scene_options.h +++ b/Basic_viewer/include/CGAL/Graphics_scene_options.h @@ -52,6 +52,8 @@ struct Graphics_scene_optionsbool { return false; }; face_wireframe=[](const DS &, face_descriptor)->bool { return false; }; + + is_face_valued=[](const DS &, face_descriptor)->bool { return false; }; } // The seven following functions should not be null @@ -65,6 +67,19 @@ struct Graphics_scene_options face_wireframe; + /// `std::function` that returns `true` if the given face carries a scalar value to + /// colour it by, `false` otherwise. `false` by default. + std::function is_face_valued; + + /// `std::function` that returns the scalar value of the given face. Called only + /// when `is_face_valued()` returns `true`. The viewer normalises the values over their + /// range and maps them to a colour palette. + std::function face_value; + + /// The name of the value, shown in the viewer's colour legend (for example + /// "aspect ratio"). Empty by default. + std::string face_value_name; + // These functions must be non null if the corresponding colored_XXX function // returns true. std::function vertex_color; diff --git a/Basic_viewer/include/CGAL/Qt/Basic_viewer.h b/Basic_viewer/include/CGAL/Qt/Basic_viewer.h index ed584d50cb76..0eed4f1a8dab 100644 --- a/Basic_viewer/include/CGAL/Qt/Basic_viewer.h +++ b/Basic_viewer/include/CGAL/Qt/Basic_viewer.h @@ -34,6 +34,7 @@ #include #include +#include #include #include @@ -124,6 +125,8 @@ class Basic_viewer : public CGAL::QGLViewer setKeyDescription(::Qt::Key_U, "Move camera direction upside down"); setKeyDescription(::Qt::Key_V, "Toggles vertices display"); setKeyDescription(::Qt::Key_W, "Toggles faces display"); + setKeyDescription(::Qt::Key_D, "Cycle colouring faces by value (distance to the plane)"); + setKeyDescription(::Qt::ShiftModifier, ::Qt::Key_D, "Colour by value: distance smooth, distance per cell, size per cell"); setKeyDescription(::Qt::Key_Plus, "Increase size of edges"); setKeyDescription(::Qt::Key_Minus, "Decrease size of edges"); setKeyDescription(::Qt::ControlModifier, ::Qt::Key_Plus, "Increase size of vertices"); @@ -733,9 +736,75 @@ class Basic_viewer : public CGAL::QGLViewer rendering_program_face.setUniformValue("u_RenderingTransparency", clipping_plane_rendering_transparency); rendering_program_face.setUniformValue("u_ClipPlane", clipPlane); rendering_program_face.setUniformValue("u_PointPlane", plane_point); + // Colour by value: the value is the distance to the clipping plane, over a scale + // anchored at the plane (0) and growing into the kept half, so moving the plane + // sweeps the colours instead of leaving them unchanged. + rendering_program_face.setUniformValue("u_ColorMapMode", static_cast(m_color_map)); + { double dvmin, dvmax; distance_value_range(clipPlane, plane_point, dvmin, dvmax); + rendering_program_face.setUniformValue("u_ValueMin", static_cast(dvmin)); + rendering_program_face.setUniformValue("u_ValueMax", static_cast(dvmax)); } vao[VAO_FACES].bind(); - glDrawArrays(GL_TRIANGLES, 0, static_cast(m_scene.number_of_elements(GS::POS_FACES))); + const std::vector> &vols=m_scene.get_volume_faces(); + if (m_color_map!=0 && m_color_value==3 && m_scene.has_face_values()) + { + // User value: one flat value per face, provided by the drawer (for example + // the aspect ratio of the face), mapped to the palette. + rendering_program_face.setUniformValue("u_ColorPerCell", static_cast(1)); + rendering_program_face.setUniformValue("u_ValueMin", static_cast(m_scene.face_value_min())); + rendering_program_face.setUniformValue("u_ValueMax", static_cast(m_scene.face_value_max())); + const std::vector &fvals=m_scene.get_face_values(); + const unsigned int nf=m_scene.number_of_faces(); + for (unsigned int f=0; f(fvals[f])); + const std::pair &r=m_scene.face_range(f); + glDrawArrays(GL_TRIANGLES, static_cast(r.first), + static_cast(r.second)); + } + } + else if (m_color_map!=0 && (m_color_value==1 || m_color_value==2) && !vols.empty()) + { + // Per cell: draw each volume with one flat value, so a whole cell takes + // one colour and neighbouring cells do not melt into one. The value is the + // centre's distance to the plane, or the cell size. + rendering_program_face.setUniformValue("u_ColorPerCell", static_cast(1)); + const std::vector &bb=m_scene.get_volume_bboxes(); + const bool size_mode=(m_color_value==2); + if (size_mode) + { + if (!m_cell_sizes_valid) { compute_cell_sizes(); } + rendering_program_face.setUniformValue("u_ValueMin", static_cast(m_cell_size_min)); + rendering_program_face.setUniformValue("u_ValueMax", static_cast(m_cell_size_max)); + } + const QVector3D n=QVector3D(clipPlane).normalized(); + const QVector3D pt=plane_point.toVector3D(); + for (std::size_t v=0; v(value)); + for (unsigned int fi : vols[v]) + { + const std::pair &r=m_scene.face_range(fi); + glDrawArrays(GL_TRIANGLES, static_cast(r.first), + static_cast(r.second)); + } + } + } + else + { + rendering_program_face.setUniformValue("u_ColorPerCell", static_cast(0)); + glDrawArrays(GL_TRIANGLES, 0, static_cast(m_scene.number_of_elements(GS::POS_FACES))); + } glDisable(GL_POLYGON_OFFSET_FILL); }; @@ -842,6 +911,12 @@ class Basic_viewer : public CGAL::QGLViewer QVector4D capcol; if (num_volumes == 0) { capcol = QVector4D(0.6f, 0.6f, 0.6f, 1.0f); } + else if (m_color_map!=0) + { // Colour by value: cap follows the palette, like the volume's faces. + const QColor cc=volume_value_color(v, clipPlane, plane_point); + capcol = QVector4D(float(cc.redF()), float(cc.greenF()), + float(cc.blueF()), 1.0f); + } else { const CGAL::IO::Color &c = vcolors[v]; @@ -939,18 +1014,70 @@ class Basic_viewer : public CGAL::QGLViewer clipping_plane_rendering_transparency); rendering_program_face.setUniformValue("u_ClipPlane", clipPlane); rendering_program_face.setUniformValue("u_PointPlane", plane_point); + // Colour by value: the kept volumes follow the same colour map as the other + // face modes, per fragment or one flat value per cell. + rendering_program_face.setUniformValue("u_ColorMapMode", static_cast(m_color_map)); + { double dvmin, dvmax; distance_value_range(clipPlane, plane_point, dvmin, dvmax); + rendering_program_face.setUniformValue("u_ValueMin", static_cast(dvmin)); + rendering_program_face.setUniformValue("u_ValueMax", static_cast(dvmax)); } + const bool per_cell=(m_color_map!=0 && m_color_value!=0 && num_volumes!=0); + const bool size_mode=(m_color_value==2); + if (per_cell && size_mode) + { + if (!m_cell_sizes_valid) { compute_cell_sizes(); } + rendering_program_face.setUniformValue("u_ValueMin", static_cast(m_cell_size_min)); + rendering_program_face.setUniformValue("u_ValueMax", static_cast(m_cell_size_max)); + } + rendering_program_face.setUniformValue("u_ColorPerCell", static_cast(per_cell?1:0)); + const QVector3D n=QVector3D(clipPlane).normalized(); + const QVector3D pt=plane_point.toVector3D(); vao[VAO_FACES].bind(); if (num_volumes == 0) { - glDrawArrays(GL_TRIANGLES, 0, static_cast( - m_scene.number_of_elements(GS::POS_FACES))); + // No volumes (a surface mesh, for example): there is nothing to clip whole, + // so colour all faces by value, including the drawer's per-face value. + if (m_color_map!=0 && m_color_value==3 && m_scene.has_face_values()) + { + rendering_program_face.setUniformValue("u_ColorPerCell", static_cast(1)); + rendering_program_face.setUniformValue("u_ValueMin", static_cast(m_scene.face_value_min())); + rendering_program_face.setUniformValue("u_ValueMax", static_cast(m_scene.face_value_max())); + const std::vector &fvals=m_scene.get_face_values(); + const unsigned int nf=m_scene.number_of_faces(); + for (unsigned int f=0; f(fvals[f])); + const std::pair &r=m_scene.face_range(f); + glDrawArrays(GL_TRIANGLES, static_cast(r.first), + static_cast(r.second)); + } + } + else + { + glDrawArrays(GL_TRIANGLES, 0, static_cast( + m_scene.number_of_elements(GS::POS_FACES))); + } } else { + const std::vector &bb = m_scene.get_volume_bboxes(); for (std::size_t v = 0; v < num_volumes; ++v) { if (!m_volumes_kept[v]) { continue; } + if (per_cell) + { + float value; + if (size_mode) { value=m_cell_sizes[v]; } + else + { + const CGAL::Bbox_3 &b=bb[v]; + const QVector3D c(float((b.xmin()+b.xmax())*0.5), + float((b.ymin()+b.ymax())*0.5), + float((b.zmin()+b.zmax())*0.5)); + value=QVector3D::dotProduct(c-pt, n); + } + rendering_program_face.setUniformValue("u_CellValue", static_cast(value)); + } for (unsigned int fi : volumes[v]) { const std::pair &r = m_scene.face_range(fi); @@ -1079,6 +1206,10 @@ class Basic_viewer : public CGAL::QGLViewer glEnable(GL_LIGHTING); } + // Colour by value: show the palette and the value range as a small legend. + if (m_color_map!=0) + { draw_color_legend(clipPlane, plane_point); } + // Multiply matrix to get in the frame coordinate system. // glMultMatrixd(manipulatedFrame()->matrix()); // Linker error // Scale down the drawings @@ -1714,9 +1845,161 @@ class Basic_viewer : public CGAL::QGLViewer } } + // Colour by value (size): one size per cell, the bounding-box volume, with the + // range over all cells so the palette spans from the smallest to the largest. + void compute_cell_sizes() + { + const std::vector &bb=m_scene.get_volume_bboxes(); + m_cell_sizes.resize(bb.size()); + m_cell_size_min=(std::numeric_limits::max)(); + m_cell_size_max=0.f; + for (std::size_t v=0; vm_cell_size_max) { m_cell_size_max=s; } + } + if (bb.empty()) { m_cell_size_min=0.f; m_cell_size_max=1.f; } + m_cell_sizes_valid=true; + } + + // Colour by value: the palette as a QColor, matching colour_palette() in the + // shader, so the legend bar shows the same colours as the faces. + QColor legend_palette_color(float t) const + { + auto cl=[](float x){ return x<0.f ? 0.f : (x>1.f ? 1.f : x); }; + float r, g, b; + if (m_color_map<2) { r=cl(t*3.f); g=cl(t*3.f-1.f); b=cl(t*3.f-2.f); } // heat + else if (m_color_map<3) { r=cl(1.5f-std::abs(4.f*t-3.f)); // jet + g=cl(1.5f-std::abs(4.f*t-2.f)); + b=cl(1.5f-std::abs(4.f*t-1.f)); } + else if (m_color_map<4) { r=g=b=cl(t); } // grey ramp + else + { // viridis, the same coefficients as colour_palette() in Basic_shaders.h + static const float C[7][3]={ + { 0.277727f, 0.005407f, 0.334100f}, + { 0.105093f, 1.404614f, 1.384590f}, + {-0.330862f, 0.214848f, 0.095095f}, + {-4.634230f, -5.799101f, -19.332441f}, + { 6.228270f, 14.179933f, 56.690553f}, + { 4.776385f,-13.745145f, -65.353033f}, + {-5.435456f, 4.645853f, 26.312435f}}; + float rgb[3]; + for (int k=0; k<3; ++k) + { float v=C[6][k]; + for (int j=5; j>=0; --j) { v=C[j][k]+t*v; } + rgb[k]=cl(v); } + r=rgb[0]; g=rgb[1]; b=rgb[2]; + } + return QColor(int(r*255.f), int(g*255.f), int(b*255.f)); + } + + // Colour by value (distance): the actual signed-distance range the geometry spans + // along the plane normal, from the scene bounding-box corners, so the palette and + // the legend cover the values really present rather than the whole scene radius + // (which left the colours bunched in the middle of the ramp). + void distance_value_range(const QVector4D &clipPlane, const QVector4D &plane_point, + double &vmin, double &vmax) + { + // Colour by distance to the clipping plane, anchored at the plane: 0 at the plane + // (one end of the palette), growing into the kept (solid) half up to its farthest + // point. The kept half is dot(pos-pt, n) > 0 (see onPlane in the shader). We do not + // use the symmetric bounding-box span, which would (a) shift with the plane so both + // range ends moved with the distances and the colours never changed when the plane + // was only translated (they did on rotation), and (b) advertise in the legend the + // colours of the clipped-away half, which no visible face shows. Anchored at the + // plane the colours sweep as the plane is moved (the farthest distance changes), and + // the legend matches the visible faces. + const CGAL::Bbox_3 b=m_scene.bounding_box(); + const QVector3D n=QVector3D(clipPlane).normalized(); + const QVector3D pt=plane_point.toVector3D(); + double dmax=0.0; + for (int c=0; c<8; ++c) + { + const QVector3D corner(float((c&1) ? b.xmax() : b.xmin()), + float((c&2) ? b.ymax() : b.ymin()), + float((c&4) ? b.zmax() : b.zmin())); + const double d=QVector3D::dotProduct(corner-pt, n); + if (d>dmax) { dmax=d; } + } + vmin=0.0; + vmax=dmax; + } + + // Colour by value: the palette colour for a volume's clip-plane cap. The cap faces + // carry no value of their own, so the cap takes the value the volume shows: its + // size, its centre distance, or the mean of its per-face values. + QColor volume_value_color(std::size_t v, const QVector4D &clipPlane, + const QVector4D &plane_point) + { + const std::vector> &vols=m_scene.get_volume_faces(); + double vmin, vmax, value; + if (m_color_value==3 && m_scene.has_face_values()) + { vmin=m_scene.face_value_min(); vmax=m_scene.face_value_max(); + const std::vector &fv=m_scene.get_face_values(); + double sum=0.0; std::size_t n=0; + for (unsigned int fi : vols[v]) { if (fi0) ? sum/double(n) : vmin; } + else if (m_color_value==2) + { if (!m_cell_sizes_valid) { compute_cell_sizes(); } + vmin=m_cell_size_min; vmax=m_cell_size_max; value=m_cell_sizes[v]; } + else + { distance_value_range(clipPlane, plane_point, vmin, vmax); + const CGAL::Bbox_3 &b=m_scene.get_volume_bboxes()[v]; + const QVector3D c(float((b.xmin()+b.xmax())*0.5), float((b.ymin()+b.ymax())*0.5), + float((b.zmin()+b.zmax())*0.5)); + value=QVector3D::dotProduct(c-plane_point.toVector3D(), + QVector3D(clipPlane).normalized()); } + double t=(vmax-vmin>1e-12) ? (value-vmin)/(vmax-vmin) : 0.0; + t=(t<0.0) ? 0.0 : (t>1.0 ? 1.0 : t); + return legend_palette_color(float(t)); + } + + // Colour by value: draw a small legend, a gradient bar with the value range, so + // the colours read as numbers. The range and label match the current value. + void draw_color_legend(const QVector4D &clipPlane, const QVector4D &plane_point) + { + double vmin, vmax; + QString label; + if (m_color_value==3 && m_scene.has_face_values()) + { vmin=m_scene.face_value_min(); vmax=m_scene.face_value_max(); + label=QString(m_scene.value_name().c_str()); } + else if (m_color_value==2 && !m_scene.get_volume_faces().empty()) + { if (!m_cell_sizes_valid) { compute_cell_sizes(); } + vmin=m_cell_size_min; vmax=m_cell_size_max; label=QString("size"); } + else + { distance_value_range(clipPlane, plane_point, vmin, vmax); label=QString("distance to clipping plane"); } + + // No range to map (a uniform value, e.g. a flat mesh with a parallel plane): + // skip the legend rather than show a misleading full gradient. The threshold is + // relative to the value magnitude, so it holds at any scale. + if (vmax-vmin<=1e-6*(std::fabs(vmin)+std::fabs(vmax))) { return; } + + const int barW=16, barH=150; + const int x=width()-barW-70, y=height()-barH-30; + QPainter painter(this); + for (int i=0; ikey()==::Qt::Key_D) && (modifiers==::Qt::NoButton)) + { + // Colour the faces by a value (here the distance to the clipping plane): + // off, then the heat, jet and grey palettes. + m_color_map=(m_color_map+1)%5; + switch(m_color_map) + { + case 0: displayMessage(QString("Colour by value = off")); break; + case 1: displayMessage(QString("Colour by value = heat")); break; + case 2: displayMessage(QString("Colour by value = jet")); break; + case 3: displayMessage(QString("Colour by value = grey")); break; + case 4: displayMessage(QString("Colour by value = viridis")); break; + default: break; + } + update(); + } + else if ((e->key()==::Qt::Key_D) && (modifiers==::Qt::ShiftModifier)) + { + // Colour by value: pick the value and how it is shown. Skip the modes that + // do not apply to the current scene: the per-cell and size modes need + // volumes, and the user value needs values set by the drawer. This keeps the + // message, the faces and the legend in agreement. + const bool has_vols=!m_scene.get_volume_faces().empty(); + const bool has_vals=m_scene.has_face_values(); + for (int step=1; step<=4; ++step) + { + const int m=(m_color_value+step)%4; + if (m==0 || ((m==1 || m==2) && has_vols) || (m==3 && has_vals)) + { m_color_value=m; break; } + } + switch(m_color_value) + { + case 0: displayMessage(QString("Colour by value = distance (smooth)")); break; + case 1: displayMessage(QString("Colour by value = distance (per cell)")); break; + case 2: displayMessage(QString("Colour by value = size (per cell)")); break; + case 3: displayMessage(QString("Colour by value = %1 (per face)").arg(m_scene.value_name().c_str())); break; + default: break; + } + update(); + } else if ((e->key()==::Qt::Key_Plus) && (!modifiers.testFlag(::Qt::ControlModifier))) // No ctrl { m_size_edges+=.5; @@ -2546,6 +2869,13 @@ class Basic_viewer : public CGAL::QGLViewer std::vector> m_edge_owners; // whole-volume clip: per edge, owning volumes std::vector> m_point_owners; // whole-volume clip: per vertex, owning volumes bool m_clip_owners_valid = false; // whole-volume clip: are the owner lists up to date + int m_color_map=0; // colour by value: 0 off, 1 heat, 2 jet, 3 grey ramp + int m_color_value=0; // colour by value source (Shift+D): 0 distance smooth, + // 1 distance per cell, 2 size per cell + std::vector m_cell_sizes; // colour by value: per-cell size (bbox volume) + float m_cell_size_min=0.f; // colour by value: size range for the palette + float m_cell_size_max=1.f; + bool m_cell_sizes_valid=false; // colour by value: recompute the sizes on scene change CGAL::qglviewer::ManipulatedFrame* m_frame_plane=nullptr; // Buffer for clipping plane is not stored in the scene because it is not diff --git a/Installation/CHANGES.md b/Installation/CHANGES.md index 22255b8075ad..c030976a1bff 100644 --- a/Installation/CHANGES.md +++ b/Installation/CHANGES.md @@ -34,6 +34,13 @@ Release date: December 2026 handle cases when some identical faces are shared between the input meshes. This leads to a significant speed up in those cases. +### [Basic Viewer](https://doc.cgal.org/6.3/Manual/packages.html#PkgBasicViewer) + +- Added the possibility to colour the faces by a value mapped to a colour palette: the + distance to the clipping plane, the cell size, or a scalar value provided by the drawer. + A drawer can attach a value to each face through the new `Graphics_scene_options` functions + `is_face_valued` and `face_value` (with `face_value_name` for the legend). As an example, the + surface mesh drawer exposes the aspect ratio of each face. ## [Release 6.2](https://github.com/CGAL/cgal/releases/tag/v6.2) diff --git a/Surface_mesh/include/CGAL/draw_surface_mesh.h b/Surface_mesh/include/CGAL/draw_surface_mesh.h index 1f1736dc37c6..03a4c0df83c6 100644 --- a/Surface_mesh/include/CGAL/draw_surface_mesh.h +++ b/Surface_mesh/include/CGAL/draw_surface_mesh.h @@ -144,6 +144,26 @@ struct Graphics_scene_options_surface_mesh } else { this->colored_face=[](const SM &, face_descriptor)->bool { return false; }; } + + // Colour by value: expose the aspect ratio (longest edge / shortest edge) of + // each face, so the viewer can colour the mesh by it (Shift+D). + this->face_value_name="aspect ratio"; + this->is_face_valued=[](const SM &, face_descriptor)->bool { return true; }; + this->face_value=[](const SM &sm, face_descriptor f)->float + { + double l2min=(std::numeric_limits::max)(), l2max=0.0; + auto h=halfedge(f, sm); const auto first=h; + do + { + const auto vec=sm.point(target(h, sm))-sm.point(source(h, sm)); + const double l2=CGAL::to_double(vec.squared_length()); + if (l2l2max) { l2max=l2; } + h=next(h, sm); + } + while (h!=first); + return (l2min>0.0) ? static_cast(std::sqrt(l2max/l2min)) : 1.f; + }; } private: