Skip to content

Commit 23e606c

Browse files
imikejacksonclaude
andcommitted
feat: add GriddedColorKey decorator for MTEX-style legend rendering
Implements the Decorator pattern: GriddedColorKey wraps any IColorKey with grid-based flat shading. Colors are precomputed at regular angular intervals (default 1 degree, matching MTEX) and looked up via nearest- grid-point snapping. This produces flat-colored patches that hide C1 discontinuities in the underlying color function. Adds LegendRenderMode enum and setLegendRenderMode() convenience method to LaueOps for switching between PerPixel (EbsdLib default) and GridInterpolated (MTEX-style) rendering. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ea7451a commit 23e606c

8 files changed

Lines changed: 416 additions & 1 deletion

File tree

Source/Apps/generate_ipf_legends.cpp

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include "EbsdLib/Utilities/ColorTable.h"
1919
#include "EbsdLib/Utilities/EbsdStringUtils.hpp"
2020
#include "EbsdLib/Utilities/FundamentalSectorGeometry.hpp"
21+
#include "EbsdLib/Utilities/GriddedColorKey.hpp"
2122
#include "EbsdLib/Utilities/NolzeHielscherColorKey.hpp"
2223
#include "EbsdLib/Utilities/TSLColorKey.hpp"
2324
#include "EbsdLib/Utilities/TiffWriter.h"
@@ -374,6 +375,23 @@ void GenerateNolzeHielscherLegends(int imageDim)
374375
result = TiffWriter::WriteColorImage(ss.str(), imageDim, imageDim, 3, legend->getPointer(0));
375376
std::cout << ops.getSymmetryName() << " NH Triangle Result: " << result.first << ": " << result.second << std::endl;
376377

378+
// Set to grid-interpolated mode (MTEX-style rendering)
379+
ops.setLegendRenderMode(ebsdlib::LegendRenderMode::GridInterpolated, 1.0);
380+
381+
// Generate gridded full-circle legend
382+
legend = ops.generateIPFTriangleLegend(imageDim, true);
383+
ss.str("");
384+
ss << k_Output_Dir << "/" << symName << "/" << symName << "_NH_GRIDDED_FULL.tiff";
385+
result = TiffWriter::WriteColorImage(ss.str(), imageDim, imageDim, 3, legend->getPointer(0));
386+
std::cout << ops.getSymmetryName() << " NH Gridded Full Result: " << result.first << ": " << result.second << std::endl;
387+
388+
// Generate gridded triangle-only legend
389+
legend = ops.generateIPFTriangleLegend(imageDim, false);
390+
ss.str("");
391+
ss << k_Output_Dir << "/" << symName << "/" << symName << "_NH_GRIDDED.tiff";
392+
result = TiffWriter::WriteColorImage(ss.str(), imageDim, imageDim, 3, legend->getPointer(0));
393+
std::cout << ops.getSymmetryName() << " NH Gridded Triangle Result: " << result.first << ": " << result.second << std::endl;
394+
377395
// Reset to TSL for subsequent operations
378396
ops.setColorKey(std::make_shared<ebsdlib::TSLColorKey>());
379397
}
@@ -393,7 +411,7 @@ int main(int argc, char* argv[])
393411
}
394412

395413
std::stringstream ss;
396-
int imageDim = 512;
414+
int imageDim = 1500;
397415
{
398416
TrigonalOps ops;
399417
auto legend = ops.generateIPFTriangleLegend(imageDim, true);

Source/EbsdLib/LaueOps/LaueOps.cpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
#include "EbsdLib/Orientation/Quaternion.hpp"
5252
#include "EbsdLib/Utilities/ColorTable.h"
5353
#include "EbsdLib/Utilities/ComputeStereographicProjection.h"
54+
#include "EbsdLib/Utilities/GriddedColorKey.hpp"
5455
#include "EbsdLib/Utilities/TSLColorKey.hpp"
5556

5657
#include <algorithm> // for std::max
@@ -113,6 +114,32 @@ ebsdlib::IColorKey::Pointer LaueOps::getColorKey() const
113114
return m_ColorKey;
114115
}
115116

