Skip to content

feat(materials): Sobel-filter normal map generator (Phase 5 slice H) - #480

Merged
fernandotonon merged 1 commit into
masterfrom
feat/phase5-slice-h-normal-map-gen
May 10, 2026
Merged

feat(materials): Sobel-filter normal map generator (Phase 5 slice H)#480
fernandotonon merged 1 commit into
masterfrom
feat/phase5-slice-h-normal-map-gen

Conversation

@fernandotonon

@fernandotonon fernandotonon commented May 10, 2026

Copy link
Copy Markdown
Owner

Summary

Phase 5 slice H — Sobel-filter normal map generator. Closes one of the two remaining Phase 5 epic acceptance items for Normal Map Generation:

Epic item Status
Generate from height/bump map (Sobel filter) ✅ this PR
Strength/intensity with real-time preview ✅ this PR
Invert R/G channels for DirectX ↔ OpenGL convention ✅ this PR
AI-assisted from diffuse texture (SD) ⏳ future slice (depends on SD enabled)

What ships

Surface What Where
Pure-data generator generate(), generateToFile() src/NormalMapGenerator.{h,cpp}
CLI qtmesh normal-from-height --src bump.png [--strength N] [--invert-r] [--invert-g/--directx] [--width N --height N] -o normal.png src/CLIPipeline.cpp
MCP tool generate_normal_map (with directx alias). SERVER_VERSION 1.6.0 → 1.7.0 src/MCPServer.cpp
GUI "Generate Normal Map…" button in Material Mode → Mode Tools qml/PropertiesPanel.qml + qml/NormalMapGeneratorDialog.qml

Design notes

  • 3×3 Sobel applied to Rec.601 luminance with edge-clamped sampling. Tangent-space normal: n = normalize(-dx*strength, -dy*strength, 1).
  • Output is RGB8 (no alpha). The Z component is always positive in tangent space, so the blue channel sits in [128..255] — matches the Ogre/glTF convention.
  • invertG flips the green channel: that's the OpenGL (+Y up, default) ↔ DirectX (+Y down) switch. MCP/CLI also accept --directx / "directx": true as an alias.
  • Strength clamped to [0, 32] so a runaway value can't produce all-saturated output.
  • Dialog is a top-level Window styled with Inspector primitives (Rectangle + Text + MouseArea over PropertiesPanelController.* colors) — same idiom as TextureChannelPackerDialog.

CLI examples

qtmesh normal-from-height --src bump.png -o normal.png
qtmesh normal-from-height --src bump.png --strength 4 --invert-g -o dx_normal.png
qtmesh normal-from-height --src bump.png --width 1024 --height 1024 -o resized.png

End-to-end smoke-tested:

  • CLI on a 2048×2048 heightmap (default, --strength 5, --invert-g, resized to 256, missing-file error). DirectX flip verified pixel-perfect: G_dx + G_gl = 255.
  • MCP via HTTP POST /api/tools/generate_normal_map: happy path, directx alias, missing source error, missing-args error — all match expected behaviour.

Test plan

  • 12 pure-data NormalMapGeneratorTest cases (flat→up-normals, H/V gradient tilts R/G, invert flags, zero strength, missing/empty source, output size override, RGB888 format, file-write round-trip, error paths)
  • 4 CLIPipelineCmdNormalFromHeight tests (missing args, missing source, flat-input correctness, --invert-g flips green)
  • 5 MCPServerTest.GenerateNormalMap* tests (missing args, flat-input write with width/height in result, missing source, tools/list registration, directx alias)
  • 5 MaterialEditorQMLTest wrapper tests (generate happy/missing-source, preview happy/missing/size-clamp)
  • Build clean on macOS arm64
  • CLI + MCP smoke tests on real 2048×2048 heightmap
  • Linux CI runs the new gtests under Xvfb

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

New Features

  • Added normal map generation from height/bump maps across the Material Editor UI, CLI (normal-from-height command), and programmatic access
  • Supports adjustable strength and channel inversion options
  • Live preview available in the UI dialog with drag-and-drop source file support
  • Version updated to 1.7.0

Review Change Stack

Generate a tangent-space normal map from a grayscale height/bump source
via a 3x3 Sobel kernel. Closes one of the two remaining Phase 5 epic
acceptance items for "Normal Map Generation":

- ✅ Generate from height/bump map (Sobel filter)
- ✅ Strength/intensity with real-time preview
- ✅ Invert R/G channels for DirectX ↔ OpenGL convention
- ⏳ AI-assisted from diffuse texture — separate slice (depends on SD)

Pure-data generator (src/NormalMapGenerator.{h,cpp}):
- GenSpec(sourcePath, strength, outputWidth/Height, invertR, invertG)
- generate() returns a GenResult with QImage; generateToFile writes via
  QImageWriter (PNG/TGA/JPG/BMP).
- Sobel kernel applied to Rec.601 luminance with edge-clamped sampling.
  Tangent-space normal n = normalize(-dx*strength, -dy*strength, 1).
- Output is RGB8. invertG is the OpenGL (+Y up, default) ↔ DirectX
  (+Y down) switch. Strength clamped to [0..32] so a runaway value
  can't produce all-saturated output.

CLI (src/CLIPipeline.cpp): `qtmesh normal-from-height`
- --src, -o, --strength, --invert-r, --invert-g (--directx alias),
  --width, --height.

MCP (src/MCPServer.cpp): `generate_normal_map` tool with full schema,
plus `directx` alias for `invert_g`. SERVER_VERSION bumped 1.6.0→1.7.0.

