github m3trik/pythontk v0.8.99
pythontk v0.8.99

2 hours ago
  • 2026-07-30 — MapFactory: packed maps declare their per-channel layout, and dropping one can no longer lose a channel. Two structural upgrades to the textures engine plus three bugs found in the sweep.

    • MapType.channels — new SSoT for what each packed map carries per channel in its canonical layout (ORM {R: AO, G: Roughness, B: Metallic}, MSAO {R: Metallic, G: AO, B: Detail_Mask?, A: Smoothness}, …; trailing ? marks filler channels coverage must not demand). The packed-map contract extends to it (__post_init__ requires it alongside is_packed/replaces/config_key), and a new consistency rule — every carried type must appear in replaces — caught a live data bug on arrival: MSAO carried Smoothness in its alpha but didn't replace it, so a loose Smoothness map stayed wired beside a mask map. Fixed.
    • filter_redundant_maps is now lossless and reports its decisions. In an unpacked workflow it judges each packed map's channels for coverage dynamically — a channel counts covered when its type, or any loose type the conversion registry can derive it from (loose Roughness covers a Smoothness channel), survives the drop. Channels nothing covers are extracted to real loose files from the packed source before it drops, via the same conversion registrations and TextureProcessor save pipeline prepare_maps uses (so dry_run plans without writing). When extraction is unavailable, the packed map is kept and its components retire — the lossless direction either way. A real loose map already on disk under the canonical output name is reused, never overwritten with extracted channel data (TextureProcessor.output_path_for now exposes the naming convention save_map writes with, so the probe can't drift from the writer). The flagged failure this kills: an unpacked preset meeting an MSAO beside loose Metallic/Roughness but no separate AO silently lost the AO channel. Returns {"dropped": {type: reason}, "extracted": {type: path}} (was None); a packed map with no loose components present stays the sole source, as before. Mat Updater and mayatk's Game Shader both get the recovery for free.
    • Bug: unpack helpers clobbered provided loose maps. Asking a packed map for one channel (get_ao_from_msao) cached all its channels over the inventory unconditionally — replacing a real loose Metallic file with the MSAO R-channel extraction. All five unpack helpers now fill gaps only (_cache_unpacked); provided files always win.
    • Bug: a Bump-sourced normal swallowed the Height map. NormalMapHandler marked both Bump and Height used after generating from one, so a real Height map (its own parallax/displacement slot) never passed through. Only the actual source is marked now.
    • Bug: inventory "specificity" was judged on full-path length. _build_map_inventory sorted by whole-path length to prefer Mixed_AO over AO, letting a longer directory name decide the winner; now basename length. test_map_factory_grouping.py +7, test_map_registry_register.py +3, test_map_factory.py +3.
  • 2026-07-30 — Albedo_Transparency and Metallic_Smoothness declared no replaces, so packing a channel into them left the map it packed in the set — and both then wired into the same material slot. User-reported: after Mat Updater packed the opacity into the albedo, the old standalone Opacity map was still connected to the shader. filter_redundant_maps resolves packed-vs-loose redundancy from each MapType's replaces list, and only ORM / MSAO / MRAO had one — so nothing ever retired the separate Opacity (or Roughness) map its packed counterpart had absorbed, and the loose map re-connected alongside it. Both now declare what they carry (Albedo_TransparencyBase_Color / Diffuse / Opacity, and it's marked is_packed; Metallic_SmoothnessMetallic / Roughness / Smoothness / Glossiness), so the existing config_key direction logic applies to them like every other packed map: packed wins when the preset asks for it, the separate maps win when it doesn't. test_map_factory_grouping.py +3.

    • The contract is now structural, not conventional. MapType.__post_init__ rejects any definition with a partial packed-map contract — is_packed or replaces present without the full trio (is_packed + replaces + config_key). Guarding on replaces too matters: filter_redundant_maps keys its precedence rules off replaces (never is_packed), and a map with replaces but no config_key skips the direction gate and silently supersedes its components in every preset, unpacked ones included. Each omission now fails at definition time (in-tree or a consumer's MapRegistry().register(...)) instead of shipping this bug class again. Loose maps are exempt. test_map_registry_register.py +6 (TestPackedMapContract), including a live-registry sweep.
  • 2026-07-29 — StepToggle — the timed multi-step press cycle behind repeat-press hotkeys (core_utils/step_toggle.py, ptk.StepToggle). CoreUtils.cycle rotates a sequence forever, which is wrong for "press again to go further": there is a home state to come back to, and a paused cycle must not step deeper. StepToggle models 0 (home) -> 1 -> ... -> steps -> 0 with an injected clock — a press older than timeout never steps deeper, it returns home (so a multi-step toggle degrades to a plain on/off toggle once the user stops tapping), and a press whose context changed restarts at step 1 instead of undoing something the user just left. payload carries whatever the caller must restore on the way home (a camera snapshot) and began_cycle marks the press that should capture it — that is not simply "was at home": a stale retarget starts a new session whose home is the state being left now, while retargeting inside the timeout keeps the original home so a quick re-aim still unwinds to where it began. get(name) keeps the state for hotkey handlers that have no instance to hang it on, and scales(steps) is the per-step magnitude ramp (more steps -> gentler start, same per-step gain). Backs m_frame in both DCC packages. test_step_toggle.py +25.

  • 2026-07-29 — MathUtils.next_clear_offset(bounds, blockers, axis, direction, margin=0.0) — park a box in the next gap along an axis. Backs the Shell Xform move pad's new snap to shell mode, which both DCC packages ship, so the geometry cannot live in either without duplicating it. Candidate landing spots are one margin short of each blocker's near edge and one margin past its far edge; the nearest spot strictly ahead whose landed box overlaps nothing wins. Validating fit is the whole point — a naive nearest-edge rule lands in a gap too small for the box and overlaps the very shell it was walking past (measured: a 0.2-tall box stepping through a 0.1 gap). Blockers not overlapping on the perpendicular axis are dropped up front: travel along axis never changes that overlap, so one lane filter serves both candidate generation and the fit test. Returns None when nothing lies ahead, leaving the fallback rule to the caller. test_math.py +7 (park, skip-too-small-gap, reversible walk, out-of-lane, nothing-ahead, already-parked-advances-past, u axis).

Don't miss a new pythontk release

NewReleases is sending notifications on new releases.