- MapFactory review sweep — resolve-chain data-correctness fixes, non-greedy passthrough restored for Height/Opacity, and the base-name regex unified onto one registry SSoT (2026-07-08). Five defect classes fixed across the
img_utils/map_factorypackage, each pinned by a failing-first regression test. (1) Raw-substitution in map resolution: handlers and the processor'screate_*assemblers passed source types as preferred types (resolve_map("Roughness", "Specular"),("Roughness","Smoothness","Glossiness","Specular")), so a direct step-1 match returned the raw file verbatim under the target's semantics — an un-inverted Smoothness saved as_Roughness, a raw RGB Specular packed as the metallic channel, and (via Specular'sinput_fallbacks=["Metallic",…]) the metallic map itself returned as "roughness" and inverted into the Mask Map's smoothness alpha. All resolution sites now request the target type only; the conversion registry performs the correct derivations (Smoothness→inverted Roughness P10, Specular→Metallic/Roughness P5, packed-map unpacks P8). Relatedly,MaskMapHandler's last-resortsmoothness = metallicsubstitution is gone — a missing smoothness now leaves the alpha topack_msao_texture's neutral white fill (pinned: alpha extrema (255,255), was an inverted metallic 225). (2)resolve_mapNone-poisoning: a converter returning None (e.g.get_roughness_from_packedon an alpha-less source) was cached into the inventory, making the type read as "present", returning None on every later lookup, and shadowing lower-priority conversions that could succeed; falsy results are no longer cached and resolution falls through. Because the unpack helpers themselves plant None for a channel the source lacks (extract_channelsskips missing channels),resolve_map's direct-match and input-fallback steps plus thecreate_*/unpack_*inventory guards all switched frominto truthiness — a None entry reads as absent instead of short-circuiting resolution. (3) Over-broad consumed-type marking dropped user maps:NormalMapHandler.get_consumed_typesclaimed Bump/Height, so processing any existing normal map silently swallowed a provided Height map (which drives its own parallax/displacement slot);BaseColorHandlerclaimed Opacity/Transparency, so the standard (non-packing) workflow dropped a separate Opacity map. Both lists now declare only what processing renders redundant; the conversion/packing branches still mark their actual sources internally. (4) Base-name regex drift:ImgUtils.get_base_texture_nameandMapFactory.get_base_texture_namecarried near-duplicate suffix-strip regexes with different semantics (brick_ao.png→brickin the factory,brick_aoin ImgUtils — grouping and extract-channel naming disagreed), and the factory's ownpack_*methods used them inconsistently. The pattern now lives once onMapRegistry.get_suffix_strip_pattern()(the alias owner; cached), and both resolvers delegate — underscore-delimited suffixes case-insensitive at any length, attached short suffixes still capital-gated. (5) Robustness/cleanup:TextureProcessor.save_mapno longer crashes in the dry-run and up-to-date paths when constructed without a logger (a public configuration — mayatk's shader templates build the processor directly); handlers stopped reaching intoMapRegistry._maps(publicget_map_types()/get()instead);ConversionRegistry.__getattr__raises for_-prefixed names so protocol probes don't receive empty conversion lists; fourunpack_*methods lost identicalif save/elsedead branches;examples/texture_factory_extensibility_example.pywas rewritten against the real API (it imported a nonexistentmap_factory_refactoredmodule and demonstrated wrong signatures throughout); and a leftover block of editing-session deliberation comments was removed fromtest_map_factory.py. Tests:test_map_factory.py(+6: Height/Opacity passthrough, Mask-Map white-alpha fill, resolve_map skips failed conversions and planted-None entries, logger-less dry-run) andtest_affix_hardening.py(+2 shared-table cases pinning_aoparity across both resolvers); full suite 1701 green (1684 passed, 17 skipped); consumer-side extapps texture-panel suites (converter/packer/compositor) 85 green. - Auto-instancing core extracted from mayatk:
PointCloud.match_clouds/pca_basis/pca_eigenvalue_signature+ newgeo_utils/assembly_sorter.py::AssemblySorter; robustpca_transformno longer hard-requires scipy; fixed an inverted relative-transform matrixmatch_cloudsinherited from the in-DCC code (2026-07-08). With blendertk's auto_instancer port as the second consumer, the DCC-neutral halves of mayatk'score_utils/auto_instancermoved down to this layer (both DCCs are now thin data-extraction adapters): (1)PointCloud.match_clouds(points_a, points_b, tolerance, scale_tolerance, normals_a/b, normal_threshold, uvs_identical, ...)— the full 3-stage identity test (fast ordered max-deviation → unordered K=4-twin KDTree identity with the flip-free normal gate → centered RMS-uniform-scale normalization + robustpca_transformwith scale fold-back and translation composition); the UV check rides as a lazy callback so it hard-rejects exactly where the in-DCC version did (stage-1/2 positional success). (2)PointCloud.pca_basis(points)— the stabilized PCA frame (third-moment sign anchors, farthest-vertex orientation of degenerate subspaces) behind transform canonicalization, andpca_eigenvalue_signature(points)— the max-normalized quantized eigenvalue bucketing descriptor. (3)AssemblySorter— the entire separated-part clustering pipeline (relative-gap area classes, vectorized same-material bbox-touch adjacency, BFS components, fractured-fusion repair, whole-multiset repeat guard, 3-model GCD count splits with touch-first growth + internal-distance-consistency assignment + distinguishability-ordered greedy fallback, orphan recovery, exemplar-distance binding, proportional-area cross-copy support gate) operating on plain part-feature dicts (contract in the module docstring; thekey-shadowing loop variable in the distance-consistency pass was renamed in transit). Bug fix with real-world impact:pca_transform's matrix maps cloud-b onto cloud-a (verified empirically, 3e-15), but the in-DCC composition assumed the opposite direction and only rebuilt the translation row — the relative transform placed a genuinely-rotated copy's replacement wrong whenever the rotation wasn't symmetric (R ≠ Rᵀ). It went unnoticed because the assembly flow canonicalizes first (rel = identity) and the leaf-mode tests only exercised identity/180° rotations.match_cloudstransposes the linear block before rebuilding translation; caught by the new direction-verifying tests. scipy now optional in robust mode: the earlyreturn Nonegate is lifted — the existing brute-force scorer handles robust candidates (bounded memory), only the Kabsch spin refinement stays scipy-gated, and a new publicPointCloud.nn_queryhelper (scipy KDTree, else chunked brute force — verified identical results) backs the unordered-identity stage (public because blendertk's geometry matcher consumes it directly — a cross-package private-API dependency would be brittle). Blender's bundled Python has no scipy; this makes the blendertk port work out of the box. RegisteredAssemblySorterinDEFAULT_INCLUDE/__all__. Tests:test_pointcloud.py+12 (match/reorder/count-mismatch/empty-clouds-identity/rotated-with-matrix-verification/scaled-strict-vs-scale-mode/UV-callback/half-flipped-normal-reject/no-scipy-fallback/NN-parity/basis-canonicalization-consistency/eigen-signature), newtest_assembly_sorter.py(7 scenario mirrors of mayatk's TestAssemblySorting: stacked clasps, material bridge, one-off cluster, scaled copies, orphan recovery); full suite 1695 green. Consumer-verified: mayatk auto_instancer 55/55 + ground truth 3/3 (scene-1 precision/recall 1.000,[5,3,1]sharing) + scene-match 1/1 + full 3576/3576 on the delegated implementation; blendertktest_auto_instancer.py38/38. Color/ColorPair/Palettefolded from a top-levelcolor_utilssubpackage intocore_utils/color.py(2026-07-08). These three are zero-dependency (colorsys-only) immutable value types, and despite the name every consumer uses them for UI/display theming — uitk option-box/switchboard status colors, the maya+blender hierarchy-diff tree renderers,Palette.axes()/.channels()for axis and animation-channel colors — never in the image pipeline (img_utils's own color code is unrelated per-pixel sRGB/linear map math). So they belong beside the other foundationalcore_utilsprimitives (NamedTupleContainer,SymbolRecord), not as a top-level domain sibling toimg_utils/file_utils. Public API unchanged —from pythontk import Color, ColorPair, Palettestill resolves through the lazy root; only the internal module path and its oneDEFAULT_INCLUDEregistration moved (color_utils._color_utils→core_utils.color). No code anywhere imported the submodule path directly, so there was zero call-site churn across uitk/mayatk/blendertk/tentacle. Test renamedtest_color_utils.py→test_color.py(one-file-per-module) and repointed to the public root import (resilient to future internal moves); full suite 1654 green.