diff --git a/examples/hexgrid.cpp b/examples/hexgrid.cpp index f013d2e5..1bc1d8d0 100644 --- a/examples/hexgrid.cpp +++ b/examples/hexgrid.cpp @@ -1,109 +1,439 @@ /* * An example mplot::Visual scene, containing hexgrids. + * + * Hexagonal grids were the first VisualModel I coded in mathplot. The interest in hexgrids came + * from Stuart Wilson, who wanted to compute reaction diffusion systems in C++. Those early hexgrids + * took part in this paper about whisker barrels: https://elifesciences.org/articles/55588 + * + * This program shows some examples of the hexgrids that you can work with using sm::hexgrid and + * mplot::HexGridVisual. + * + * Author: Seb James */ #include #include #include #include +#include +#include +#include +#include import sm.vec; import sm.hexgrid; +import sm.algo.hexgrid; // hexgrid algorithms, particularly the ability to reduce down to the 'next-coarser' grid import mplot.visual; import mplot.visualdatamodel; import mplot.hexgridvisual; +// We make some dummy data (a sine wave) to show on our hexgrids and either colour or elevation +namespace hexexample +{ + template + void orthogonal_sines (sm::hexgrid& hg, sm::vvec& data) + { + data.resize (hg.num(), F{0}); + for (unsigned int ri = 0; ri < hg.num(); ++ri) { + data[ri] = 0.05f + 0.05f * std::sin (20.0f * hg.d_x[ri]) * std::sin (10.0f * hg.d_y[ri]) ; // Range 0->1 + } + } +} + int main() { // Contructor args are width, height, title - mplot::Visual v(1600, 1000, "mplot::HexGridVisual"); + mplot::Visual v(1800, 1800, "mplot::HexGridVisual examples"); + // Set inital scene translation + v.setSceneTrans (sm::vec{ float{-0.635992}, float{0.75472}, float{-6.91652} }); // You can set a field of view (in degrees) v.fov = 15; - // set the x/y offset. Try pressing 'z' in the app window to see what the current sceneTrans is - v.setSceneTransXY (0.0f, 0.0f); - // Make this larger to "scroll in and out of the image" faster. 0.02f is the default in VisualBase. - v.scenetrans_stepsize = 0.02f; - // The coordinate arrows can be hidden - v.showCoordArrows (true); + // The coordinate arrows can be shown/hidden + v.showCoordArrows (false); // You can set the background (white, black, or any other colour) v.backgroundWhite(); // You can switch on the "lighting shader" which puts diffuse light into the scene v.lightingEffects(); - // Add some text labels to the scene - //v.addLabel ("This is a\nmplot::HexGridVisual\nobject", {0.26f, -0.16f, 0.0f}); + v.rotateAboutNearest (true); - // Create a HexGrid to show in the scene. Hexes outside the circular boundary will - // all be discarded. + // Create a HexGrid to show in the scene. Hexes outside the circular boundary will all be + // discarded. sm::hexgrid hg1(0.01f, 0.205f, 0.0f); - //hg1.set_circular_boundary (0.1f); - hg1.set_boundary_on_outer_edge(); - std::cout << "Number of pixels in point_up grid:" << hg1.num() << std::endl; + hg1.set_circular_boundary (0.1f); + // Make data for hg1 + sm::vvec data1 = {}; + hexexample::orthogonal_sines (hg1, data1); + // A second hexgrid and data. This is a 'flat_up' grid sm::hexgrid hg2(0.01f, 0.205f, 0.0f); - //hg2.set_circular_boundary (0.1f); hg2.set_boundary_on_outer_edge(); - std::cout << "Number of pixels in flat_up grid:" << hg2.num() << std::endl; + sm::vvec data2 = {}; + hexexample::orthogonal_sines (hg2, data2); - // Make some dummy data (a sine wave) to make an interesting surface - std::vector data1(hg1.num(), 0.0f); - for (unsigned int ri = 0; ri < hg1.num(); ++ri) { - data1[ri] = 0.05f + 0.05f * std::sin (20.0f * hg1.d_x[ri]) * std::sin (10.0f * hg1.d_y[ri]) ; // Range 0->1 - } - std::vector data2(hg2.num(), 0.0f); - for (unsigned int ri = 0; ri < hg2.num(); ++ri) { - data2[ri] = 0.05f + 0.05f * std::sin (20.0f * hg2.d_x[ri]) * std::sin (10.0f * hg2.d_y[ri]) ; // Range 0->1 - } + // Reduced hexgrid, copied from hg1r + sm::hexgrid hg1r = sm::algo::hexgrid::reduced_hexgrid (hg1); + sm::vvec data1r = {}; + hexexample::orthogonal_sines (hg1r, data1r); + + // Doubly reduced hexgrid + sm::hexgrid hg1rr = sm::algo::hexgrid::reduced_hexgrid (hg1r); + sm::vvec data1rr = {}; + hexexample::orthogonal_sines (hg1rr, data1rr); + + // And again + sm::hexgrid hg1rrr = sm::algo::hexgrid::reduced_hexgrid (hg1rr); + sm::vvec data1rrr = {}; + hexexample::orthogonal_sines (hg1rrr, data1rrr); + + // More flat up grids (for reduction) + sm::hexgrid hgf1(0.01f, 0.205f, 0.0f); + hgf1.set_circular_boundary (0.1f); + sm::vvec dataf1 = {}; + hexexample::orthogonal_sines (hgf1, dataf1); + + // Create a reduced hexgrid + sm::hexgrid hgf1r = sm::algo::hexgrid::reduced_hexgrid (hgf1); + // Regenerate data for hgf1r + sm::vvec dataf1r = {}; + hexexample::orthogonal_sines (hgf1r, dataf1r); + // Also *map* data from dataf1 onto dataf1r_mapped + auto mapping1 = sm::algo::hexgrid::reduced_hexgrid_mapping (hgf1, hgf1r); + sm::vvec dataf1r_mapped = {}; + sm::algo::hexgrid::map_to_reduced_hexgrid (mapping1, dataf1, dataf1r_mapped); + + sm::hexgrid hgf1rr = sm::algo::hexgrid::reduced_hexgrid (hgf1r); + sm::vvec dataf1rr = {}; + hexexample::orthogonal_sines (hgf1rr, dataf1rr); - // sm::HexVisMode::HexInterp to see the hexagons or sm::HexVisMode::Triangles for a smoother surface plot - const mplot::HexVisMode visMode = mplot::HexVisMode::HexInterp; + // Layout parameters + constexpr float rh = 0.46f; // row height + constexpr float cw = 0.3f; // col width + sm::vec label_pos = { -0.1f, -0.15f, 0.0f }; + const mplot::TextFeatures tf(0.02f); + + /* + * Row 1 + */ // Add a HexGridVisual to display the HexGrid within the sm::Visual scene - sm::vec offset = { 0.0f, -0.05f, 0.0f }; - auto hgv1 = std::make_unique>(&hg1, offset); - hgv1->set_parent (v.get_id()); - hgv1->cm.setType (mplot::ColourMapType::Ice); - hgv1->setScalarData (&data1); - hgv1->hexVisMode = visMode; - hgv1->zScale.null_scaling(); - hgv1->addLabel ("hexalign::point_up", sm::vec<>{ -hg1.width()/2.1f, -hg1.width()/1.6f }, mplot::TextFeatures(0.02f)); - hgv1->finalize(); - v.addVisualModel (hgv1); - - offset[0] += 1.1f * hg1.width(); - auto hgv2 = std::make_unique>(&hg2, offset); - hgv2->set_parent (v.get_id()); - hgv2->cm.setType (mplot::ColourMapType::Ice); - hgv2->setScalarData (&data2); - hgv2->hexVisMode = visMode; - hgv2->zScale.null_scaling(); - // You can transform the view of the hexgrid in HexGridVisual (transforms may also be applied to the underlying hexgrid) - // This applies a scaling first, and then a reflection. - //hgv2->tfm = sm::mat::reflection (sm::vec::ux()) * sm::mat::identity() * 0.5f; - hgv2->addLabel ("hexalign::flat_up", sm::vec<>{ -hg1.width()/2.6f, -hg1.width()/1.6f }, mplot::TextFeatures(0.02f)); - hgv2->finalize(); - - if (v.checkContext() == true) { - std::cout << "I have the context after hgv->finalize()\n"; - } else { - std::cout << "I don't have the context after hgv->finalize()\n"; - } + sm::vec offset = { 0.0f, 0.0f, 0.0f }; + auto hgv_pu = std::make_unique>(&hg1, offset); + hgv_pu->set_parent (v.get_id()); + hgv_pu->cm.setType (mplot::ColourMapType::CET_L04); + hgv_pu->setScalarData (&data1); + hgv_pu->hexVisMode = mplot::HexVisMode::HexInterp; + hgv_pu->addLabel ("hexalign::point_up\n3D (value is z and colour)\ncircular boundary\nboundary not shown\nHexVisMode::HexInterp\nColourMapType::CET_L04", + label_pos, tf); + hgv_pu->finalize(); + v.addVisualModel (hgv_pu); - v.addVisualModel (hgv2); + offset[0] += cw; + auto hgv_fl = std::make_unique>(&hg2, offset); + hgv_fl->set_parent (v.get_id()); + hgv_fl->cm.setType (mplot::ColourMapType::Cork); + hgv_fl->showboundary = true; + hgv_fl->setScalarData (&data2); + hgv_fl->hexVisMode = mplot::HexVisMode::HexInterp; + hgv_fl->zScale.null_scaling(); + hgv_fl->addLabel ("hexalign::flat_up\n2D (value is only colour)\noriginal hex boundary\nboundary indicated in black\nHexVisMode::HexInterp\nColourMapType::Cork", + label_pos, tf); + hgv_fl->finalize(); + v.addVisualModel (hgv_fl); - if (v.checkContext() == true) { - std::cout << "I have the context after addVisualModel\n"; - } else { - std::cout << "I don't have the context after addVisualModel()\n"; - } + offset[0] += cw; + hgv_pu = std::make_unique>(&hg1, offset); + hgv_pu->set_parent (v.get_id()); + hgv_pu->cm.setType (mplot::ColourMapType::Batlow); + hgv_pu->showboundary = true; + hgv_pu->setScalarData (&data1); + hgv_pu->hexVisMode = mplot::HexVisMode::Triangles; + hgv_pu->zScale.null_scaling(); + hgv_pu->addLabel ("hexalign::point_up\n2D\ncircular boundary\nboundary shown\nHexVisMode::Triangles\nColourMapType::Batlow", + label_pos, tf); + hgv_pu->finalize(); + v.addVisualModel (hgv_pu); - v.keepOpen(); + offset[0] += cw; + hgv_pu = std::make_unique>(&hg1, offset); + hgv_pu->set_parent (v.get_id()); + hgv_pu->cm.setType (mplot::ColourMapType::Plasma); + hgv_pu->showboundary = true; + hgv_pu->setScalarData (&data1); + hgv_pu->hexVisMode = mplot::HexVisMode::HexInterp; + hgv_pu->zScale.null_scaling(); + hgv_pu->addLabel ("hexalign::point_up\n2D\ncircular boundary\nboundary shown\nHexVisMode::HexInterp\nColourMapType::Plasma", + label_pos, tf); + hgv_pu->finalize(); + v.addVisualModel (hgv_pu); - if (v.checkContext() == true) { - std::cout << "I have the context after user requested exit\n"; - } else { - std::cout << "I don't have the context after user requested exit\n"; - } + offset[0] += cw; + hgv_pu = std::make_unique>(&hg1, offset); + hgv_pu->set_parent (v.get_id()); + hgv_pu->cm.setType (mplot::ColourMapType::Acton); + hgv_pu->showboundary = true; + hgv_pu->setScalarData (&data1); + hgv_pu->hexVisMode = mplot::HexVisMode::HexInterp; + hgv_pu->addLabel ("hexalign::point_up\n3D (boundary always at z=0)\ncircular boundary\nboundary shown\nHexVisMode::HexInterp\nColourMapType::Acton", + label_pos, tf); + hgv_pu->finalize(); + v.addVisualModel (hgv_pu); + + /* + * Row 2 + */ + label_pos = { -0.1f, -0.175f, 0.0f }; + + offset = {0.0f, -rh, 0.0f }; + hgv_pu = std::make_unique>(&hg1, offset); + hgv_pu->set_parent (v.get_id()); + hgv_pu->cm.setType (mplot::ColourMapType::Ice); + hgv_pu->showboundary = true; + hgv_pu->setScalarData (&data1); + hgv_pu->hexVisMode = mplot::HexVisMode::HexInterp; + hgv_pu->zScale.null_scaling(); + hgv_pu->addLabel ("hexalign::point_up\n2D (flat)\ncircular boundary\nboundary shown\nColourMapType::Ice", label_pos, tf); + hgv_pu->finalize(); + v.addVisualModel (hgv_pu); + + offset = {cw, -rh, 0.01f }; + hgv_pu = std::make_unique>(&hg1r, offset); + hgv_pu->set_parent (v.get_id()); + hgv_pu->cm.setType (mplot::ColourMapType::Ice); + hgv_pu->showboundary = true; + hgv_pu->setScalarData (&data1r); + hgv_pu->hexVisMode = mplot::HexVisMode::HexInterp; + hgv_pu->zScale.null_scaling(); + hgv_pu->addLabel ("Grid reduced once", label_pos, tf); + hgv_pu->finalize(); + v.addVisualModel (hgv_pu); + + offset = {2 * cw, -rh, 0.02f }; + hgv_pu = std::make_unique>(&hg1rr, offset); + hgv_pu->set_parent (v.get_id()); + hgv_pu->cm.setType (mplot::ColourMapType::Ice); + hgv_pu->showboundary = true; + hgv_pu->setScalarData (&data1rr); + hgv_pu->hexVisMode = mplot::HexVisMode::HexInterp; + hgv_pu->zScale.null_scaling(); + hgv_pu->addLabel ("Grid reduced twice", label_pos, tf); + hgv_pu->finalize(); + v.addVisualModel (hgv_pu); + + offset = {3 * cw, -rh, 0.0f }; + hgv_pu = std::make_unique>(&hg1, offset); + hgv_pu->set_parent (v.get_id()); + hgv_pu->cm.setType (mplot::ColourMapType::Ice); + hgv_pu->setScalarData (&data1); + hgv_pu->hexVisMode = mplot::HexVisMode::HexInterp; + hgv_pu->zScale.null_scaling(); + hgv_pu->addLabel ("Previous 3 shown layered\nLayers separated in z\nTop 2 transparent", label_pos, tf); + hgv_pu->finalize(); + v.addVisualModel (hgv_pu); + + offset = {3 * cw, -rh, 0.01f }; + hgv_pu = std::make_unique>(&hg1r, offset); + hgv_pu->set_parent (v.get_id()); + hgv_pu->cm.setType (mplot::ColourMapType::Ice); + hgv_pu->showboundary = true; + hgv_pu->setAlpha (0.5f); + hgv_pu->setScalarData (&data1r); + hgv_pu->hexVisMode = mplot::HexVisMode::HexInterp; + hgv_pu->zScale.null_scaling(); + hgv_pu->finalize(); + v.addVisualModel (hgv_pu); - return 0; + offset = {3 * cw, -rh, 0.02f }; + hgv_pu = std::make_unique>(&hg1rr, offset); + hgv_pu->set_parent (v.get_id()); + hgv_pu->cm.setType (mplot::ColourMapType::Ice); + hgv_pu->showboundary = true; + hgv_pu->setAlpha (0.5f); + hgv_pu->setScalarData (&data1rr); + hgv_pu->hexVisMode = mplot::HexVisMode::HexInterp; + hgv_pu->zScale.null_scaling(); + hgv_pu->finalize(); + v.addVisualModel (hgv_pu); + + /* + * Row 3 + */ + + offset = {0.0f, -2 * rh, 0.0f }; + hgv_fl = std::make_unique>(&hgf1, offset); + hgv_fl->set_parent (v.get_id()); + hgv_fl->cm.setType (mplot::ColourMapType::CET_L07); + hgv_fl->showboundary = true; + hgv_fl->setScalarData (&dataf1); + hgv_fl->hexVisMode = mplot::HexVisMode::HexInterp; + hgv_fl->zScale.null_scaling(); + hgv_fl->addLabel ("hexalign::flat_up\n2D (flat)\ncircular boundary\nboundary shown\nColourMapType::CET_L07", label_pos, tf); + hgv_fl->finalize(); + v.addVisualModel (hgv_fl); + + offset = {cw, -2 * rh, 0.01f }; + hgv_fl = std::make_unique>(&hgf1r, offset); + hgv_fl->set_parent (v.get_id()); + hgv_fl->cm.setType (mplot::ColourMapType::CET_L07); + hgv_fl->showboundary = true; + hgv_fl->setScalarData (&dataf1r); + hgv_fl->hexVisMode = mplot::HexVisMode::HexInterp; + hgv_fl->zScale.null_scaling(); + hgv_fl->addLabel ("Grid reduced once\nData regenerated", label_pos, tf); + hgv_fl->finalize(); + v.addVisualModel (hgv_fl); + + offset = {2 * cw, -2 * rh, 0.01f }; + hgv_fl = std::make_unique>(&hgf1r, offset); + hgv_fl->set_parent (v.get_id()); + hgv_fl->cm.setType (mplot::ColourMapType::CET_L07); + hgv_fl->showboundary = true; + hgv_fl->setScalarData (&dataf1r_mapped); + hgv_fl->hexVisMode = mplot::HexVisMode::HexInterp; + hgv_fl->zScale.null_scaling(); + hgv_fl->addLabel ("Grid reduced once\nData mapped", label_pos, tf); + hgv_fl->finalize(); + v.addVisualModel (hgv_fl); + + offset = {3 * cw, -2 * rh, 0.02f }; + hgv_fl = std::make_unique>(&hgf1rr, offset); + hgv_fl->set_parent (v.get_id()); + hgv_fl->cm.setType (mplot::ColourMapType::CET_L07); + hgv_fl->showboundary = true; + hgv_fl->setScalarData (&dataf1rr); + hgv_fl->hexVisMode = mplot::HexVisMode::HexInterp; + hgv_fl->zScale.null_scaling(); + hgv_fl->addLabel ("Grid reduced twice", label_pos, tf); + hgv_fl->finalize(); + v.addVisualModel (hgv_fl); + + offset = {4 * cw, -2 * rh, 0.0f }; + hgv_fl = std::make_unique>(&hgf1, offset); + hgv_fl->set_parent (v.get_id()); + hgv_fl->cm.setType (mplot::ColourMapType::CET_L07); + hgv_fl->setScalarData (&dataf1); + hgv_fl->hexVisMode = mplot::HexVisMode::HexInterp; + hgv_fl->zScale.null_scaling(); + hgv_fl->addLabel ("Previous 3 shown layered\nLayers separated in z\nUpper two have setAlpha(0.5)", label_pos, tf); + hgv_fl->finalize(); + v.addVisualModel (hgv_fl); + + offset = {4 * cw, -2 * rh, 0.01f }; + hgv_fl = std::make_unique>(&hgf1r, offset); + hgv_fl->set_parent (v.get_id()); + hgv_fl->cm.setType (mplot::ColourMapType::CET_L07); + hgv_fl->showboundary = true; + hgv_fl->setAlpha (0.5f); + hgv_fl->setScalarData (&dataf1r); + hgv_fl->hexVisMode = mplot::HexVisMode::HexInterp; + hgv_fl->zScale.null_scaling(); + hgv_fl->finalize(); + v.addVisualModel (hgv_fl); + + offset = {4 * cw, -2 * rh, 0.02f }; + hgv_fl = std::make_unique>(&hgf1rr, offset); + hgv_fl->set_parent (v.get_id()); + hgv_fl->cm.setType (mplot::ColourMapType::CET_L07); + hgv_fl->showboundary = true; + hgv_fl->setAlpha (0.5f); + hgv_fl->setScalarData (&dataf1rr); + hgv_fl->hexVisMode = mplot::HexVisMode::HexInterp; + hgv_fl->zScale.null_scaling(); + hgv_fl->finalize(); + v.addVisualModel (hgv_fl); + + /* + * Row 3 + */ + sm::mat rotn; + rotn.rotate (sm::mathconst::pi_over_4); + hgf1.transform (rotn); + + // Regenerate data/reduced grids + hexexample::orthogonal_sines (hgf1, dataf1); + + sm::hexgrid hgf1rt = sm::algo::hexgrid::reduced_hexgrid (hgf1); + // Regenerate data for hgf1rt + sm::vvec dataf1rt = {}; + hexexample::orthogonal_sines (hgf1r, dataf1rt); + + sm::hexgrid hgf1rrt = sm::algo::hexgrid::reduced_hexgrid (hgf1rt); + sm::vvec dataf1rrt = {}; + hexexample::orthogonal_sines (hgf1rrt, dataf1rrt); + + offset = {0.0f, -3 * rh, 0.0f }; + hgv_fl = std::make_unique>(&hgf1, offset); + hgv_fl->set_parent (v.get_id()); + hgv_fl->cm.setType (mplot::ColourMapType::Ocean); + hgv_fl->showboundary = true; + hgv_fl->setScalarData (&dataf1); + hgv_fl->hexVisMode = mplot::HexVisMode::HexInterp; + hgv_fl->zScale.null_scaling(); + hgv_fl->addLabel ("flat_up, rotated pi/4 (ccw)\n2D (flat)\ncircular boundary\nboundary shown\nColourMapType::Ocean", label_pos, tf); + hgv_fl->finalize(); + v.addVisualModel (hgv_fl); + + offset = {cw, -3 * rh, 0.01f }; + hgv_fl = std::make_unique>(&hgf1rt, offset); + hgv_fl->set_parent (v.get_id()); + hgv_fl->cm.setType (mplot::ColourMapType::Ocean); + hgv_fl->showboundary = true; + hgv_fl->setScalarData (&dataf1rt); + hgv_fl->hexVisMode = mplot::HexVisMode::HexInterp; + hgv_fl->zScale.null_scaling(); + hgv_fl->addLabel ("Grid reduced once\nData regenerated", label_pos, tf); + hgv_fl->finalize(); + v.addVisualModel (hgv_fl); + + offset = {2 * cw, -3 * rh, 0.02f }; + hgv_fl = std::make_unique>(&hgf1rrt, offset); + hgv_fl->set_parent (v.get_id()); + hgv_fl->cm.setType (mplot::ColourMapType::Ocean); + hgv_fl->showboundary = true; + hgv_fl->setScalarData (&dataf1rrt); + hgv_fl->hexVisMode = mplot::HexVisMode::HexInterp; + hgv_fl->zScale.null_scaling(); + hgv_fl->addLabel ("Grid reduced twice", label_pos, tf); + hgv_fl->finalize(); + v.addVisualModel (hgv_fl); + + // Lastly, a dynamic HexGridVisual. + + // Dynamic hexgrid data is a Bessel function + std::vector dyn_data(hg1.num(), 0.0f); + std::vector r(hg1.num(), 0.0f); + float k = 1.0f; + auto compute_bessel = [&r, &dyn_data, hg1] (const float k) { + for (std::uint32_t hi = 0; hi < hg1.num(); ++hi) { + r[hi] = std::sqrt (hg1.d_x[hi] * hg1.d_x[hi] + hg1.d_y[hi] * hg1.d_y[hi]); + dyn_data[hi] = std::sin (100.0f * k * r[hi]) / k * r[hi]; + } + }; + + // Compute first data state + compute_bessel (k); + + // Add a HexGridVisual to display the HexGrid within the mplot::Visual scene + offset = {4 * cw, -3 * rh, 0.02f }; + auto hgv = std::make_unique>(&hg1, offset); + hgv->set_parent (v.get_id()); + hgv->setScalarData (&dyn_data); + hgv->showboundary = true; + hgv->cm.setType (mplot::ColourMapType::CET_L09); + hgv->hexVisMode = mplot::HexVisMode::Triangles; + hgv->addLabel ("Dynamic hexgrid\nBessel function\nHexVisMode::Triangles\nColourMapType::CET_L09", label_pos, tf); + hgv->finalize(); + auto hgvp = v.addVisualModel (hgv); + + // Now loop, updating the dynamic hexgrid + while (v.readyToFinish() == false) { + v.wait (0.08); + if (k > 8.0f) { k = 1.0f; } + compute_bessel (k); + k += 0.1f; + if (v.validVisualModel (hgvp) != nullptr) { // Test hgvp is still valid + hgvp->updateData (&dyn_data); + } + v.render(); + } } diff --git a/examples/screenshots/hexgrid.png b/examples/screenshots/hexgrid.png index ab27d748..b10acef6 100644 Binary files a/examples/screenshots/hexgrid.png and b/examples/screenshots/hexgrid.png differ diff --git a/maths b/maths index 46b2a4d5..97cbc49b 160000 --- a/maths +++ b/maths @@ -1 +1 @@ -Subproject commit 46b2a4d5c5007c3928b6916d5b3baef4b5281da9 +Subproject commit 97cbc49b0fa7fe413107e585e170679d31b6eb3a