117+
// -----------------------------------------------------------------------------
118+
void LaueOps::setLegendRenderMode(ebsdlib::LegendRenderMode mode, double gridResolutionDeg)
119+
{
120+
if(mode == ebsdlib::LegendRenderMode::GridInterpolated)
121+
{
122+
// Wrap the current color key with a GriddedColorKey if not already wrapped
123+
auto currentKey = m_ColorKey;
124+
// If already gridded, unwrap first to avoid double-wrapping
125+
auto griddedKey = std::dynamic_pointer_cast<ebsdlib::GriddedColorKey>(currentKey);
126+
if(griddedKey)
127+
{
128+
currentKey = griddedKey->innerKey();
129+
}
130+
m_ColorKey = std::make_shared<ebsdlib::GriddedColorKey>(currentKey, gridResolutionDeg);
131+
}
132+
else
133+
{
134+
// PerPixel mode: unwrap if currently gridded
135+
auto griddedKey = std::dynamic_pointer_cast<ebsdlib::GriddedColorKey>(m_ColorKey);
136+
if(griddedKey)
137+
{
138+
m_ColorKey = griddedKey->innerKey();
139+
}
140+
}
141+
}
142+
116143
// -----------------------------------------------------------------------------
117144
std::string LaueOps::FZTypeToString(const FZType value)
118145
{

Source/EbsdLib/LaueOps/LaueOps.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
#include "EbsdLib/Orientation/OrientationFwd.hpp"
4747
#include "EbsdLib/Orientation/Quaternion.hpp"
4848
#include "EbsdLib/Orientation/Rodrigues.hpp"
49+
#include "EbsdLib/Utilities/GriddedColorKey.hpp"
4950
#include "EbsdLib/Utilities/IColorKey.hpp"
5051
#include "EbsdLib/Utilities/PoleFigureUtilities.h"
5152
#include "EbsdLib/Utilities/TSLColorKey.hpp"
@@ -302,6 +303,15 @@ class EbsdLib_EXPORT LaueOps
302303
*/
303304
ebsdlib::IColorKey::Pointer getColorKey() const;
304305

306+
/**
307+
* @brief Set the legend rendering mode.
308+
* PerPixel: exact color at every pixel (default, current behavior)
309+
* GridInterpolated: MTEX-style flat-shaded grid cells at the given resolution
310+
* @param mode The rendering mode to use
311+
* @param gridResolutionDeg Grid cell size in degrees (only used for GridInterpolated mode)
312+
*/
313+
void setLegendRenderMode(ebsdlib::LegendRenderMode mode, double gridResolutionDeg = 1.0);
314+
305315
/**
306316
* @brief generateRodriguesColor Generates an RGB Color from a Rodrigues Vector
307317
* @param r1 First component of the Rodrigues Vector
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#include "EbsdLib/Utilities/GriddedColorKey.hpp"
2+
3+
#include <algorithm>
4+
#include <cmath>
5+
6+
namespace ebsdlib
7+
{
8+
9+
namespace
10+
{
11+
constexpr double k_Pi = 3.14159265358979323846;
12+
constexpr double k_HalfPi = k_Pi / 2.0;
13+
constexpr double k_DegToRad = k_Pi / 180.0;
14+
} // namespace
15+
16+
GriddedColorKey::GriddedColorKey(IColorKey::Pointer innerKey, double resolutionDeg)
17+
: m_InnerKey(std::move(innerKey))
18+
, m_ResolutionDeg(resolutionDeg)
19+
, m_ResolutionRad(resolutionDeg * k_DegToRad)
20+
{
21+
// Grid covers eta in [0, pi] (180 degrees) and chi in [0, pi/2] (90 degrees)
22+
// This covers all possible Laue group SSTs
23+
m_EtaSteps = static_cast<int>(std::ceil(180.0 / resolutionDeg)) + 1;
24+
m_ChiSteps = static_cast<int>(std::ceil(90.0 / resolutionDeg)) + 1;
25+
precomputeGrid();
26+
}
27+
28+
void GriddedColorKey::precomputeGrid()
29+
{
30+
m_Grid.resize(m_EtaSteps);
31+
32+
for(int ei = 0; ei < m_EtaSteps; ei++)
33+
{
34+
m_Grid[ei].resize(m_ChiSteps);
35+
double eta = static_cast<double>(ei) * m_ResolutionRad;
36+
37+
for(int ci = 0; ci < m_ChiSteps; ci++)
38+
{
39+
double chi = static_cast<double>(ci) * m_ResolutionRad;
40+
41+
// Convert spherical to Cartesian direction and compute color
42+
// via the inner key's Vec3 overload
43+
double sinChi = std::sin(chi);
44+
double cosChi = std::cos(chi);
45+
Vec3 dir = {sinChi * std::cos(eta), sinChi * std::sin(eta), cosChi};
46+
47+
m_Grid[ei][ci] = m_InnerKey->direction2Color(dir);
48+
}
49+
}
50+
}
51+
52+
GriddedColorKey::Vec3 GriddedColorKey::lookupGrid(double eta, double chi) const
53+
{
54+
// Map to grid indices via nearest-neighbor snapping
55+
int ei = static_cast<int>(std::round(eta / m_ResolutionRad));
56+
int ci = static_cast<int>(std::round(chi / m_ResolutionRad));
57+
58+
// Clamp to grid bounds
59+
ei = std::clamp(ei, 0, m_EtaSteps - 1);
60+
ci = std::clamp(ci, 0, m_ChiSteps - 1);
61+
62+
return m_Grid[ei][ci];
63+
}
64+
65+
GriddedColorKey::Vec3 GriddedColorKey::direction2Color(const Vec3& direction) const
66+
{
67+
// Convert direction to (eta, chi) and look up from grid
68+
double chi = std::acos(std::clamp(direction[2], -1.0, 1.0));
69+
double eta = std::atan2(direction[1], direction[0]);
70+
if(eta < 0.0)
71+
{
72+
eta += 2.0 * k_Pi;
73+
}
74+
return lookupGrid(eta, chi);
75+
}
76+
77+
GriddedColorKey::Vec3 GriddedColorKey::direction2Color(double eta, double chi, const Vec3& angleLimits) const
78+
{
79+
// Snap eta and chi to nearest grid point (flat shading).
80+
// Instead of computing the exact color at (eta, chi),
81+
// we return the precomputed color at the nearest grid point.
82+
// This produces flat-colored patches like MTEX's surf() rendering.
83+
double etaPositive = eta;
84+
if(etaPositive < 0.0)
85+
{
86+
etaPositive += 2.0 * k_Pi;
87+
}
88+
return lookupGrid(etaPositive, chi);
89+
}
90+
91+
std::string GriddedColorKey::name() const
92+
{
93+
return m_InnerKey->name() + " (gridded)";
94+
}
95+
96+
IColorKey::Pointer GriddedColorKey::innerKey() const
97+
{
98+
return m_InnerKey;
99+
}
100+
101+
double GriddedColorKey::resolutionDeg() const
102+
{
103+
return m_ResolutionDeg;
104+
}
105+
106+
} // namespace ebsdlib
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#pragma once
2+
3+
#include "EbsdLib/EbsdLib.h"
4+
#include "EbsdLib/Utilities/IColorKey.hpp"
5+
6+
#include <memory>
7+
#include <string>
8+
#include <vector>
9+
10+
namespace ebsdlib
11+
{
12+
13+
/**
14+
* @brief Rendering mode for IPF legend generation.
15+
*/
16+
enum class LegendRenderMode
17+
{
18+
PerPixel, ///< Compute exact color at every pixel (EbsdLib default)
19+
GridInterpolated ///< Sample at grid points, flat-shade cells (MTEX-style)
20+
};
21+
22+
/**
23+
* @brief Decorator that wraps any IColorKey with grid-based flat shading.
24+
*
25+
* On construction, precomputes colors at a regular grid of (eta, chi) sample
26+
* points by calling the inner color key. When direction2Color() is called,
27+
* it snaps the direction to the nearest grid point and returns the precomputed
28+
* color (flat shading), producing smooth color patches that hide C1
29+
* discontinuities in the underlying color function.
30+
*
31+
* This replicates the MTEX rendering approach where colors are sampled at
32+
* ~1-degree intervals and rendered as flat-colored quadrilateral patches.
33+
*
34+
* Usage:
35+
* auto nhKey = std::make_shared<NolzeHielscherColorKey>(sector);
36+
* auto gridKey = std::make_shared<GriddedColorKey>(nhKey, 1.0);
37+
* ops.setColorKey(gridKey);
38+
*/
39+
class EbsdLib_EXPORT GriddedColorKey : public IColorKey
40+
{
41+
public:
42+
/**
43+
* @brief Construct a grid-decorated color key.
44+
* @param innerKey The underlying color key to sample from
45+
* @param resolutionDeg Grid cell size in degrees (default 1.0, matching MTEX)
46+
*/
47+
explicit GriddedColorKey(IColorKey::Pointer innerKey, double resolutionDeg = 1.0);
48+
~GriddedColorKey() override = default;
49+
50+
Vec3 direction2Color(const Vec3& direction) const override;
51+
Vec3 direction2Color(double eta, double chi, const Vec3& angleLimits) const override;
52+
std::string name() const override;
53+
54+
/**
55+
* @brief Get the underlying (unwrapped) color key.
56+
*/
57+
IColorKey::Pointer innerKey() const;
58+
59+
/**
60+
* @brief Get the grid resolution in degrees.
61+
*/
62+
double resolutionDeg() const;
63+
64+
private:
65+
IColorKey::Pointer m_InnerKey;
66+
double m_ResolutionRad; // grid cell size in radians
67+
68+
// Precomputed color grid: m_Grid[etaIdx][chiIdx] = RGB color
69+
// Covers eta in [0, pi] and chi in [0, pi/2] to handle all Laue groups
70+
std::vector<std::vector<Vec3>> m_Grid;
71+
int m_EtaSteps;
72+
int m_ChiSteps;
73+
double m_ResolutionDeg;
74+
75+
void precomputeGrid();
76+
Vec3 lookupGrid(double eta, double chi) const;
77+
};
78+
79+
} // namespace ebsdlib