MaterialEditorQML wrappers:
- previewNormalMap(...) returns "data:image/png;base64,..." for live
  preview thumbnails (capped 32..512 px).
- generateNormalMap(...) writes the final PNG.
- saveNormalMapDialog() native save-as.

QML dialog (qml/NormalMapGeneratorDialog.qml): top-level Window with
Inspector-styled primitives. Source field with DropArea, strength
slider (0..10), OpenGL/DirectX toggle, save-as picker, 256x256 live
preview thumbnail that updates on every input change. Wired to the
"Generate Normal Map…" button in Material Mode → Mode Tools, right
after "Pack Texture Channels…".

Tests:
- 12 pure-data NormalMapGenerator_test cases (flat→up-normals,
  H/V gradient tilts R/G, invert flags, zero strength, missing/empty
  source, output size override, RGB888 format, file-write round-trip).
- 4 CLI cmdNormalFromHeight tests (missing args, missing source,
  flat-input correctness, --invert-g flips G).
- 5 MCP generate_normal_map tests (missing args, flat-input write,
  missing source, tools/list registration, directx alias).
- 5 MaterialEditorQML wrapper tests (generate happy/missing-source,
  preview happy/missing/size-clamp).

End-to-end smoke-tested locally via CLI on a 2048x2048 height map and
via HTTP MCP (POST /api/tools/generate_normal_map). DirectX flip
verified pixel-perfect: G_dx + G_gl = 255.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 10, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This pull request introduces a complete Normal Map Generator feature for converting grayscale height/bump maps to tangent-space normal maps using a Sobel filter. The feature is exposed across three interfaces: a QML Material Editor UI dialog with live preview, a CLI subcommand for headless workflows, and an MCP server tool for remote generation. The implementation includes core Sobel math, all UI and integration wiring, comprehensive test coverage across 25+ test cases, and updated documentation.

Changes

Normal Map Generator Feature

Layer / File(s) Summary
Data Contracts & API
src/NormalMapGenerator.h
GenSpec input struct (source path, strength, optional dimensions, R/G inversion flags) and GenResult output struct (ok status, error string, RGB8 image, used dimensions). Two public functions: in-memory generate() and generateToFile() for disk output.
Core Sobel Implementation
src/NormalMapGenerator.cpp
Reads source image as RGBA8888, optionally resizes to specified output dimensions, computes per-pixel Sobel gradients from luminance samples, normalizes/inverts normals based on spec flags, encodes to 8-bit RGB. generateToFile wraps generate and writes PNG via QImageWriter.
Core Generator Tests
src/NormalMapGenerator_test.cpp
13+ test cases validating flat/gradient height inputs, channel inversion flags, strength scaling, dimension overrides, output format (RGB888), file I/O, and error handling. Includes PNG generation helper utilities.
QML Dialog UI
qml/NormalMapGeneratorDialog.qml
Window exposing sourcePath, strength, invertR/invertG, outputPath, previewDataUrl properties. Reusable inline components (button, label, text field, checkbox, strength slider). Form layout with source drop-target plus browse button, strength slider, inversion toggles, output path with save dialog, status label, and Generate/Close buttons. Right pane displays live preview thumbnail.
UI Backend (MaterialEditorQML)
src/MaterialEditorQML.h, src/MaterialEditorQML.cpp
Three QML-invokable methods: saveNormalMapDialog (native file picker), previewNormalMap (returns base64 PNG data URI), generateNormalMap (writes file, returns error string).
Material Editor Wiring
qml/PropertiesPanel.qml, qml/qmldir, src/qml_resources.qrc
PropertiesPanel adds "Generate Normal Map..." button triggering loader-based dialog activation. Dialog registered in qmldir module and added to QML resource collection.
UI Backend Tests
src/MaterialEditorQML_test.cpp
6 test cases covering generateNormalMap (flat input, error handling) and previewNormalMap (PNG encoding, size clamping, missing source). Includes PNG helper.
CLI Subcommand Integration
src/CLIPipeline.h, src/CLIPipeline.cpp, src/main.cpp
New cmdNormalFromHeight handler parsing --src, --strength, --invert-r, --invert-g/--directx, optional --width/--height, -o/--output. Validates required args, calls generateToFile, prints dimensions or error. Integrated into run() dispatch and main.cpp CLI detection.
CLI Subcommand Tests
src/CLIPipeline_test.cpp
5 test cases for argument validation, missing file errors, flat/gradient input generation, and channel inversion behavior. Includes PNG helper.
MCP Server Tool Integration
src/MCPServer.h, src/MCPServer.cpp
Registers generate_normal_map tool with JSON schema (required: source, output; optional: strength, width, height, invert_r, invert_g). Handler maps args including directx→invert_g alias, validates, calls generateToFile, returns result JSON. SERVER_VERSION bumped 1.6.0 → 1.7.0.
MCP Server Tool Tests
src/MCPServer_test.cpp
Test coverage for tool registration, argument validation, flat-height PNG generation with assertions, missing source handling, and directx alias behavior.
Build Configuration
src/CMakeLists.txt, tests/CMakeLists.txt
NormalMapGenerator.cpp/.h added to project SRC_FILES and HEADER_FILES. NormalMapGenerator.cpp added to TEST_SRC_FILES for test executable linking.
Documentation
CLAUDE.md
Added CLI usage examples (default, --strength, inversion). Documented NormalMapGenerator feature (Sobel-based tangent-space generation, strength/inversion behavior, format support, UI/CLI/MCP touchpoints). Updated CLI mode detection and subcommand lists.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • fernandotonon/QtMeshEditor#479: Also extends MaterialEditorQML with live-preview generator methods (previewPackedTextureChannels) in similar architectural pattern to the new previewNormalMap/generateNormalMap additions.
  • fernandotonon/QtMeshEditor#394: Both PRs add QML-invokable material workflow methods and UI tooling to MaterialEditorQML, touching the same class and integration patterns.

