Skip to content

Add Blender material texture normalization tool#96

Open
Ker102 wants to merge 1 commit into
mainfrom
feature/blender-material-texture-normalization
Open

Add Blender material texture normalization tool#96
Ker102 wants to merge 1 commit into
mainfrom
feature/blender-material-texture-normalization

Conversation

@Ker102

@Ker102 Ker102 commented Jun 18, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add normalize_material_texture_channels for report-only inspection and optional deterministic color-space repair of imported material texture maps.
  • Infer common PBR texture roles from node labels, image names/paths, and linked sockets.
  • Wire the tool through the addon bundles, LangChain schema, tool filtering, system prompt, material guide, inventory docs, and focused test coverage.

Validation

  • npx tsx scripts/test/test-blender-material-texture-normalization-tool.ts
  • npx tsx scripts/test/test-tool-guide-trigger-coverage.ts
  • npx tsx scripts/test/test-blender-capability-inventory.ts
  • python -m py_compile desktop/assets/vipermesh-addon.py public/downloads/vipermesh-addon.py
  • npx tsc --noEmit --incremental false
  • npm run lint (passes with existing baseline-browser-mapping age warning)
  • git diff --check (passes with Windows line-ending warnings)

Summary by CodeRabbit

  • New Features

    • Added material texture normalization tool that inspects imported materials and reports texture color-space issues. Operates in safe report-only mode by default, with an optional flag to automatically correct inferred texture channel color spaces (e.g., normal maps, base colors).
  • Documentation

    • Updated material guide with guidance on using the new normalization tool for imported assets with incorrect appearance.

@github-actions github-actions Bot added documentation Improvements or additions to documentation backend desktop scripts labels Jun 18, 2026
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a new normalize_material_texture_channels command to the Blender addon (both desktop and public copies), a LangChain agent tool wrapper, tool registry and filter entries, system prompt documentation, materials tool guide updates, a capability inventory update, and static assertion tests covering the end-to-end wiring.

Changes

normalize_material_texture_channels full-stack addition

Layer / File(s) Summary
Blender addon: dispatch wiring and PBR normalization method
desktop/assets/vipermesh-addon.py, public/downloads/vipermesh-addon.py
Both copies register "normalize_material_texture_channels" in the command handlers table and implement the method: selects materials, walks ShaderNodeTexImage nodes, infers PBR roles via name/label heuristics and graph topology (NormalMap/Displacement links, socket names), compares current vs expected color spaces (sRGB vs Non-Color), and optionally applies corrections with per-node reporting.
Agent tool wrapper, registry, filter, and system prompt
lib/ai/agents.ts, lib/orchestration/tool-registry.ts, lib/orchestration/tool-filter.ts, lib/orchestration/prompts/blender-agent-system.md
Adds the LangChain tool with Zod schema (material_names?: string[], apply_changes?: boolean) and registers it in ALL_TOOLS; adds a TOOL_REGISTRY entry; appends to CATEGORY_GROUPS.materials and broadens the materials keyword regex to include color space, normal maps, PBR channels, and image texture terms; documents the tool in the agent system prompt.
Tool guide, capability inventory, and tests
data/tool-guides/materials-guide.md, docs/blender-mcp-capability-inventory.md, scripts/test/test-blender-material-texture-normalization-tool.ts, scripts/test/test-tool-guide-trigger-coverage.ts
Updates materials-guide.md front-matter, usage guidance, and PBR map rules; bumps the capability inventory to 100 commands; adds a static content assertion test validating wiring across addon sources, registry, prompt, and guide; extends trigger-coverage test to require guide coverage for the new tool.

Sequence Diagram

sequenceDiagram
  participant Agent as BlenderAgentV2
  participant Tool as normalize_material_texture_channels (LangChain)
  participant MCP as executeMcpCommand
  participant Addon as BlenderMCPServer (vipermesh-addon.py)
  participant Blender as Blender Node Graph

  Agent->>Tool: invoke(material_names?, apply_changes?)
  Tool->>MCP: executeMcpCommand("normalize_material_texture_channels", params)
  MCP->>Addon: dispatch via handlers table
  Addon->>Blender: iterate ShaderNodeTexImage nodes per material
  Blender-->>Addon: node labels, image names, downstream links
  Addon->>Addon: infer PBR role, compare colorspace_settings vs expected
  alt apply_changes=true
    Addon->>Blender: set image.colorspace_settings.name
  end
  Addon-->>MCP: per-material report, changes list, warnings
  MCP-->>Tool: structured result
  Tool-->>Agent: report (optionally triggers inspect_material_node_graph)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Ker102/ViperMesh#41: Adds inspect_material_node_graph to the same Blender MCP command dispatch, tool registry, and agent system prompt pipeline that this PR extends with normalize_material_texture_channels.

Suggested labels