Source/EbsdLib/Utilities/SourceList.cmake

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ set(EbsdLib_${DIR_NAME}_HDRS
2828
${EbsdLibProj_SOURCE_DIR}/Source/EbsdLib/${DIR_NAME}/FundamentalSectorGeometry.hpp
2929
${EbsdLibProj_SOURCE_DIR}/Source/EbsdLib/${DIR_NAME}/TSLColorKey.hpp
3030
${EbsdLibProj_SOURCE_DIR}/Source/EbsdLib/${DIR_NAME}/NolzeHielscherColorKey.hpp
31+
${EbsdLibProj_SOURCE_DIR}/Source/EbsdLib/${DIR_NAME}/GriddedColorKey.hpp
3132
)
3233

3334
set(EbsdLib_${DIR_NAME}_SRCS
@@ -47,6 +48,7 @@ set(EbsdLib_${DIR_NAME}_SRCS
4748
${EbsdLibProj_SOURCE_DIR}/Source/EbsdLib/${DIR_NAME}/FundamentalSectorGeometry.cpp
4849
${EbsdLibProj_SOURCE_DIR}/Source/EbsdLib/${DIR_NAME}/TSLColorKey.cpp
4950
${EbsdLibProj_SOURCE_DIR}/Source/EbsdLib/${DIR_NAME}/NolzeHielscherColorKey.cpp
51+
${EbsdLibProj_SOURCE_DIR}/Source/EbsdLib/${DIR_NAME}/GriddedColorKey.cpp
5052
)
5153
# # QT5_WRAP_CPP( EbsdLib_Generated_MOC_SRCS ${EbsdLib_Utilities_MOC_HDRS} )
5254
# set_source_files_properties( ${EbsdLib_Generated_MOC_SRCS} PROPERTIES HEADER_FILE_ONLY TRUE)

Source/Test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ set(EbsdLib_UnitTest_SRCS
5858
${EbsdLibProj_SOURCE_DIR}/Source/Test/FundamentalSectorGeometryTest.cpp
5959
${EbsdLibProj_SOURCE_DIR}/Source/Test/TSLColorKeyTest.cpp
6060
${EbsdLibProj_SOURCE_DIR}/Source/Test/NolzeHielscherColorKeyTest.cpp
61+
${EbsdLibProj_SOURCE_DIR}/Source/Test/GriddedColorKeyTest.cpp
6162
)
6263

6364

0 commit comments

Comments
 (0)