Poem

🐰 A height map hops across the screen,
With Sobel's grace, a blur between—
From gray to normal, up so bright,
Red and green dance left and right.
Three paths converge: QML, CLI, wire,
To paint the bumps that eyes desire!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main feature added in the changeset: a Sobel-filter normal map generator for Phase 5 slice H, which is implemented across CLI, MCP, GUI, and pure-data components.
Description check ✅ Passed The pull request description is comprehensive and well-structured, covering summary, technical details, design notes, CLI examples, and test plan. However, it does not strictly follow the provided template structure (Summary and Technical Details sections with ✨ Features and 🐛 Bugfixes subsections).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/phase5-slice-h-normal-map-gen

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ca2d6f83ca

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +37 to +38
const s = url.toString()
return s.startsWith("file://") ? s.substring(7) : s

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Decode dropped file URLs before using as paths

The drop handler converts a QUrl to a path via toString() + substring(7), which leaves URL encoding intact (for example %20) and can produce malformed local paths on Windows/UNC forms. In those cases, preview and generation fail even though the user dropped a valid file. This should use a local-file conversion (for example toLocalFile() or equivalent decoding) so drag-and-drop works for paths with spaces, non-ASCII characters, and platform-specific file URL shapes.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (3)
src/MCPServer_test.cpp (2)

5906-6002: ⚡ Quick win

Test coverage could include width/height resize and invert_r flag.

The current tests cover the core functionality, but the PR description mentions additional parameters (width, height, invertR) that aren't exercised. Consider adding tests for:

  • Custom output dimensions (e.g., 64×64 source → 128×128 output)
  • The invert_r flag
  • Strength bounds (PR says clamped to [0, 32])

These would strengthen confidence that the full feature set works as specified.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/MCPServer_test.cpp` around lines 5906 - 6002, Add unit tests in
MCPServer_test that exercise the generate_normal_map tool's width/height
resizing, invert_r behavior, and strength clamping: create tests similar to
GenerateNormalMap_FlatHeightWritesPng that use writeGreyPngForMcp (or construct
a ramp image) and call server->callTool("generate_normal_map") with explicit
"width"/"height" to verify output dimensions (e.g., 64→128), with "invert_r" set
to true to assert the red channel is inverted, and with out-of-range "strength"
values to assert the implementation clamps them into [0,32]; name the new tests
clearly (e.g., GenerateNormalMap_ResizesOutput, GenerateNormalMap_InvertR,
GenerateNormalMap_StrengthClamped) and follow the same ASSERT/EXPECT patterns
used in existing tests to validate image pixels and error handling.

5977-6002: 💤 Low value

Consider adding explanatory comment for the DirectX test expectations.

The test correctly verifies that directx=true inverts the green channel, but the expectation at line 6001 (EXPECT_GT(qGreen(...), 135)) would benefit from a brief comment explaining why green should be >128 after inversion for a vertical ramp.

💡 Suggested clarification
+    // Vertical ramp: Sobel dy produces green <128 (OpenGL +Y up).
+    // With directx=true (invert_g), green should flip to >128.
     EXPECT_GT(qGreen(gen.pixel(8, 8)), 135);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/MCPServer_test.cpp` around lines 5977 - 6002, In the test
GenerateNormalMap_DirectxAliasInvertsGreen, add a short explanatory comment
before the assertion that checks qGreen(gen.pixel(8, 8)) > 135 stating that the
input vertical ramp produced green values <128 and setting args["directx"] =
true (alias for invert_g) causes the green channel to be inverted, so the
sampled pixel should be above 128 (hence the >135 threshold); place this comment
immediately above the EXPECT_GT(...) assertion to clarify the expectation.
src/CLIPipeline_test.cpp (1)

3103-3122: ⚡ Quick win

Add a dedicated --directx alias test

The CLI contract includes --invert-g/--directx; this suite validates --invert-g only. A small alias test would lock compatibility and prevent regressions in argument parsing.

