- 2026-07-29 —
NormalOutputMode.BOTHis actually both. The guard that stops the engine clobbering a user-supplied complement short-circuited the whole branch whenever the batch carried OpenGL, so a GL-only batch — the common case — never got its DirectX counterpart, and the DX-from-GL branch below it was unreachable in the normal flow. Verified empirically: a DirectX source produced both files, an OpenGL source produced only OpenGL, contradicting the mode's own docstring ("save the provided format + auto-generate the complement"). The guard is now per-direction — emit the counterpart of whichever format the map is, and only when that counterpart is absent from the batch inventory — restoring symmetry without weakening the anti-clobber rule: a batch carrying both formats still regenerates neither, and the DX/GL variant sets are disjoint so at most one branch applies. Note for GL-only projects: BOTH now writes the_Normal_DirectX.pngsibling it used to skip; selectOpenGL onlyin the compositor's normal-mode combo to opt out. - 2026-07-29 —
MapCompositor's output template no longer touches textures it didn't write.apply_output_templatere-scannedoutput_dirwithget_imagesand handed everything it found toMapFactory.prepare_maps. Compositing into a shared library folder — a project'ssourceimages, the normal case — therefore swept every pre-existing texture set in there into the post-pass:group_textures_by_setsplit the folder into N unrelated sets and generated packed/format-converted siblings for materials the user never selected (observed: a 2-set composite produced template output for 9 sets). The composite pass now records every file it writes (written_paths, cleared per batch, including auto-generated normal complements and minus any variant a*_ONLYnormal mode pruned), and the post-pass is scoped to that list. Tracking followsoptimize_map's return value rather than the pre-optimization path — the optimizer resolves its own output name, so recording the input would leave a tracked path the optimizer had renamed away.apply_output_template(output_dir, files=None)takes an explicit override; it falls back to a directory scan only when the engine has written nothing, so a standalone call on a pre-existing folder still works — and that scan is now a path-only listing over the same extension set, where the oldget_imagescall decoded every texture in the folder (whole 4K sets) just to discard the pixels. TheOptimize outputtoggle was never implicated —_maybe_optimizeonly ever saw the path it just saved. - 2026-07-29 —
MathUtils.fit_into_tile+uv_tile_margin+majority_tile: the UDIM-gather and tile-border rules, one copy each.fit_into_tilereturns the relative offset that brings a UV bounding box into a target tile — a whole-tile translation preserving the box's sub-tile position (the cheap stand-in for a repack), a minimal pull-in when the box merely straddles the target's border, and a margin-inset clamp for the remainder. A box too big for the inset tile is centered, not edge-anchored: anchoring pushed the common full-coverage shell (UVs spanning 0-1 exactly) off the far border by the whole margin, which the Blender engine test caught.uv_tile_marginnames the tile-border rule — half the island gutter, since the border is shared across a seam — replacing thecalculate_uv_padding(...)/2that was open-coded in the packers. Both are map-size-invariant, so a layout built at one resolution stays correct at another.majority_tilecompletes the set — it decides which tile a gather targets when none is named (each box votes for its bbox-center tile, most votes wins), so the bulk of a layout defines where it lives and only strays travel; it was written twice, once per DCC engine, before being pulled here. Consumers:mayatk/blendertkUvUtils.gather_to_udim, the Shell Xform snap,mayatk._uv_pack.