documentation, backend, desktop, scripts

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The pull request description provides a clear summary of changes, validation steps performed, and links to test coverage; however, it does not follow the required template structure with sections like Type of Change, Related Issues, and Checklist. Reorganize the description to match the repository template by adding Type of Change selection, Related Issues section, and completing the Checklist items for consistency and clarity.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically describes the main feature being added—a Blender material texture normalization tool—which aligns with the primary change across all modified files.
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 feature/blender-material-texture-normalization

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: dependency version conflict. Check your lock file or package.json.


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.

@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: 7

🤖 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 `@desktop/assets/vipermesh-addon.py`:
- Around line 6930-6937: The function returns success even when explicit
material names are requested but some are missing. After the loop where
requested_names are processed and missing materials are collected into the
missing_materials list, add a check to verify that no materials were requested
that couldn't be found. If missing_materials is not empty after processing the
requested_names loop, the function should not report success (set success to
False or raise an appropriate error) instead of returning a successful result
with materials_checked: 0. This prevents masking no-op normalization requests
when stale or typoed material names are provided.
- Around line 6842-6865: The role_aliases dictionary in the vipermesh-addon.py
file uses overly broad substring matching where the generic alias "color"
matches within more specific material names like "metallic_color" or
"normal_color", causing them to be incorrectly classified as base_color. Fix
this by either removing the overly broad "color" entry from the role_aliases
dictionary, or by modifying the matching logic that uses this dictionary to
perform token-based or word-boundary matching instead of simple substring
matching. This ensures that specific material names like "metallic_color" are
matched against their specific aliases ("metallic") rather than the generic
"color" alias.
- Around line 6907-6918: The texture role inference logic only checks immediate
destination nodes (one-hop) but needs to traverse through utility nodes like
ShaderNodeSeparateColor, ShaderNodeBump, and ColorRamp to reach the final
destination socket. Refactor the loop that iterates over node outputs and links
to recursively or iteratively follow connections through these utility nodes.
When encountering a utility node as the to_node_type, continue traversing its
outputs to find the final target node and socket instead of stopping at the
utility node. This will allow the existing checks for ShaderNodeNormalMap,
ShaderNodeDisplacement, and socket_roles to properly detect textures routed
through intermediate processing nodes.

In `@public/downloads/vipermesh-addon.py`:
- Around line 6862-6864: The AO (ambient occlusion) texture name alias mapping
dictionary is missing an entry for the common `occlusion` naming convention used
in standalone AO maps. Add a new mapping entry in the dictionary (alongside the
existing "ao", "ambientocclusion", and "ambient_occlusion" entries) that maps
"occlusion" to "ambient_occlusion". This will prevent AO maps named `occlusion`
from being classified as `unknown` and will ensure they receive the proper
`Non-Color` repair.
- Around line 6929-7018: The function returns success: True even when all
requested materials in requested_names are missing and no materials are actually
processed. Add a condition before the return statement to check if
requested_names was provided but resulted in zero materials being processed
(i.e., len(requested_names) > 0 and len(materials) == 0), and if so, set success
to False in the returned dictionary instead of True. This ensures that a no-op
due to misspelled or non-existent material names is correctly reported as a
failed operation.
- Around line 6889-6918: The infer_texture_role function checks text-based
inference (node label, image name/path) before graph link inference, which
allows broad substring matches in paths to override specific socket connections.
Reorder the logic in infer_texture_role to check the graph links first (the loop
iterating over outputs and links to find connected socket roles), and only fall
back to text-based inference on node labels and image names if no graph link
information is found. This ensures specific socket connections take precedence
over generic text pattern matching.

In `@scripts/test/test-blender-material-texture-normalization-tool.ts`:
- Around line 19-21: The test assertions are incomplete and only verify color
space mappings for base_color and normal channels, but the addon implementation
defines mappings for additional PBR channels: emission (sRGB), roughness
(Non-Color), metallic (Non-Color), ambient_occlusion (Non-Color), alpha
(Non-Color), and displacement (Non-Color). Add additional assert.match calls
after the existing ones to verify that the source contains the correct color
space mappings for each of these additional PBR channels, following the same
pattern as the existing base_color and normal assertions.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8359c380-01a5-494d-abaa-25bfe8c5fe12

📥 Commits

Reviewing files that changed from the base of the PR and between b4e639a and 04301f6.

📒 Files selected for processing (10)
  • data/tool-guides/materials-guide.md
  • desktop/assets/vipermesh-addon.py
  • docs/blender-mcp-capability-inventory.md
  • lib/ai/agents.ts
  • lib/orchestration/prompts/blender-agent-system.md
  • lib/orchestration/tool-filter.ts
  • lib/orchestration/tool-registry.ts
  • public/downloads/vipermesh-addon.py
  • scripts/test/test-blender-material-texture-normalization-tool.ts
  • scripts/test/test-tool-guide-trigger-coverage.ts