Suggested test addition
+TEST(CLIPipelineCmdNormalFromHeight, DirectxAliasAlsoFlipsGreenChannel)
+{
+    QTemporaryDir tmp;
+    ASSERT_TRUE(tmp.isValid());
+    const QByteArray src = writeGreyPng(tmp, "ramp_y.png", 16, 16,
+                                         [](int, int y){ return std::min(255, y * 16); }).toUtf8();
+    const QByteArray outPath = tmp.filePath("normal_dx_alias.png").toUtf8();
+    TestArgv args({"qtmesh", "normal-from-height",
+                   "--src", src.constData(),
+                   "--strength", "2.0",
+                   "--directx",
+                   "-o", outPath.constData()});
+    EXPECT_EQ(CLIPipeline::cmdNormalFromHeight(args.argc(), args.argv()), 0);
+
+    QImage img(outPath);
+    ASSERT_FALSE(img.isNull());
+    EXPECT_GT(qGreen(img.pixel(8, 8)), 135);
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/CLIPipeline_test.cpp` around lines 3103 - 3122, Add a new unit test
mirroring TEST(CLIPipelineCmdNormalFromHeight, InvertGFlipsGreenChannel) that
verifies the CLI alias --directx behaves identically to --invert-g: construct
the same temporary ramp PNG, invoke CLIPipeline::cmdNormalFromHeight with args
containing "--directx" instead of "--invert-g" (using the same TestArgv pattern
and output path), assert the command returns 0, load the output QImage and
assert qGreen(img.pixel(8,8)) is greater than the same threshold (e.g., 135);
name the test something like TEST(CLIPipelineCmdNormalFromHeight,
DirectXAliasInvertsGreen) so it clearly ties to the original test and prevents
regressions in argument parsing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/CLIPipeline.cpp`:
- Around line 2705-2710: Replace the single "cli.normal-from-height" breadcrumb
with three breadcrumbs to reflect the I/O operations: keep the existing
SentryReporter::addBreadcrumb("cli.normal-from-height", ...) but add
SentryReporter::addBreadcrumb("file.import", ...) just before reading the source
(use spec.sourcePath / QFileInfo(spec.sourcePath).fileName() in the message) and
add SentryReporter::addBreadcrumb("file.export", ...) just before calling
NormalMapGenerator::generateToFile(spec, outputPath) (use outputPath /
QFileInfo(outputPath).fileName() in the message) so telemetry records both the
import and export around the NormalMapGenerator::generateToFile call.
- Around line 2696-2701: The help/usage text for the normal-from-height
subcommand is missing the new --directx alias and the global help listing
doesn't include the normal-from-height command; update the usage output in the
cmdNormalFromHeight handling (the block that prints "Usage: qtmesh
normal-from-height ...") to include the --directx option alongside --invert-g,
and add an entry for "normal-from-height" in the global printUsage() command
list so the subcommand is shown when users run --help; locate the CLI usage
strings in CLIPipeline.cpp (the cmdNormalFromHeight message) and the
printUsage() function and add the corresponding text entries.

In `@src/MaterialEditorQML_test.cpp`:
- Around line 2695-2702: The helper writeGreyPngForNormal currently ignores the
return value of img.save(...) which can hide fixture creation failures; modify
writeGreyPngForNormal to capture the bool result of img.save(path, "PNG") and
assert or fatal out if it returns false (e.g., Q_ASSERT(ok) or qFatal with a
message including the path and dimensions) so the test fails early and with a
clear message when PNG creation fails.

In `@src/MaterialEditorQML.cpp`:
- Around line 2924-2946: The saveNormalMapDialog function opens a user-facing
save dialog but doesn't record a breadcrumb; add a SentryReporter::addBreadcrumb
call before invoking QFileDialog in MaterialEditorQML::saveNormalMapDialog using
the 'file.export' category and a clear message like "Open Save Normal Map
dialog" so the action is tracked (place it immediately before the
QFileDialog::getSaveFileName call).
- Around line 2974-2990: In generateNormalMap, keep the existing
SentryReporter::addBreadcrumb("ui.action", "Generate normal map") but also
record the disk write by adding a SentryReporter::addBreadcrumb("file.export",
...) around the NormalMapGenerator::generateToFile call (use a descriptive
message like "Export normal map" and include the outputPath or other context),
so that NormalMapGenerator::generateToFile's I/O is tracked in addition to the
UI action.

In `@src/MCPServer.cpp`:
- Around line 3413-3416: The current logic treats "directx" as only setting
invertG when true and ignores conflicts; instead treat "directx" as a strict
alias of "invert_g": if args.contains("directx") set spec.invertG =
args.value("directx").toBool() (before/overriding any invert_g), and if both
"directx" and "invert_g" are present and their boolean values differ, either
reject the input or log an explicit conflict error; update the block that
references args.contains("directx"), args.value("directx"),
args.contains("invert_g"), and args.value("invert_g") accordingly.
- Around line 3401-3423: The code calls
SentryReporter::addBreadcrumb("ai.tool_call", "generate_normal_map") but does
not record the file I/O operations; add SentryReporter::addBreadcrumb calls
before reading the source and after writing the output: emit
SentryReporter::addBreadcrumb("file.import", spec.sourcePath) (or a descriptive
message) immediately after spec.sourcePath is set/validated and before calling
NormalMapGenerator::generateToFile, and emit
SentryReporter::addBreadcrumb("file.export", outPath) after generation succeeds
(i.e., after NormalMapGenerator::generateToFile returns ok) so both the input
and output paths are tracked alongside the existing ai.tool_call breadcrumb.

In `@src/NormalMapGenerator_test.cpp`:
- Around line 1-7: The test file is missing an explicit include of <algorithm>,
causing uses of std::min in NormalMapGenerator_test.cpp to rely on transitive
includes; add `#include` <algorithm> near the other includes at the top of the
file so std::min (used in the test cases) is declared portably and builds on
stricter toolchains.

---

Nitpick comments:
In `@src/CLIPipeline_test.cpp`:
- Around line 3103-3122: Add a new unit test mirroring
TEST(CLIPipelineCmdNormalFromHeight, InvertGFlipsGreenChannel) that verifies the
CLI alias --directx behaves identically to --invert-g: construct the same
temporary ramp PNG, invoke CLIPipeline::cmdNormalFromHeight with args containing
"--directx" instead of "--invert-g" (using the same TestArgv pattern and output
path), assert the command returns 0, load the output QImage and assert
qGreen(img.pixel(8,8)) is greater than the same threshold (e.g., 135); name the
test something like TEST(CLIPipelineCmdNormalFromHeight,
DirectXAliasInvertsGreen) so it clearly ties to the original test and prevents
regressions in argument parsing.

In `@src/MCPServer_test.cpp`:
- Around line 5906-6002: Add unit tests in MCPServer_test that exercise the
generate_normal_map tool's width/height resizing, invert_r behavior, and
strength clamping: create tests similar to GenerateNormalMap_FlatHeightWritesPng
that use writeGreyPngForMcp (or construct a ramp image) and call
server->callTool("generate_normal_map") with explicit "width"/"height" to verify
output dimensions (e.g., 64→128), with "invert_r" set to true to assert the red
channel is inverted, and with out-of-range "strength" values to assert the
implementation clamps them into [0,32]; name the new tests clearly (e.g.,
GenerateNormalMap_ResizesOutput, GenerateNormalMap_InvertR,
GenerateNormalMap_StrengthClamped) and follow the same ASSERT/EXPECT patterns
used in existing tests to validate image pixels and error handling.
- Around line 5977-6002: In the test GenerateNormalMap_DirectxAliasInvertsGreen,
add a short explanatory comment before the assertion that checks
qGreen(gen.pixel(8, 8)) > 135 stating that the input vertical ramp produced
green values <128 and setting args["directx"] = true (alias for invert_g) causes
the green channel to be inverted, so the sampled pixel should be above 128
(hence the >135 threshold); place this comment immediately above the
EXPECT_GT(...) assertion to clarify the expectation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 156f9578-8f37-4e83-a9ea-7eda4cadf564

📥 Commits

Reviewing files that changed from the base of the PR and between 97038c2 and ca2d6f8.

📒 Files selected for processing (20)
  • CLAUDE.md
  • qml/NormalMapGeneratorDialog.qml
  • qml/PropertiesPanel.qml
  • qml/qmldir
  • src/CLIPipeline.cpp
  • src/CLIPipeline.h
  • src/CLIPipeline_test.cpp
  • src/CMakeLists.txt
  • src/MCPServer.cpp
  • src/MCPServer.h
  • src/MCPServer_test.cpp
  • src/MaterialEditorQML.cpp
  • src/MaterialEditorQML.h
  • src/MaterialEditorQML_test.cpp
  • src/NormalMapGenerator.cpp
  • src/NormalMapGenerator.h
  • src/NormalMapGenerator_test.cpp
  • src/main.cpp
  • src/qml_resources.qrc
  • tests/CMakeLists.txt

Comment thread src/CLIPipeline.cpp
Comment on lines +2696 to +2701
if (spec.sourcePath.isEmpty() || outputPath.isEmpty()) {
err() << "Error: missing --src or -o." << Qt::endl;
err() << "Usage: qtmesh normal-from-height --src <height.png>" << Qt::endl;
err() << " [--strength N] [--invert-r] [--invert-g]" << Qt::endl;
err() << " [--width N --height N]" << Qt::endl;
err() << " -o <normal.png>" << Qt::endl;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Document the new CLI alias and command in help output

cmdNormalFromHeight accepts --directx, but the usage text here only advertises --invert-g, and global printUsage() also needs the new normal-from-height command so users can discover it via --help.

Suggested patch
diff --git a/src/CLIPipeline.cpp b/src/CLIPipeline.cpp
@@
-        err() << "Usage: qtmesh normal-from-height --src <height.png>" << Qt::endl;
-        err() << "                                 [--strength N] [--invert-r] [--invert-g]" << Qt::endl;
+        err() << "Usage: qtmesh normal-from-height --src <height.png>" << Qt::endl;
+        err() << "                                 [--strength N] [--invert-r] [--invert-g|--directx]" << Qt::endl;
         err() << "                                 [--width N --height N]" << Qt::endl;
         err() << "                                 -o <normal.png>" << Qt::endl;
@@
         "  material --list-presets         List the built-in preset names\n"
+        "  normal-from-height --src <img> -o <out>\n"
+        "                                  Generate tangent-space normal map (Sobel)\n"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/CLIPipeline.cpp` around lines 2696 - 2701, The help/usage text for the
normal-from-height subcommand is missing the new --directx alias and the global
help listing doesn't include the normal-from-height command; update the usage
output in the cmdNormalFromHeight handling (the block that prints "Usage: qtmesh
normal-from-height ...") to include the --directx option alongside --invert-g,
and add an entry for "normal-from-height" in the global printUsage() command
list so the subcommand is shown when users run --help; locate the CLI usage
strings in CLIPipeline.cpp (the cmdNormalFromHeight message) and the
printUsage() function and add the corresponding text entries.

Comment thread src/CLIPipeline.cpp
Comment on lines +2705 to +2710
SentryReporter::addBreadcrumb("cli.normal-from-height",
QString("Normal map from %1 -> %2")
.arg(QFileInfo(spec.sourcePath).fileName(),
QFileInfo(outputPath).fileName()));

auto r = NormalMapGenerator::generateToFile(spec, outputPath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use file.import / file.export breadcrumb categories for this I/O operation

This path performs a source image read + output file write but only logs cli.normal-from-height. Add explicit file.import and file.export breadcrumbs to align with telemetry conventions.

Suggested patch
-    SentryReporter::addBreadcrumb("cli.normal-from-height",
-        QString("Normal map from %1 -> %2")
-            .arg(QFileInfo(spec.sourcePath).fileName(),
-                 QFileInfo(outputPath).fileName()));
+    SentryReporter::addBreadcrumb("file.import",
+        QString("Read height map %1").arg(QFileInfo(spec.sourcePath).absoluteFilePath()));
+    SentryReporter::addBreadcrumb("file.export",
+        QString("Write normal map %1").arg(QFileInfo(outputPath).absoluteFilePath()));

As per coding guidelines **/*.cpp: Track all user-facing actions and significant operations with SentryReporter::addBreadcrumb(category, message). Use 'ui.action' for toolbar/menu clicks, 'ai.tool_call' for MCP tool invocations, 'file.import'/'file.export' for I/O operations.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
SentryReporter::addBreadcrumb("cli.normal-from-height",
QString("Normal map from %1 -> %2")
.arg(QFileInfo(spec.sourcePath).fileName(),
QFileInfo(outputPath).fileName()));
auto r = NormalMapGenerator::generateToFile(spec, outputPath);
SentryReporter::addBreadcrumb("file.import",
QString("Read height map %1").arg(QFileInfo(spec.sourcePath).absoluteFilePath()));
SentryReporter::addBreadcrumb("file.export",
QString("Write normal map %1").arg(QFileInfo(outputPath).absoluteFilePath()));
auto r = NormalMapGenerator::generateToFile(spec, outputPath);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/CLIPipeline.cpp` around lines 2705 - 2710, Replace the single
"cli.normal-from-height" breadcrumb with three breadcrumbs to reflect the I/O
operations: keep the existing
SentryReporter::addBreadcrumb("cli.normal-from-height", ...) but add
SentryReporter::addBreadcrumb("file.import", ...) just before reading the source
(use spec.sourcePath / QFileInfo(spec.sourcePath).fileName() in the message) and
add SentryReporter::addBreadcrumb("file.export", ...) just before calling
NormalMapGenerator::generateToFile(spec, outputPath) (use outputPath /
QFileInfo(outputPath).fileName() in the message) so telemetry records both the
import and export around the NormalMapGenerator::generateToFile call.

Comment on lines +2695 to +2702
QString writeGreyPngForNormal(const QTemporaryDir& dir, const QString& name,
int w, int h, int grey)
{
QImage img(w, h, QImage::Format_RGBA8888);
img.fill(qRgba(grey, grey, grey, 255));
const QString path = dir.filePath(name);
img.save(path, "PNG");
return path;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert test fixture PNG creation succeeds.

Line 2701 ignores img.save(...) result. If fixture creation fails, failures surface later as less-informative generator errors.

Proposed patch
 QString writeGreyPngForNormal(const QTemporaryDir& dir, const QString& name,
                                int w, int h, int grey)
 {
     QImage img(w, h, QImage::Format_RGBA8888);
     img.fill(qRgba(grey, grey, grey, 255));
     const QString path = dir.filePath(name);
-    img.save(path, "PNG");
+    EXPECT_TRUE(img.save(path, "PNG")) << path.toStdString();
     return path;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/MaterialEditorQML_test.cpp` around lines 2695 - 2702, The helper
writeGreyPngForNormal currently ignores the return value of img.save(...) which
can hide fixture creation failures; modify writeGreyPngForNormal to capture the
bool result of img.save(path, "PNG") and assert or fatal out if it returns false
(e.g., Q_ASSERT(ok) or qFatal with a message including the path and dimensions)
so the test fails early and with a clear message when PNG creation fails.

Comment thread src/MaterialEditorQML.cpp
Comment on lines +2924 to +2946
QString MaterialEditorQML::saveNormalMapDialog()
{
QString texturesPath = "./media/materials/textures";
QDir texturesDir(texturesPath);
QString startDir = texturesDir.exists() ? texturesDir.absolutePath() : QDir::currentPath();

QApplication::processEvents();
if (QWidget *activeWin = QApplication::activeWindow()) {
activeWin->raise();
activeWin->activateWindow();
}
QApplication::processEvents();

QString selectedFile = QFileDialog::getSaveFileName(
QApplication::activeWindow(),
"Save Normal Map",
startDir + "/normal.png",
"PNG (*.png);;TGA (*.tga);;JPEG (*.jpg *.jpeg);;BMP (*.bmp)",
nullptr,
QFileDialog::DontUseNativeDialog | QFileDialog::DontUseCustomDirectoryIcons
);
return selectedFile;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add breadcrumb for opening the normal-map save dialog.

Line 2937 triggers a user-facing save workflow, but no breadcrumb is recorded for that action.

Proposed patch
 QString MaterialEditorQML::saveNormalMapDialog()
 {
+    SentryReporter::addBreadcrumb("ui.action", "Open normal map save dialog");
     QString texturesPath = "./media/materials/textures";

As per coding guidelines, "**/*.cpp: Track all user-facing actions and significant operations with SentryReporter::addBreadcrumb(category, message). Use 'ui.action' for toolbar/menu clicks, 'ai.tool_call' for MCP tool invocations, 'file.import'/'file.export' for I/O operations".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/MaterialEditorQML.cpp` around lines 2924 - 2946, The saveNormalMapDialog
function opens a user-facing save dialog but doesn't record a breadcrumb; add a
SentryReporter::addBreadcrumb call before invoking QFileDialog in
MaterialEditorQML::saveNormalMapDialog using the 'file.export' category and a
clear message like "Open Save Normal Map dialog" so the action is tracked (place
it immediately before the QFileDialog::getSaveFileName call).

Comment thread src/MaterialEditorQML.cpp
Comment on lines +2974 to +2990
QString MaterialEditorQML::generateNormalMap(const QString& sourcePath,
double strength,
bool invertR,
bool invertG,
const QString& outputPath)
{
SentryReporter::addBreadcrumb("ui.action", "Generate normal map");

NormalMapGenerator::GenSpec spec;
spec.sourcePath = sourcePath;
spec.strength = static_cast<float>(strength);
spec.invertR = invertR;
spec.invertG = invertG;

auto r = NormalMapGenerator::generateToFile(spec, outputPath);
return r.ok ? QString() : r.error;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Record file-export breadcrumb for normal map generation output.

Line 2988 writes to disk (generateToFile), but only a "ui.action" breadcrumb is emitted. Add a "file.export" breadcrumb for the I/O operation.

Proposed patch
 QString MaterialEditorQML::generateNormalMap(const QString& sourcePath,
                                               double strength,
                                               bool invertR,
                                               bool invertG,
                                               const QString& outputPath)
 {
     SentryReporter::addBreadcrumb("ui.action", "Generate normal map");
+    SentryReporter::addBreadcrumb("file.export", QString("Export normal map: %1").arg(outputPath));

As per coding guidelines, "**/*.cpp: Track all user-facing actions and significant operations with SentryReporter::addBreadcrumb(category, message). Use 'ui.action' for toolbar/menu clicks, 'ai.tool_call' for MCP tool invocations, 'file.import'/'file.export' for I/O operations".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
QString MaterialEditorQML::generateNormalMap(const QString& sourcePath,
double strength,
bool invertR,
bool invertG,
const QString& outputPath)
{
SentryReporter::addBreadcrumb("ui.action", "Generate normal map");
NormalMapGenerator::GenSpec spec;
spec.sourcePath = sourcePath;
spec.strength = static_cast<float>(strength);
spec.invertR = invertR;
spec.invertG = invertG;
auto r = NormalMapGenerator::generateToFile(spec, outputPath);
return r.ok ? QString() : r.error;
}
QString MaterialEditorQML::generateNormalMap(const QString& sourcePath,
double strength,
bool invertR,
bool invertG,
const QString& outputPath)
{
SentryReporter::addBreadcrumb("ui.action", "Generate normal map");
SentryReporter::addBreadcrumb("file.export", QString("Export normal map: %1").arg(outputPath));
NormalMapGenerator::GenSpec spec;
spec.sourcePath = sourcePath;
spec.strength = static_cast<float>(strength);
spec.invertR = invertR;
spec.invertG = invertG;
auto r = NormalMapGenerator::generateToFile(spec, outputPath);
return r.ok ? QString() : r.error;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/MaterialEditorQML.cpp` around lines 2974 - 2990, In generateNormalMap,
keep the existing SentryReporter::addBreadcrumb("ui.action", "Generate normal
map") but also record the disk write by adding a
SentryReporter::addBreadcrumb("file.export", ...) around the
NormalMapGenerator::generateToFile call (use a descriptive message like "Export
normal map" and include the outputPath or other context), so that
NormalMapGenerator::generateToFile's I/O is tracked in addition to the UI
action.

Comment thread src/MCPServer.cpp
Comment on lines +3401 to +3423
SentryReporter::addBreadcrumb("ai.tool_call", "generate_normal_map");

NormalMapGenerator::GenSpec spec;
spec.sourcePath = args.value("source").toString();
if (args.contains("strength"))
spec.strength = static_cast<float>(args.value("strength").toDouble());
if (args.contains("width"))
spec.outputWidth = args.value("width").toInt();
if (args.contains("height"))
spec.outputHeight = args.value("height").toInt();
if (args.contains("invert_r"))
spec.invertR = args.value("invert_r").toBool();
if (args.contains("invert_g"))
spec.invertG = args.value("invert_g").toBool();
if (args.contains("directx") && args.value("directx").toBool())
spec.invertG = true; // alias for invert_g

const QString outPath = args.value("output").toString();
if (spec.sourcePath.isEmpty() || outPath.isEmpty())
return makeErrorResult("Error: missing required 'source' and 'output' arguments");

auto r = NormalMapGenerator::generateToFile(spec, outPath);
if (!r.ok)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add file I/O breadcrumbs for this tool path.

This flow performs file read/write but only emits ai.tool_call. Add file.import and file.export breadcrumbs around generation to satisfy telemetry requirements.

Proposed patch
 QJsonObject MCPServer::toolGenerateNormalMap(const QJsonObject &args)
 {
     SentryReporter::addBreadcrumb("ai.tool_call", "generate_normal_map");
@@
     const QString outPath = args.value("output").toString();
     if (spec.sourcePath.isEmpty() || outPath.isEmpty())
         return makeErrorResult("Error: missing required 'source' and 'output' arguments");
 
+    SentryReporter::addBreadcrumb("file.import",
+        QStringLiteral("Read height map: %1").arg(spec.sourcePath));
+    SentryReporter::addBreadcrumb("file.export",
+        QStringLiteral("Write normal map: %1").arg(outPath));
+
     auto r = NormalMapGenerator::generateToFile(spec, outPath);
     if (!r.ok)
         return makeErrorResult(QString("Error: %1").arg(r.error));

As per coding guidelines, "Track all user-facing actions and significant operations with SentryReporter::addBreadcrumb(category, message). Use 'ui.action' for toolbar/menu clicks, 'ai.tool_call' for MCP tool invocations, 'file.import'/'file.export' for I/O operations".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/MCPServer.cpp` around lines 3401 - 3423, The code calls
SentryReporter::addBreadcrumb("ai.tool_call", "generate_normal_map") but does
not record the file I/O operations; add SentryReporter::addBreadcrumb calls
before reading the source and after writing the output: emit
SentryReporter::addBreadcrumb("file.import", spec.sourcePath) (or a descriptive
message) immediately after spec.sourcePath is set/validated and before calling
NormalMapGenerator::generateToFile, and emit
SentryReporter::addBreadcrumb("file.export", outPath) after generation succeeds
(i.e., after NormalMapGenerator::generateToFile returns ok) so both the input
and output paths are tracked alongside the existing ai.tool_call breadcrumb.

Comment thread src/MCPServer.cpp
Comment on lines +3413 to +3416
if (args.contains("invert_g"))
spec.invertG = args.value("invert_g").toBool();
if (args.contains("directx") && args.value("directx").toBool())
spec.invertG = true; // alias for invert_g

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make directx a strict alias of invert_g.

At Line [3415], directx=false is ignored. If both fields are sent with conflicting values, output can be unintuitive. If directx is present, set spec.invertG directly from it (or reject conflicts).

Proposed patch
     if (args.contains("invert_g"))
         spec.invertG = args.value("invert_g").toBool();
-    if (args.contains("directx") && args.value("directx").toBool())
-        spec.invertG = true;  // alias for invert_g
+    if (args.contains("directx"))
+        spec.invertG = args.value("directx").toBool();  // alias for invert_g
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/MCPServer.cpp` around lines 3413 - 3416, The current logic treats
"directx" as only setting invertG when true and ignores conflicts; instead treat
"directx" as a strict alias of "invert_g": if args.contains("directx") set
spec.invertG = args.value("directx").toBool() (before/overriding any invert_g),
and if both "directx" and "invert_g" are present and their boolean values
differ, either reject the input or log an explicit conflict error; update the
block that references args.contains("directx"), args.value("directx"),
args.contains("invert_g"), and args.value("invert_g") accordingly.

Comment on lines +1 to +7
#include <gtest/gtest.h>

#include <QImage>
#include <QTemporaryDir>

#include "NormalMapGenerator.h"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify std::min usage and missing direct include in this file.
rg -n 'std::min\s*\(' src/NormalMapGenerator_test.cpp
rg -n '^#include <algorithm>$' src/NormalMapGenerator_test.cpp

Repository: fernandotonon/QtMeshEditor

Length of output: 419


🏁 Script executed:

cat -n src/NormalMapGenerator_test.cpp | head -30

Repository: fernandotonon/QtMeshEditor

Length of output: 1064


🏁 Script executed:

# Check what Qt headers and gtest might include
rg -A 5 '^#include' src/NormalMapGenerator_test.cpp | head -20

Repository: fernandotonon/QtMeshEditor

Length of output: 223


🏁 Script executed:

cat -n src/NormalMapGenerator.h | head -50

Repository: fernandotonon/QtMeshEditor

Length of output: 2121


🏁 Script executed:

# Check if NormalMapGenerator.h includes algorithm
rg '^#include.*algorithm' src/NormalMapGenerator.h

Repository: fernandotonon/QtMeshEditor

Length of output: 52


🏁 Script executed:

# Check if gtest.h or Qt headers might transitively include algorithm
rg 'std::min|#include.*algorithm' /usr/include/gtest/gtest.h 2>/dev/null | head -5

Repository: fernandotonon/QtMeshEditor

Length of output: 52


🏁 Script executed:

# Better check: Look at the full test file to understand the context
wc -l src/NormalMapGenerator_test.cpp

Repository: fernandotonon/QtMeshEditor

Length of output: 106


🏁 Script executed:

# Get the lines where std::min is used to see the full context
sed -n '55,65p; 75,85p; 92,102p; 110,120p' src/NormalMapGenerator_test.cpp

Repository: fernandotonon/QtMeshEditor

Length of output: 1652


🏁 Script executed:

# Check if other _test.cpp files explicitly include <algorithm>
fd -e '\.cpp' -e '\.h' src/ | grep -i test | xargs rg '#include.*algorithm' 2>/dev/null

Repository: fernandotonon/QtMeshEditor

Length of output: 1202


🏁 Script executed:

# Check if any other source files use std::min and how they handle it
rg 'std::min' src/ | grep -v '_test.cpp' | head -10

Repository: fernandotonon/QtMeshEditor

Length of output: 1007


🏁 Script executed:

# Verify the coding guidelines requirement for test files
echo "Checking if test files must compile portably across toolchains..."
# Look for any makefile or build config that shows compiler strictness
fd -e CMakeLists.txt -e '*.mk' | head -5

Repository: fernandotonon/QtMeshEditor

Length of output: 136


Add explicit <algorithm> include for std::min portability.

std::min is used at lines 61, 80, 97, and 115, but this file doesn't include <algorithm>. Relying on transitive includes breaks builds on stricter toolchains. Other test files in the codebase (FBXExporter_test.cpp, ScanEngine_test.cpp) explicitly include it.

Proposed patch
 `#include` <gtest/gtest.h>
 
+#include <algorithm>
 `#include` <QImage>
 `#include` <QTemporaryDir>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#include <gtest/gtest.h>
#include <QImage>
#include <QTemporaryDir>
#include "NormalMapGenerator.h"
`#include` <gtest/gtest.h>
`#include` <algorithm>
`#include` <QImage>
`#include` <QTemporaryDir>
`#include` "NormalMapGenerator.h"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/NormalMapGenerator_test.cpp` around lines 1 - 7, The test file is missing
an explicit include of <algorithm>, causing uses of std::min in
NormalMapGenerator_test.cpp to rely on transitive includes; add `#include`
<algorithm> near the other includes at the top of the file so std::min (used in
the test cases) is declared portably and builds on stricter toolchains.

@sonarqubecloud

Copy link
Copy Markdown

@fernandotonon
fernandotonon merged commit 08d5959 into master May 10, 2026
19 checks passed
@fernandotonon
fernandotonon deleted the feat/phase5-slice-h-normal-map-gen branch May 10, 2026 19:54
@coderabbitai coderabbitai Bot mentioned this pull request May 20, 2026
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant