You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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_factory package, each pinned by a failing-first regression test. (1) Raw-substitution in map resolution: handlers and the processor's create_* 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's input_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-resort smoothness = metallic substitution is gone — a missing smoothness now leaves the alpha to pack_msao_texture's neutral white fill (pinned: alpha extrema (255,255), was an inverted metallic 225). (2) resolve_map None-poisoning: a converter returning None (e.g. get_roughness_from_packed on 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_channels skips missing channels), resolve_map's direct-match and input-fallback steps plus the create_*/unpack_* inventory guards all switched from in to truthiness — a None entry reads as absent instead of short-circuiting resolution. (3) Over-broad consumed-type marking dropped user maps:NormalMapHandler.get_consumed_types claimed Bump/Height, so processing any existing normal map silently swallowed a provided Height map (which drives its own parallax/displacement slot); BaseColorHandler claimed 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_name and MapFactory.get_base_texture_name carried near-duplicate suffix-strip regexes with different semantics (brick_ao.png → brick in the factory, brick_ao in ImgUtils — grouping and extract-channel naming disagreed), and the factory's own pack_* methods used them inconsistently. The pattern now lives once on MapRegistry.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_map no 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 into MapRegistry._maps (public get_map_types()/get() instead); ConversionRegistry.__getattr__ raises for _-prefixed names so protocol probes don't receive empty conversion lists; four unpack_* methods lost identical if save/else dead branches; examples/texture_factory_extensibility_example.py was rewritten against the real API (it imported a nonexistent map_factory_refactored module and demonstrated wrong signatures throughout); and a leftover block of editing-session deliberation comments was removed from test_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) and test_affix_hardening.py (+2 shared-table cases pinning _ao parity 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 + new geo_utils/assembly_sorter.py::AssemblySorter; robust pca_transform no longer hard-requires scipy; fixed an inverted relative-transform matrix match_clouds inherited from the in-DCC code (2026-07-08). With blendertk's auto_instancer port as the second consumer, the DCC-neutral halves of mayatk's core_utils/auto_instancer moved 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 + robust pca_transform with 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, and pca_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; the key-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_clouds transposes the linear block before rebuilding translation; caught by the new direction-verifying tests. scipy now optional in robust mode: the early return None gate is lifted — the existing brute-force scorer handles robust candidates (bounded memory), only the Kabsch spin refinement stays scipy-gated, and a new public PointCloud.nn_query helper (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. Registered AssemblySorter in DEFAULT_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), new test_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; blendertk test_auto_instancer.py 38/38.
Color/ColorPair/Palette folded from a top-level color_utils subpackage into core_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 foundational core_utils primitives (NamedTupleContainer, SymbolRecord), not as a top-level domain sibling to img_utils/file_utils. Public API unchanged — from pythontk import Color, ColorPair, Palette still resolves through the lazy root; only the internal module path and its one DEFAULT_INCLUDE registration 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 renamed test_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.