Comment on lines +6842 to +6865
role_aliases = {
"albedo": "base_color",
"diffuse": "base_color",
"basecolor": "base_color",
"base_color": "base_color",
"color": "base_color",
"emissive": "emission",
"emission": "emission",
"rough": "roughness",
"roughness": "roughness",
"metal": "metallic",
"metalness": "metallic",
"metallic": "metallic",
"normal": "normal",
"normalmap": "normal",
"normal_map": "normal",
"height": "displacement",
"displacement": "displacement",
"alpha": "alpha",
"opacity": "alpha",
"ao": "ambient_occlusion",
"ambientocclusion": "ambient_occlusion",
"ambient_occlusion": "ambient_occlusion",
}

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

Match PBR aliases by token/specificity, not broad substrings.

Line 6892 lets "color" match before later data roles, so names like metallic_color.png or normal_color.png are classified as base_color and can be changed to sRGB.

🐛 Proposed fix
             role_aliases = {
                 "albedo": "base_color",
                 "diffuse": "base_color",
                 "basecolor": "base_color",
                 "base_color": "base_color",
                 "color": "base_color",
+                "base_colour": "base_color",
+                "colour": "base_color",
                 "emissive": "emission",
                 "emission": "emission",
                 "rough": "roughness",
                 "roughness": "roughness",
                 "metal": "metallic",
                 "metalness": "metallic",
                 "metallic": "metallic",
                 "normal": "normal",
                 "normalmap": "normal",
                 "normal_map": "normal",
+                "nrm": "normal",
+                "nor": "normal",
                 "height": "displacement",
                 "displacement": "displacement",
+                "bump": "displacement",
                 "alpha": "alpha",
                 "opacity": "alpha",
                 "ao": "ambient_occlusion",
+                "occlusion": "ambient_occlusion",
                 "ambientocclusion": "ambient_occlusion",
                 "ambient_occlusion": "ambient_occlusion",
+                "arm": "ambient_occlusion",
+                "orm": "ambient_occlusion",
             }
@@
             def infer_from_text(*values):
                 joined = "_".join(normalize_token(value) for value in values if value)
-                for alias, role in role_aliases.items():
-                    if alias in joined:
+                padded = f"_{joined}_"
+                compact = joined.replace("_", "")
+                for alias in sorted(role_aliases, key=len, reverse=True):
+                    alias_compact = alias.replace("_", "")
+                    if f"_{alias}_" in padded or compact == alias_compact:
+                        role = role_aliases[alias]
                         return role
                 return None

Also applies to: 6889-6893

🤖 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 `@desktop/assets/vipermesh-addon.py` around lines 6842 - 6865, The role_aliases
dictionary in the vipermesh-addon.py file uses overly broad substring matching
where the generic alias "color" matches within more specific material names like
"metallic_color" or "normal_color", causing them to be incorrectly classified as
base_color. Fix this by either removing the overly broad "color" entry from the
role_aliases dictionary, or by modifying the matching logic that uses this
dictionary to perform token-based or word-boundary matching instead of simple
substring matching. This ensures that specific material names like
"metallic_color" are matched against their specific aliases ("metallic") rather
than the generic "color" alias.

Source: MCP tools

Comment on lines +6907 to +6918
for output in getattr(node, "outputs", []) or []:
for link in getattr(output, "links", []) or []:
to_node = getattr(link, "to_node", None)
to_socket = getattr(link, "to_socket", None)
to_socket_name = str(getattr(to_socket, "name", "")).strip().lower()
to_node_type = str(getattr(to_node, "bl_idname", ""))
if to_node_type == "ShaderNodeNormalMap":
return "normal", "normal_map_node"
if to_node_type == "ShaderNodeDisplacement":
return "displacement", "displacement_node"
if to_socket_name in socket_roles:
return socket_roles[to_socket_name], "linked_socket"

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Locate current material graph patterns where texture nodes route through utility nodes.
# Expected: Any SeparateColor/Bump/Mix routes should be covered by normalize_material_texture_channels inference.

rg -n -C 5 'ShaderNodeSeparateColor|ShaderNodeSeparateRGB|ShaderNodeBump|texture_nodes\[[' desktop/assets/vipermesh-addon.py public/downloads/vipermesh-addon.py

Repository: Ker102/ViperMesh

Length of output: 275


🏁 Script executed:

#!/bin/bash
# Fixed script: search for utility nodes and material construction patterns

# First, let's check the exact code at lines 6907-6918
echo "=== Code at target lines ==="
sed -n '6900,6925p' desktop/assets/vipermesh-addon.py

# Now search for ShaderNode utility patterns (fixed regex)
echo -e "\n=== Searching for utility node usage ==="
rg -n 'ShaderNodeSeparateColor|ShaderNodeSeparateRGB|ShaderNodeBump|ShaderNodeColorRamp' desktop/assets/vipermesh-addon.py | head -20

# Check material construction context
echo -e "\n=== Searching for texture node and material construction ==="
rg -n 'normalize_material_texture_channels|def.*normalize' desktop/assets/vipermesh-addon.py | head -10

