<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged addon</title>
<link>https://coderlegion.com/tag/addon</link>
<description>Powered by Question2Answer</description>
<item>
<title>I Built an ML Routing Engine for 3D Mesh Retopology — Here's How It Works</title>
<link>https://coderlegion.com/13196/i-built-an-ml-routing-engine-for-3d-mesh-retopology-heres-how-it-works</link>
<description>&lt;p&gt;I've spent the last several months building Quadify Ultra, a Blender 5.0 addon that uses a KNN-based ML engine to automatically route meshes to the right retopology algorithm. This post is about the technical decisions behind it — the parts that were harder than expected and the approaches that actually worked.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-problem-retopology-algorithm-selection-is-a-classification-task&quot;&gt;The problem: retopology algorithm selection is a classification task&lt;/h3&gt;
&lt;p&gt;Retopology — converting triangulated meshes to clean quad topology — isn't one problem. It's five different problems depending on what kind of mesh you're dealing with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Open curved panels (car doors, fenders) need edge-flip and greedy quad matching that preserves vertex positions&lt;/li&gt;
&lt;li&gt;CAD imports with hard edges need boundary detection before any merging happens&lt;/li&gt;
&lt;li&gt;Flat CAD surfaces with n-gon triangulation need dissolve passes before quadification&lt;/li&gt;
&lt;li&gt;Closed flat meshes need field-guided remeshing (QuadriFlow)&lt;/li&gt;
&lt;li&gt;Organic meshes and scans with broken topology need voxel rebuilding&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The routing decision — &lt;em&gt;which algorithm for this mesh&lt;/em&gt; — is something experienced artists do implicitly. I wanted to automate it.&lt;/p&gt;
&lt;p&gt;The input features I settled on after testing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;features = [
    tri_ratio,           # fraction of triangles
    quad_ratio,          # fraction of quads  
    ngon_ratio,          # fraction of n-gons
    boundary_ratio,      # open boundary edges / total edges
    non_manifold_ratio,  # non-manifold edges / total edges
    normal_std,          # standard deviation of face normals (surface curvature proxy)
    edge_length_cv,      # coefficient of variation of edge lengths
    face_area_cv,        # coefficient of variation of face areas
    avg_valence,         # average vertex valence
    pole_ratio,          # fraction of vertices with valence != 4
    # ... 8 more geometric ratios
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;18 features total. All normalised ratios — no absolute values, so the classifier is scale-independent.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-classifier-knn-with-confidence-blending&quot;&gt;The classifier: KNN with confidence blending&lt;/h3&gt;
&lt;p&gt;I went with KNN rather than a neural net for two reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Interpretability&lt;/strong&gt; — when the engine recommends Smart mode at 73% confidence, I can inspect the k nearest neighbours and understand why&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cold start&lt;/strong&gt; — a neural net needs hundreds of examples before it's useful. KNN with k=3 gives reasonable results from the first 10 operations&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The confidence score is the fraction of the k nearest neighbours that agree on the top recommendation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def get_confidence(neighbours, top_strategy):
    agreeing = sum(1 for n in neighbours if n['strategy'] == top_strategy)
    return agreeing / len(neighbours)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At low DB count the system blends toward a deterministic heuristic:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;blend_weight = min(1.0, db_count / 30)
confidence = (ml_confidence * blend_weight) + (heuristic_confidence * (1 - blend_weight))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This means the first 30 operations use mostly heuristic routing, gradually transitioning to learned routing as the DB grows.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-hardest-bug-voxel-remesh-freezing-blender&quot;&gt;The hardest bug: voxel remesh freezing Blender&lt;/h3&gt;
&lt;p&gt;The original implementation used &lt;code&gt;bpy.ops.object.voxel_remesh()&lt;/code&gt;. This is a synchronous operator — it runs on Python's main thread and blocks the entire UI. On a 39,000-face burger mesh it produced about 731,000 intermediate voxel faces and froze Blender for several minutes.&lt;/p&gt;
&lt;p&gt;The fix came from reading Quadify Pro's source code. They use a &lt;strong&gt;REMESH modifier&lt;/strong&gt; instead:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;mod              = obj.modifiers.new(&quot;QP_VoxelRebuild&quot;, &quot;REMESH&quot;)
mod.mode         = 'VOXEL'
mod.voxel_size   = max(0.0005, min(0.05, world_diag / resolution))
mod.use_smooth_shade = True
bpy.ops.object.modifier_apply(modifier=mod.name)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The modifier executes in Blender's C stack, not Python. It never blocks the UI. Same result, dramatically faster, works on any mesh size.&lt;/p&gt;
&lt;p&gt;The voxel_size formula needed one more fix. The original code had:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;voxel_size = max(
    min_dim / 20.0,      # THIS was the bug
    local_diag / voxel_resolution,
    0.0005,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;min_dim / 20.0&lt;/code&gt; floor was designed to prevent microscopic voxels on large meshes, but it overrode the resolution setting for small objects. A burger mesh at 0.2m scale gave &lt;code&gt;min_dim / 20.0 = 0.01&lt;/code&gt; — 15x larger than the resolution-based target of &lt;code&gt;0.00064&lt;/code&gt;. With 0.01 voxels the mesh produced only 3,138 intermediate faces. QuadriFlow then got &lt;code&gt;min(target_faces, 3138)&lt;/code&gt; = 3,138 as its target and produced a low-detail result.&lt;/p&gt;
&lt;p&gt;Fix: remove the floor entirely, use world-space diagonal from &lt;code&gt;matrix_world @ Vector(v)&lt;/code&gt; for scale independence, and add a 150k face cap to prevent genuinely large meshes from freezing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;bb_world = [obj.matrix_world @ Vector(v) for v in obj.bound_box]
diag = max((p - bb_world[0]).length for p in bb_world)
voxel_size = max(0.0001, min(0.05, diag / voxel_resolution))

# Cap to prevent freeze on large meshes
estimated_faces = (diag / voxel_size) ** 2
if estimated_faces &amp;gt; 150000:
    voxel_size = diag / math.sqrt(150000)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-community-telemetry-system&quot;&gt;The community telemetry system&lt;/h3&gt;
&lt;p&gt;Each opted-in operation sends 18 geometry ratios and the algorithm result to Supabase:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;payload = {
    'version':   1,
    'features':  [float(f) for f in features],  # 18 ratios only
    'strategy':  strategy,
    'quad_pct':  quad_pct,
    'corrected': corrected,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No mesh geometry. No vertex positions. No filenames. Just the feature vector and outcome.&lt;/p&gt;
&lt;p&gt;The fetch uses only &lt;code&gt;apikey&lt;/code&gt; header — no &lt;code&gt;Authorization: Bearer&lt;/code&gt; — because the new Supabase &lt;code&gt;sb_publishable_*&lt;/code&gt; key format explicitly rejects the Authorization header:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;req = urllib.request.Request(url, headers={
    'apikey':  SUPABASE_KEY,
    'Accept':  'application/json',
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Applying the update merges remote records into the local KNN experience DB, with deduplication and Postgres array format handling (&lt;code&gt;{0.1,0.2,...}&lt;/code&gt; → Python list).&lt;/p&gt;
&lt;p&gt;The callback uses &lt;code&gt;bpy.app.timers&lt;/code&gt; to write results back to Blender properties on the main thread — background threads cannot write to Blender properties directly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def apply_result():
    if _result[0] is None:
        return 0.5  # check again in 0.5s
    success, data = _result[0]
    scene.quadify_ultra.ml_update_status = f&quot;Updated — {total:,} operations&quot;
    return None  # stop timer

bpy.app.timers.register(apply_result, first_interval=0.5)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;numpy-boolean-ambiguity-in-batch-processing&quot;&gt;numpy boolean ambiguity in batch processing&lt;/h3&gt;
&lt;p&gt;One subtle bug that cost time: &lt;code&gt;MeshFeatureExtractor.extract()&lt;/code&gt; returns a numpy array. In the batch loop:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;normal_std = features[5] if features else 0.3  # WRONG
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;if features&lt;/code&gt; on a numpy array raises &lt;code&gt;ValueError: The truth value of an array is ambiguous&lt;/code&gt;. The fix:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;normal_std = float(features[5]) if features is not None else 0.3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;float()&lt;/code&gt; wrapper also prevents numpy scalar type issues downstream in JSON serialisation.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-s-live&quot;&gt;What's live&lt;/h3&gt;
&lt;p&gt;Quadify Ultra is available on Superhive: &lt;a rel=&quot;nofollow&quot; href=&quot;https://superhivemarket.com/products/quadify&quot;&gt;https://superhivemarket.com/products/quadify&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;$200 one-time, Blender 5.0+, MIT licensed. The ML model improves as the community database grows — currently ~160+ operations from early testers.&lt;/p&gt;
&lt;p&gt;Happy to go deeper on any part of the implementation.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Asset credit:&lt;/strong&gt; Car mesh © 2017 Khronos Group. CC BY 4.0 — &lt;a rel=&quot;nofollow&quot; href=&quot;https://creativecommons.org/licenses/by/4.0/&quot;&gt;https://creativecommons.org/licenses/by/4.0/&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13196/i-built-an-ml-routing-engine-for-3d-mesh-retopology-heres-how-it-works</guid>
<pubDate>Thu, 19 Mar 2026 12:31:11 +0000</pubDate>
</item>
<item>
<title>I Built a Batch Asset Validator for Blender in Python — Here's What I Learned</title>
<link>https://coderlegion.com/12945/i-built-a-batch-asset-validator-for-blender-in-python-heres-what-i-learned</link>
<description>&lt;h2 id=&quot;i-built-a-batch-asset-validator-for-blender-in-python-here-s-what-i-learned&quot;&gt;I Built a Batch Asset Validator for Blender in Python — Here's What I Learned&lt;/h2&gt;
&lt;p&gt;Game developers and 3D artists deal with a problem that doesn't get talked about enough: &lt;strong&gt;you find out your assets are broken after you've already submitted them.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Wrong scale. Tri count way over budget. Missing textures. Unapplied transforms. Negative scale that makes your mesh flip inside-out in Unity. These issues are obvious in hindsight, but catching them manually across a folder of 50 FBX files before every export is the kind of tedious work that either doesn't happen, or happens inconsistently.&lt;/p&gt;
&lt;p&gt;So I built AssetSentinel — a Blender addon that batch validates FBX and glTF assets against a target platform profile before they leave your machine. Here's the technical breakdown of how it works, what I ran into building it, and a full demo video at the end.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-core-problem-validation-belongs-before-export-not-after&quot;&gt;The Core Problem: Validation Belongs Before Export, Not After&lt;/h3&gt;
&lt;p&gt;Most 3D pipelines look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Model → Export FBX/glTF → Import to Engine → Discover problem → Go back to Blender
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The discovery step is the expensive one. If you're submitting to a marketplace like Fab or CGTrader, a rejection costs you review time. If you're a studio delivering to a client, it costs credibility. The fix is moving validation to before the export step:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Model → Validate → Fix → Export FBX/glTF → Import to Engine
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's what AssetSentinel does.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-it-works-temp-scene-import-loop&quot;&gt;How It Works: Temp Scene Import Loop&lt;/h3&gt;
&lt;p&gt;The core mechanism is a &lt;strong&gt;modal operator&lt;/strong&gt; that imports each file into a temporary Blender scene, runs a series of checks, then destroys the scene and moves to the next file. This keeps the user's working scene completely untouched.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def _evaluate_in_temp_scene(context, path: str, profile: dict) -&amp;gt; dict:
    win = context.window
    orig_scene = win.scene
    temp_scene = bpy.data.scenes.new(name=&quot;ASSETSENTINEL_TEMP&quot;)
    win.scene = temp_scene
    try:
        _import_file(path)
        # run all checks here
        tris, verts = _tri_vert_counts()
        ngons = _ngon_count()
        # ... more checks
    except Exception:
        # classify the import failure
        detail = _tb_last_meaningful_line(traceback.format_exc())
        ftype, suggestion = _classify_import_failure(detail)
    finally:
        win.scene = orig_scene
        bpy.data.scenes.remove(temp_scene)
        bpy.ops.outliner.orphans_purge(do_recursive=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;finally&lt;/code&gt; block is non-negotiable. You have to restore the original scene and purge orphan data whether the import succeeded or failed — otherwise you accumulate stale mesh data across every file in the batch.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-modal-operator-pattern&quot;&gt;The Modal Operator Pattern&lt;/h3&gt;
&lt;p&gt;Blender's UI locks up if you run long operations on the main thread. The solution is a &lt;strong&gt;modal operator&lt;/strong&gt; driven by a timer event. Each tick of the timer processes one file, then yields back to Blender so the UI stays responsive and the user can cancel with ESC.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def modal(self, context, event):
    wm = context.window_manager
    if event.type == &quot;ESC&quot;:
        wm.assetsentinel_cancel_requested = True
    if event.type != &quot;TIMER&quot;:
        return {&quot;PASS_THROUGH&quot;}
    if self._idx &amp;gt;= len(self._files):
        wm.assetsentinel_status = &quot;Done&quot;
        return self._finish(context, cancelled=False)
    # process one file per tick
    path = self._files[self._idx]
    res = _evaluate_in_temp_scene(context, path, prof)
    self._results.append(res)
    self._idx += 1
    wm.assetsentinel_progress = int((self._idx / len(self._files)) * 100)
    return {&quot;RUNNING_MODAL&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The timer interval is 0.1 seconds. Fast enough to feel responsive, slow enough not to hammer the CPU.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;profile-based-validation&quot;&gt;Profile-Based Validation&lt;/h3&gt;
&lt;p&gt;Rather than hardcoding limits, AssetSentinel uses &lt;strong&gt;JSON profiles&lt;/strong&gt; — one per target platform. Each profile defines the limits that matter for that destination:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;UNITY_MOBILE&quot;: {
    &quot;label&quot;: &quot;Unity - Mobile&quot;,
    &quot;max_tris&quot;: 20000,
    &quot;max_texture_res&quot;: 2048,
    &quot;max_material_slots&quot;: 4,
    &quot;lod_required&quot;: true,
    &quot;no_spaces&quot;: true,
    &quot;min_bbox_m&quot;: 0.05,
    &quot;max_bbox_m&quot;: 10.0,
    &quot;tri_over_is_error&quot;: true,
    &quot;lod_missing_is_error&quot;: false
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;_is_error&lt;/code&gt; flags per rule let you configure severity independently. A tri count violation might be a hard error on mobile, but only a warning on desktop. The profile system auto-merges on load — if a user has an older &lt;code&gt;profiles.json&lt;/code&gt;, new keys from the defaults get added without losing their custom values.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-checks&quot;&gt;The Checks&lt;/h3&gt;
&lt;p&gt;Here's what runs on every imported asset:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Geometry:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Triangle and vertex count via &lt;code&gt;me.calc_loop_triangles()&lt;/code&gt; — falls back to bmesh triangulate if that fails&lt;/li&gt;
&lt;li&gt;Ngon detection by checking &lt;code&gt;p.loop_total &amp;gt; 4&lt;/code&gt; on every polygon&lt;/li&gt;
&lt;li&gt;Negative scale on any axis&lt;/li&gt;
&lt;li&gt;Unapplied transforms (scale/rotation not at identity)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Textures:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Total image count from &lt;code&gt;bpy.data.images&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;External file resolution — whether the filepath exists on disk&lt;/li&gt;
&lt;li&gt;Max texture resolution from &lt;code&gt;img.size[0]&lt;/code&gt; and &lt;code&gt;img.size[1]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Missing external textures list&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Naming:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Regex match against &lt;code&gt;allowed_name_chars&lt;/code&gt; from the profile&lt;/li&gt;
&lt;li&gt;Space detection in object and material names&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Scale:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;World-space bounding box via &lt;code&gt;evaluated_get(depsgraph).bound_box&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Compared against &lt;code&gt;min_bbox_m&lt;/code&gt; and &lt;code&gt;max_bbox_m&lt;/code&gt; in the profile&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;LOD:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Name-based detection — checks for &lt;code&gt;_LOD0&lt;/code&gt;, &lt;code&gt;_LOD1&lt;/code&gt;, &lt;code&gt;_LOD2&lt;/code&gt;, &lt;code&gt;_LOD3&lt;/code&gt; substrings&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;import-failure-classification&quot;&gt;Import Failure Classification&lt;/h3&gt;
&lt;p&gt;One of the more useful features: when an import fails, it doesn't just say &quot;import error.&quot; It classifies the failure and suggests a fix:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def _classify_import_failure(detail_line: str) -&amp;gt; tuple[str, str]:
    s = (detail_line or &quot;&quot;).lower()
    if &quot;khr_texture_basisu&quot; in s:
        return (
            &quot;UNSUPPORTED_EXTENSION&quot;,
            &quot;Re-export without BasisU/KHR_texture_basisu and try again.&quot;
        )
    if &quot;bad gltf&quot; in s and &quot;json error&quot; in s:
        return (
            &quot;INVALID_GLTF&quot;,
            &quot;The glTF/GLB appears corrupted. Re-export from your DCC.&quot;
        )
    return (&quot;UNKNOWN&quot;, &quot;Try re-exporting and verify the file opens in Blender.&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is genuinely useful in practice. &lt;code&gt;KHR_texture_basisu&lt;/code&gt; failures come up frequently with AI-generated GLB files and photogrammetry exports. The error message from Blender's importer isn't helpful on its own — this surfaces the actual fix.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-html-report&quot;&gt;The HTML Report&lt;/h3&gt;
&lt;p&gt;After the scan, results are exported as an HTML report with a CSV alongside it. The report uses a dark theme, shows per-asset pass/warning/fail status with a triangle count bar against the profile limit, and includes filter controls to show only failed or warning assets.&lt;/p&gt;
&lt;p&gt;The report also surfaces &quot;top violations&quot; — which checks failed most frequently across the batch. On a folder of 26 game assets, the most common failures were tri count overages and suspicious bounding box scales. Both are things that would have caused problems in the engine.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;watch-the-full-demo&quot;&gt;Watch the Full Demo&lt;/h3&gt;
&lt;p&gt;[embed: https://www.youtube.com/watch?v=64qQiMLCSD0]&lt;/p&gt;
&lt;p&gt;The video walks through a real batch scan of 26 assets against the Unreal Desktop profile — showing the scan running, reading the HTML report, and interpreting the results.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;get-the-addon&quot;&gt;Get the Addon&lt;/h3&gt;
&lt;p&gt;AssetSentinel is available on Superhive (Blender Market): &lt;a rel=&quot;nofollow&quot; href=&quot;https://superhivemarket.com/products/assetsentinel&quot;&gt;https://superhivemarket.com/products/assetsentinel&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Supports Blender 3.6 and above. No external dependencies — pure Python, no PyPI packages.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-i-d-do-differently&quot;&gt;What I'd Do Differently&lt;/h3&gt;
&lt;p&gt;A few things I'd approach differently if I started over:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Results storage.&lt;/strong&gt; I'm serialising the results to JSON and storing them in a &lt;code&gt;WindowManager&lt;/code&gt; property string. It works but it's awkward — a proper addon-level data store would be cleaner.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The modal pattern.&lt;/strong&gt; The timer-based modal is the right approach for Blender, but the 0.1s interval is a guess. A smarter approach would adapt the interval based on file size or import time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Profile UI.&lt;/strong&gt; Right now profiles are JSON files edited externally. An in-panel profile editor would make this accessible to artists who don't want to touch JSON.&lt;/p&gt;
&lt;p&gt;Happy to answer questions about any part of the implementation — drop them in the comments.&lt;/p&gt;
&lt;hr&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/12945/i-built-a-batch-asset-validator-for-blender-in-python-heres-what-i-learned</guid>
<pubDate>Mon, 16 Mar 2026 13:58:07 +0000</pubDate>
</item>
<item>
<title>Modifier Stack Fixer [addon]</title>
<link>https://coderlegion.com/9906/modifier-stack-fixer-addon</link>
<description>&lt;p&gt;Modifier Stack Fixer scans your modifier stack and warns you about order issues that cause broken geometry, failed exports, and unexpected behavior—before the damage happens.If you work seriously in Blender, this is a tool you should own.Save time, protect your work, and get the addon now.&lt;/p&gt;
</description>
<category>Launches</category>
<guid isPermaLink="true">https://coderlegion.com/9906/modifier-stack-fixer-addon</guid>
<pubDate>Sat, 17 Jan 2026 15:04:28 +0000</pubDate>
</item>
<item>
<title>Modifier Stack Fixer</title>
<link>https://coderlegion.com/9427/modifier-stack-fixer</link>
<description>&lt;p&gt;Hey everyone &lt;br&gt;
I built Modifier Stack Fixer to solve a problem I kept running into:&lt;br&gt;
broken geometry caused by wrong modifier order, especially in complex or game-ready meshes.&lt;br&gt;
Blender won’t warn you when a modifier stack is quietly sabotaging your mesh. This addon does.&lt;br&gt;
What it does:&lt;br&gt;
Analyzes your modifier stack in real time&lt;br&gt;
Detects common modifier order issues (Subdivision, Boolean, Mirror, Solidify, Armature, etc.)&lt;br&gt;
Flags risky or illogical modifier placements&lt;br&gt;
Suggests safer modifier orders without destructively applying anything&lt;br&gt;
Helps prevent shading issues, deformation bugs, and export problems&lt;br&gt;
Why I made it:&lt;br&gt;
Most stack issues only show up after rigging, UVs, or export  when it’s already painful to fix.&lt;br&gt;
Modifier Stack Fixer catches problems early, before your mesh breaks downstream.&lt;/p&gt;
</description>
<category>Launches</category>
<guid isPermaLink="true">https://coderlegion.com/9427/modifier-stack-fixer</guid>
<pubDate>Thu, 08 Jan 2026 18:57:40 +0000</pubDate>
</item>
</channel>
</rss>