Repository: Ker102/ViperMesh

Length of output: 2218


🏁 Script executed:

#!/bin/bash
# Get more context on SeparateColor usage at line 8842
echo "=== Context around SeparateColor usage (line 8842) ==="
sed -n '8835,8860p' desktop/assets/vipermesh-addon.py

# Get the function definition and first 100 lines
echo -e "\n=== normalize_material_texture_channels function start ==="
sed -n '6829,6900p' desktop/assets/vipermesh-addon.py

# Check if there's a loop/traversal pattern elsewhere for utility nodes
echo -e "\n=== Search for queue or traversal patterns ==="
rg -n 'queue|traverse|depth|while.*:' desktop/assets/vipermesh-addon.py | grep -A2 -B2 '68[0-9]{2}:\|69[0-9]{2}:' | head -20

Repository: Ker102/ViperMesh

Length of output: 4572


🏁 Script executed:

#!/bin/bash
# Need to understand the flow better - check if normalize_material_texture_channels 
# is run before or after material construction with SeparateColor

echo "=== Full infer_texture_role function ==="
sed -n '6900,6930p' desktop/assets/vipermesh-addon.py

# Check what calls normalize_material_texture_channels
echo -e "\n=== Calls to normalize_material_texture_channels ==="
rg -n 'normalize_material_texture_channels' desktop/assets/vipermesh-addon.py

# Check context around line 8842 - is this construction or import?
echo -e "\n=== Broader context around line 8842 ==="
sed -n '8800,8850p' desktop/assets/vipermesh-addon.py | head -40

# Get function name at line 8842
echo -e "\n=== Which function contains line 8842? ==="
awk 'NR<=8842 && /^[[:space:]]*(def |class )/ {func=$0; line=NR} NR==8842 {print "Line 8842 is in function defined at line " line ": " func}' desktop/assets/vipermesh-addon.py

Repository: Ker102/ViperMesh

Length of output: 4508


🏁 Script executed:

#!/bin/bash
# Fix awk syntax and find function containing line 8842
echo "=== Finding function at line 8842 ==="
python3 << 'PYEOF'
with open('desktop/assets/vipermesh-addon.py', 'r') as f:
    lines = f.readlines()
    current_func = None
    for i, line in enumerate(lines[:8850], 1):
        if line.lstrip().startswith(('def ', 'class ')):
            current_func = (i, line.strip())
        if i == 8842:
            print(f"Line 8842 is in function/class at line {current_func[0]}: {current_func[1]}")
            break
PYEOF

# Check if ARM material construction is in normalize_material_texture_channels or elsewhere
echo -e "\n=== Check if ARM handling is in normalize_material_texture_channels ==="
sed -n '6829,7100p' desktop/assets/vipermesh-addon.py | grep -n 'arm\|ARM' | head -5

# Let's see the full extent of normalize_material_texture_channels
echo -e "\n=== Find where normalize_material_texture_channels ends ==="
python3 << 'PYEOF'
with open('desktop/assets/vipermesh-addon.py', 'r') as f:
    lines = f.readlines()
    in_func = False
    start_indent = 0
    for i, line in enumerate(lines[6828:], 6829):  # Start from line 6829
        if not in_func and 'def normalize_material_texture_channels' in line:
            in_func = True
            start_indent = len(line) - len(line.lstrip())
            continue
        if in_func:
            if line.strip() and not line.startswith(' ' * (start_indent + 1)) and line[0] != ' ':
                print(f"normalize_material_texture_channels ends before line {i}")
                break
            elif line.strip() and len(line) - len(line.lstrip()) == start_indent and line.lstrip().startswith('def '):
                print(f"normalize_material_texture_channels ends at line {i-1}")
                print(f"Next function starts at line {i}: {line.strip()[:50]}")
                break
PYEOF

Repository: Ker102/ViperMesh

Length of output: 465


🏁 Script executed:

#!/bin/bash
# Now understand what normalize_material_texture_channels actually does
echo "=== Full normalize_material_texture_channels to understand its purpose ==="
sed -n '6829,7021p' desktop/assets/vipermesh-addon.py | head -120

# Check if this function imports or processes existing materials
echo -e "\n=== Key parts of the function ==="
sed -n '6930,6980p' desktop/assets/vipermesh-addon.py

Repository: Ker102/ViperMesh

Length of output: 7684


Traverse utility nodes in texture role inference to handle packed maps routed through Separate Color or Bump nodes.

The current inference only checks immediate destination nodes. Materials—whether imported or programmatically constructed—often route ARM (Ambient Occlusion, Roughness, Metallic) or height maps through ShaderNodeSeparateColor, ShaderNodeBump, or ColorRamp nodes before connecting to Principled BSDF sockets. This breaks the one-hop link inference, leaving such textures classified as unknown and missing Non-Color colorspace corrections.

Proposed direction
-                for output in getattr(node, "outputs", []) or []:
-                    for link in getattr(output, "links", []) or []:
+                queue = [(output, 0) for output in (getattr(node, "outputs", []) or [])]
+                seen_nodes = set()
+                while queue:
+                    output, depth = queue.pop(0)
+                    for link in getattr(output, "links", []) or []:
                         to_node = getattr(link, "to_node", None)
                         to_socket = getattr(link, "to_socket", None)
                         to_socket_name = str(getattr(to_socket, "name", "")).strip().lower()
                         to_node_type = str(getattr(to_node, "bl_idname", ""))
                         if to_node_type == "ShaderNodeNormalMap":
                             return "normal", "normal_map_node"
+                        if to_node_type == "ShaderNodeBump":
+                            return "displacement", "bump_node"
                         if to_node_type == "ShaderNodeDisplacement":
                             return "displacement", "displacement_node"
                         if to_socket_name in socket_roles:
                             return socket_roles[to_socket_name], "linked_socket"
+                        if to_node and depth < 4 and to_node.name not in seen_nodes:
+                            seen_nodes.add(to_node.name)
+                            queue.extend((candidate, depth + 1) for candidate in getattr(to_node, "outputs", []) or [])
                 return None, "unknown"
🤖 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 `@desktop/assets/vipermesh-addon.py` around lines 6907 - 6918, The texture role
inference logic only checks immediate destination nodes (one-hop) but needs to
traverse through utility nodes like ShaderNodeSeparateColor, ShaderNodeBump, and
ColorRamp to reach the final destination socket. Refactor the loop that iterates
over node outputs and links to recursively or iteratively follow connections
through these utility nodes. When encountering a utility node as the
to_node_type, continue traversing its outputs to find the final target node and
socket instead of stopping at the utility node. This will allow the existing
checks for ShaderNodeNormalMap, ShaderNodeDisplacement, and socket_roles to
properly detect textures routed through intermediate processing nodes.

Source: MCP tools

Comment on lines +6930 to +6937
if requested_names:
materials = []
for name in requested_names:
material = bpy.data.materials.get(name)
if material is None:
missing_materials.append(name)
else:
materials.append(material)

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

Do not report success when explicit material names are missing.

A call with only stale/typoed material_names returns success: True and materials_checked: 0, which masks a no-op normalization request.

🐛 Proposed fix
             if requested_names:
                 materials = []
                 for name in requested_names:
                     material = bpy.data.materials.get(name)
                     if material is None:
                         missing_materials.append(name)
                     else:
                         materials.append(material)
+                if missing_materials and not materials:
+                    return {
+                        "success": False,
+                        "error": f"Material(s) not found: {', '.join(missing_materials)}",
+                        "apply_changes": apply_changes,
+                        "materials_checked": 0,
+                        "missing_materials": missing_materials,
+                        "changes": [],
+                        "warnings": [],
+                        "reports": [],
+                    }
@@
             return {
-                "success": True,
+                "success": len(missing_materials) == 0,
                 "apply_changes": apply_changes,
                 "materials_checked": len(reports),
                 "missing_materials": missing_materials,

Also applies to: 7009-7014

🤖 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 `@desktop/assets/vipermesh-addon.py` around lines 6930 - 6937, The function
returns success even when explicit material names are requested but some are
missing. After the loop where requested_names are processed and missing
materials are collected into the missing_materials list, add a check to verify
that no materials were requested that couldn't be found. If missing_materials is
not empty after processing the requested_names loop, the function should not
report success (set success to False or raise an appropriate error) instead of
returning a successful result with materials_checked: 0. This prevents masking
no-op normalization requests when stale or typoed material names are provided.

Comment on lines +6862 to +6864
"ao": "ambient_occlusion",
"ambientocclusion": "ambient_occlusion",
"ambient_occlusion": "ambient_occlusion",

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 the common occlusion AO alias.

Standalone AO maps are often named occlusion; because there is no direct Principled AO socket, these maps can remain unknown and skip the intended Non-Color repair.

Suggested fix
                 "ao": "ambient_occlusion",
+                "occlusion": "ambient_occlusion",
                 "ambientocclusion": "ambient_occlusion",
                 "ambient_occlusion": "ambient_occlusion",
📝 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
"ao": "ambient_occlusion",
"ambientocclusion": "ambient_occlusion",
"ambient_occlusion": "ambient_occlusion",
"ao": "ambient_occlusion",
"occlusion": "ambient_occlusion",
"ambientocclusion": "ambient_occlusion",
"ambient_occlusion": "ambient_occlusion",
🤖 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 `@public/downloads/vipermesh-addon.py` around lines 6862 - 6864, The AO
(ambient occlusion) texture name alias mapping dictionary is missing an entry
for the common `occlusion` naming convention used in standalone AO maps. Add a
new mapping entry in the dictionary (alongside the existing "ao",
"ambientocclusion", and "ambient_occlusion" entries) that maps "occlusion" to
"ambient_occlusion". This will prevent AO maps named `occlusion` from being
classified as `unknown` and will ensure they receive the proper `Non-Color`
repair.

Source: MCP tools

Comment on lines +6889 to +6918
def infer_from_text(*values):
joined = "_".join(normalize_token(value) for value in values if value)
for alias, role in role_aliases.items():
if alias in joined:
return role
return None

def infer_texture_role(node):
role = infer_from_text(getattr(node, "label", ""), getattr(node, "name", ""))
if role:
return role, "node_label_or_name"

image = getattr(node, "image", None)
if image:
role = infer_from_text(getattr(image, "name", ""), getattr(image, "filepath", ""))
if role:
return role, "image_name_or_path"

for output in getattr(node, "outputs", []) or []:
for link in getattr(output, "links", []) or []:
to_node = getattr(link, "to_node", None)
to_socket = getattr(link, "to_socket", None)
to_socket_name = str(getattr(to_socket, "name", "")).strip().lower()
to_node_type = str(getattr(to_node, "bl_idname", ""))
if to_node_type == "ShaderNodeNormalMap":
return "normal", "normal_map_node"
if to_node_type == "ShaderNodeDisplacement":
return "displacement", "displacement_node"
if to_socket_name in socket_roles:
return socket_roles[to_socket_name], "linked_socket"

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

Use graph links before generic text aliases.

infer_texture_role trusts label/image/path text before socket links, and infer_from_text matches broad substrings like color before roughness/normal. A data map in a path such as /color_corrected/Wood_Roughness.png can be inferred as base_color and changed to sRGB even when linked to the Roughness socket.

Suggested fix
+            def infer_from_links(node):
+                for output in getattr(node, "outputs", []) or []:
+                    for link in getattr(output, "links", []) or []:
+                        to_node = getattr(link, "to_node", None)
+                        to_socket = getattr(link, "to_socket", None)
+                        to_socket_name = str(getattr(to_socket, "name", "")).strip().lower()
+                        to_node_type = str(getattr(to_node, "bl_idname", ""))
+                        if to_node_type == "ShaderNodeNormalMap":
+                            return "normal", "normal_map_node"
+                        if to_node_type == "ShaderNodeDisplacement":
+                            return "displacement", "displacement_node"
+                        if to_socket_name in socket_roles:
+                            return socket_roles[to_socket_name], "linked_socket"
+                return None, "unknown"
+
             def infer_texture_role(node):
+                role, role_source = infer_from_links(node)
+                if role:
+                    return role, role_source
+
                 role = infer_from_text(getattr(node, "label", ""), getattr(node, "name", ""))
                 if role:
                     return role, "node_label_or_name"
 
                 image = getattr(node, "image", None)
                 if image:
                     role = infer_from_text(getattr(image, "name", ""), getattr(image, "filepath", ""))
                     if role:
                         return role, "image_name_or_path"
 
-                for output in getattr(node, "outputs", []) or []:
-                    for link in getattr(output, "links", []) or []:
-                        to_node = getattr(link, "to_node", None)
-                        to_socket = getattr(link, "to_socket", None)
-                        to_socket_name = str(getattr(to_socket, "name", "")).strip().lower()
-                        to_node_type = str(getattr(to_node, "bl_idname", ""))
-                        if to_node_type == "ShaderNodeNormalMap":
-                            return "normal", "normal_map_node"
-                        if to_node_type == "ShaderNodeDisplacement":
-                            return "displacement", "displacement_node"
-                        if to_socket_name in socket_roles:
-                            return socket_roles[to_socket_name], "linked_socket"
                 return None, "unknown"
📝 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
def infer_from_text(*values):
joined = "_".join(normalize_token(value) for value in values if value)
for alias, role in role_aliases.items():
if alias in joined:
return role
return None
def infer_texture_role(node):
role = infer_from_text(getattr(node, "label", ""), getattr(node, "name", ""))
if role:
return role, "node_label_or_name"
image = getattr(node, "image", None)
if image:
role = infer_from_text(getattr(image, "name", ""), getattr(image, "filepath", ""))
if role:
return role, "image_name_or_path"
for output in getattr(node, "outputs", []) or []:
for link in getattr(output, "links", []) or []:
to_node = getattr(link, "to_node", None)
to_socket = getattr(link, "to_socket", None)
to_socket_name = str(getattr(to_socket, "name", "")).strip().lower()
to_node_type = str(getattr(to_node, "bl_idname", ""))
if to_node_type == "ShaderNodeNormalMap":
return "normal", "normal_map_node"
if to_node_type == "ShaderNodeDisplacement":
return "displacement", "displacement_node"
if to_socket_name in socket_roles:
return socket_roles[to_socket_name], "linked_socket"
def infer_from_text(*values):
joined = "_".join(normalize_token(value) for value in values if value)
for alias, role in role_aliases.items():
if alias in joined:
return role
return None
def infer_from_links(node):
for output in getattr(node, "outputs", []) or []:
for link in getattr(output, "links", []) or []:
to_node = getattr(link, "to_node", None)
to_socket = getattr(link, "to_socket", None)
to_socket_name = str(getattr(to_socket, "name", "")).strip().lower()
to_node_type = str(getattr(to_node, "bl_idname", ""))
if to_node_type == "ShaderNodeNormalMap":
return "normal", "normal_map_node"
if to_node_type == "ShaderNodeDisplacement":
return "displacement", "displacement_node"
if to_socket_name in socket_roles:
return socket_roles[to_socket_name], "linked_socket"
return None, "unknown"
def infer_texture_role(node):
role, role_source = infer_from_links(node)
if role:
return role, role_source
role = infer_from_text(getattr(node, "label", ""), getattr(node, "name", ""))
if role:
return role, "node_label_or_name"
image = getattr(node, "image", None)
if image:
role = infer_from_text(getattr(image, "name", ""), getattr(image, "filepath", ""))
if role:
return role, "image_name_or_path"
return None, "unknown"
🧰 Tools
🪛 Ruff (0.15.17)

[warning] 6889-6889: Missing return type annotation for private function infer_from_text

(ANN202)


[warning] 6889-6889: Missing type annotation for *values

(ANN002)


[warning] 6896-6896: Missing return type annotation for private function infer_texture_role

(ANN202)

🤖 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 `@public/downloads/vipermesh-addon.py` around lines 6889 - 6918, The
infer_texture_role function checks text-based inference (node label, image
name/path) before graph link inference, which allows broad substring matches in
paths to override specific socket connections. Reorder the logic in
infer_texture_role to check the graph links first (the loop iterating over
outputs and links to find connected socket roles), and only fall back to
text-based inference on node labels and image names if no graph link information
is found. This ensures specific socket connections take precedence over generic
text pattern matching.

Comment on lines +6929 to +7018
missing_materials = []
if requested_names:
materials = []
for name in requested_names:
material = bpy.data.materials.get(name)
if material is None:
missing_materials.append(name)
else:
materials.append(material)
else:
materials = [material for material in bpy.data.materials if getattr(material, "use_nodes", False)]

reports = []
changes = []
warnings = []
for material in materials:
material_report = {
"material": material.name,
"image_textures": [],
}
node_tree = getattr(material, "node_tree", None)
if not node_tree:
reports.append(material_report)
continue

for node in node_tree.nodes:
if getattr(node, "bl_idname", "") != "ShaderNodeTexImage":
continue
image = getattr(node, "image", None)
if image is None:
material_report["image_textures"].append({
"node": node.name,
"role": None,
"issue": "missing_image",
})
continue

role, role_source = infer_texture_role(node)
current_color_space = image.colorspace_settings.name
expected_color_space = expected_color_spaces.get(role)
needs_change = bool(expected_color_space and current_color_space != expected_color_space)
entry = {
"node": node.name,
"label": node.label,
"image": image.name,
"filepath": bpy.path.abspath(image.filepath) if image.filepath else "",
"role": role,
"role_source": role_source,
"current_color_space": current_color_space,
"expected_color_space": expected_color_space,
"needs_change": needs_change,
}

if role is None:
warnings.append(f"{material.name}/{node.name}: could not infer texture role")
elif needs_change:
change = {
"material": material.name,
"node": node.name,
"image": image.name,
"role": role,
"from": current_color_space,
"to": expected_color_space,
"applied": False,
}
if apply_changes:
try:
image.colorspace_settings.name = expected_color_space
change["applied"] = True
entry["current_color_space"] = image.colorspace_settings.name
entry["needs_change"] = image.colorspace_settings.name != expected_color_space
except Exception as exc:
change["error"] = str(exc)
warnings.append(f"{material.name}/{node.name}: failed to set color space to {expected_color_space}: {str(exc)}")
changes.append(change)

material_report["image_textures"].append(entry)

reports.append(material_report)

return {
"success": True,
"apply_changes": apply_changes,
"materials_checked": len(reports),
"missing_materials": missing_materials,
"changes": changes,
"warnings": warnings,
"reports": reports,
"next_safe_action": "run inspect_material_node_graph on changed materials, then validate_export_readiness if exporting",
}

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

Return unsuccessful status when requested materials are missing.

A call with only misspelled material_names returns success: True with materials_checked: 0, so the agent can treat a no-op as completed.

Suggested fix
             return {
-                "success": True,
+                "success": not bool(missing_materials),
                 "apply_changes": apply_changes,
                 "materials_checked": len(reports),
                 "missing_materials": missing_materials,
📝 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
missing_materials = []
if requested_names:
materials = []
for name in requested_names:
material = bpy.data.materials.get(name)
if material is None:
missing_materials.append(name)
else:
materials.append(material)
else:
materials = [material for material in bpy.data.materials if getattr(material, "use_nodes", False)]
reports = []
changes = []
warnings = []
for material in materials:
material_report = {
"material": material.name,
"image_textures": [],
}
node_tree = getattr(material, "node_tree", None)
if not node_tree:
reports.append(material_report)
continue
for node in node_tree.nodes:
if getattr(node, "bl_idname", "") != "ShaderNodeTexImage":
continue
image = getattr(node, "image", None)
if image is None:
material_report["image_textures"].append({
"node": node.name,
"role": None,
"issue": "missing_image",
})
continue
role, role_source = infer_texture_role(node)
current_color_space = image.colorspace_settings.name
expected_color_space = expected_color_spaces.get(role)
needs_change = bool(expected_color_space and current_color_space != expected_color_space)
entry = {
"node": node.name,
"label": node.label,
"image": image.name,
"filepath": bpy.path.abspath(image.filepath) if image.filepath else "",
"role": role,
"role_source": role_source,
"current_color_space": current_color_space,
"expected_color_space": expected_color_space,
"needs_change": needs_change,
}
if role is None:
warnings.append(f"{material.name}/{node.name}: could not infer texture role")
elif needs_change:
change = {
"material": material.name,
"node": node.name,
"image": image.name,
"role": role,
"from": current_color_space,
"to": expected_color_space,
"applied": False,
}
if apply_changes:
try:
image.colorspace_settings.name = expected_color_space
change["applied"] = True
entry["current_color_space"] = image.colorspace_settings.name
entry["needs_change"] = image.colorspace_settings.name != expected_color_space
except Exception as exc:
change["error"] = str(exc)
warnings.append(f"{material.name}/{node.name}: failed to set color space to {expected_color_space}: {str(exc)}")
changes.append(change)
material_report["image_textures"].append(entry)
reports.append(material_report)
return {
"success": True,
"apply_changes": apply_changes,
"materials_checked": len(reports),
"missing_materials": missing_materials,
"changes": changes,
"warnings": warnings,
"reports": reports,
"next_safe_action": "run inspect_material_node_graph on changed materials, then validate_export_readiness if exporting",
}
return {
"success": not bool(missing_materials),
"apply_changes": apply_changes,
"materials_checked": len(reports),
"missing_materials": missing_materials,
"changes": changes,
"warnings": warnings,
"reports": reports,
"next_safe_action": "run inspect_material_node_graph on changed materials, then validate_export_readiness if exporting",
}
🧰 Tools
🪛 Ruff (0.15.17)

[warning] 7000-7000: Do not catch blind exception: Exception

(BLE001)


[warning] 7002-7002: Use explicit conversion flag

Replace with conversion flag

(RUF010)

🤖 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 `@public/downloads/vipermesh-addon.py` around lines 6929 - 7018, The function
returns success: True even when all requested materials in requested_names are
missing and no materials are actually processed. Add a condition before the
return statement to check if requested_names was provided but resulted in zero
materials being processed (i.e., len(requested_names) > 0 and len(materials) ==
0), and if so, set success to False in the returned dictionary instead of True.
This ensures that a no-op due to misspelled or non-existent material names is
correctly reported as a failed operation.

Comment on lines +19 to +21
assert.match(source, /expected_color_spaces = \{/, `${label} defines deterministic color-space expectations`)
assert.match(source, /"base_color": "sRGB"/, `${label} treats base color maps as sRGB`)
assert.match(source, /"normal": "Non-Color"/, `${label} treats normal maps as Non-Color`)

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the addon defines color space mappings for all standard PBR channels

rg -A 20 'expected_color_spaces\s*=' desktop/assets/vipermesh-addon.py

Repository: Ker102/ViperMesh

Length of output: 874


Test coverage for color space mappings is incomplete, but the addon implementation is more comprehensive than originally assessed.

The test validates only base_color (sRGB) and normal (Non-Color), but the addon implementation actually defines color spaces for additional PBR channels: emission (sRGB), roughness (Non-Color), metallic (Non-Color), ambient_occlusion (Non-Color), alpha (Non-Color), and displacement (Non-Color).

Note: The addon maps displacement to Non-Color rather than Linear, contradicting the external context assumption. Consider expanding test assertions to verify the complete set of expected color space mappings if full coverage is desired.

🤖 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 `@scripts/test/test-blender-material-texture-normalization-tool.ts` around
lines 19 - 21, The test assertions are incomplete and only verify color space
mappings for base_color and normal channels, but the addon implementation
defines mappings for additional PBR channels: emission (sRGB), roughness
(Non-Color), metallic (Non-Color), ambient_occlusion (Non-Color), alpha
(Non-Color), and displacement (Non-Color). Add additional assert.match calls
after the existing ones to verify that the source contains the correct color
space mappings for each of these additional PBR channels, following the same
pattern as the existing base_color and normal assertions.

Source: MCP tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend desktop documentation Improvements or additions to documentation scripts

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant