github m3trik/pythontk v0.8.84
pythontk v0.8.84

2 hours ago
  • 2026-07-10 — Review-pass fixes + new B-spline/falloff math (hoisted from mayatk). ExposureEqualizer.equalize_directories's new stale-output purge can no longer destroy a source capture: when the output dir resolves onto the source dir itself (output_root=parent + suffix=""), the purge is skipped with a warning and the run overwrites in place — pre-fix, the rmtree deleted the capture before it was ever read (regression test: test_equalize_never_purges_the_source_dir). The two EXIF-loss failure paths no longer double-count one frame (an EXIF-read failure followed by a PIL-save failure on the same frame counted twice) and each gets its own first-occurrence log gate (whichever fired first used to suppress the other's message for the whole run); the run summary attributes the loss to its per-cause errors instead of blaming a missing PIL. Per-run _sample_stats memo — _reference_stats and the per-capture loop sampled the same directories, decoding every source dir's sample set twice. Stale docs corrected (dhash threshold default, curate output-stem qualification, rasterize_silhouette persistence note). NEW public MathUtils.resolve_falloff_profile / bspline_clamped_knots / bspline_basis — the DCC-agnostic falloff-resolver and clamped-B-spline basis math hoisted out of mayatk's skinning module per the generic-helpers-upstream rule, so the blendertk mirror shares one implementation (partition-of-unity / end-pinning / knot-structure tests in test_math.py). Full suite green (1711).

  • 2026-07-10 — AppLauncher.scan_install_dirs: glob pattern order is now a real priority (first pattern's matches outrank later patterns'). The old implementation pooled every pattern's hits into ONE reverse-sorted list, so for multi-pattern callers the filename became an accidental tiebreaker inside a single install dir: the RizomUV bridges pass (Rizomuv_VS.exe, rizomuv_RS.exe, rizomuv.exe) as an explicit preference, but the pooled sort yielded rizomuv_RS.exe first (lowercase sorts above uppercase) and ranked the bare rizomuv.exe — a launcher that ignores -cfi and hangs a headless run until timeout — above the intended VS binary. Found via mayatk's new RizomUV headless probe (see mayatk CHANGELOG same date); latent on machines where find_app misses (resolution only reaches the scan stage then). Matches now yield per-pattern in caller order, newest install dir first within each pattern, duplicates skipped — mirroring resolve_app_path's own first-hit-wins staging. Single-pattern callers (Maya / Blender / Toolbag bridges) are unaffected. Regression-locked (test_bridge.py::test_scan_glob_pattern_order_is_priority + test_scan_glob_priority_still_prefers_newest_within_pattern); full suite green (1723).

  • 2026-07-10 — Prep primitives, second-pass review fixes. ImageCurator._cluster with hash_threshold <= 0 now disables clustering outright: Hamming-0 matching still merged bit-identical dHashes (routine for consecutive frames of a paused camera on a thumbnail-sized hash) and kept one per cluster, silently contradicting the "0 = no dedup, keep all" contract every consumer documents (regression test: three byte-identical frames all survive at 0; the representatives-percentile test repinned to hash_threshold=1, which preserves its intent under the new contract). Scan resize height clamped ≥1 (an extreme panorama strip — width > 1024×height — rounded to height 0 and cv2.resize aborted the whole scan). ExposureEqualizer._save_image counts EXIF-read failures in last_fallback_count and logs the first (frames were silently written EXIF-less and uncounted); the per-capture sigma guard is hoisted out of the per-image loop.

  • 2026-07-10 — PresetStore writes are now atomic (FileUtils.atomic_write_text): preset saves and the .active sidecar can no longer be torn by a concurrent reader or corrupted by a crash mid-write. save() and the active setter used plain write_text/json.dump — another process reading at the wrong moment (e.g. a DCC session applying its startup preset while a second session saves; newly relevant since tentacle now applies the active macro preset at every launch, see tentacle CHANGELOG same date) could parse a partial file, and a crash mid-save permanently truncated the user's preset. Both now route through a module-level _atomic_write_text helper delegating to the existing FileUtils.atomic_write_text (temp file in the same dir + fsync + os.replace; imported lazily — file_utils imports from core_utils, so a module-level import would risk a cycle). Failure modes preserved: atomic_write_text re-raises after temp cleanup and its failures are OSError subclasses, so save() still propagates and the active setter's swallow-and-log contract is unchanged; a torn read was already handled gracefully by consumers (ValueError → fall back / skip), so this closes the write side. No API change. Pinned by a new PresetStoreAtomicWriteTest pair (failed save leaves the existing preset readable + no temp litter; failed .active write keeps the prior pointer) — both verified to FAIL against the old plain-write_text path, so a silent revert can't pass. Full suite green (1720 tests); uitk's test_preset_manager.py (76) green against the changed store.

  • 2026-07-10 — Photogrammetry prep primitives: quality-regression fixes (ImageCurator, ExposureEqualizer, ImgUtils.unique_dir_stems). Audit of the extapps photogrammetry pipeline traced several "results got worse" mechanisms into these classes. ImageCurator: the scan thumbnail was decimated 4K→256px with default INTER_LINEAR — the aliasing fed pseudo-random high-frequency energy into the Laplacian-variance sharpness ranking (and at 256px real 4K motion blur is sub-pixel, so blur culling was near-random); scans now use INTER_AREA at a 1024px analysis width (SCAN_WIDTH; absolute sharpness_floor values recalibrate — the percentile/median-relative floors are unaffected). curate() signature default hash_threshold 5→0 (dedup opt-in; extapps' profile drives production values). ExposureEqualizer: now computes one transform per capture directory by default instead of per-image (per-image normalization applied an unbounded ref_std/σ gain to dark/low-contrast frames — noise amplification exactly where SfM matching is weakest; legacy mode via per_image=True); per-source output dirs are purged before writing (overwrite_output=True, mirroring the curator — without it a re-run after tighter curation left previously-equalized, since-culled frames in the set downstream ingests); np.rint before the uint8 cast (bare astype truncated ~0.5 LSB down per channel per pass); the PIL-save fallback to cv2.imwrite (which drops EXIF + the deliberately-unbaked Orientation tag) is now counted (last_fallback_count) and logged loudly instead of silent; TIFF writes warn once that EXIF/16-bit don't survive. Both classes key per-source output dirs by the new ImgUtils.unique_dir_stems (basename, parent-qualified on collision) — two captures like capA/images + capB/images previously mapped to one output stem, and the curator's purge deleted the first capture's just-copied files. Consumers: extapps photogrammetry workflows (see extapps CHANGELOG same date).

  • 2026-07-10 — NetUtils.is_port_bindable: the bind-vs-connect port-probe split, as a shared primitive. A hung (zombie) process can hold a TCP port bound but not listening: a connect probe (is_port_open) reads that as free, yet a new server's bind() still fails — anything launched on that port then waits out its whole startup timeout (this exact failure burned three consecutive mayatk test-runner launches on 2026-07-09). The new helper answers the launch-side question ("can a NEW listener bind this?") by actually binding; is_port_open remains the attach-side question ("is a service listening here?") — the docstrings now spell out which to use when. Consumers: mayatk.MayaConnection.get_available_port (refactored off its inline socket code) and extapps' PainterConnection.get_available_port (which had the same latent flaw). Tests: free-port + zombie-bind regression pair in test_net_utils.py (the zombie case also pins the semantic difference against is_port_open).

  • 2026-07-09 — Atlas primitives: ImgUtils.atlas_pixel_rects + ImgUtils.inset_atlas_rects; assemble_atlas refactored onto the shared pixel mapping (mayatk lightmap atlas-gutter support). compute_atlas_layout tiles the unit square exactly, which leaves zero spacing between neighboring rects — any mip level or bilinear tap near a rect edge bleeds into the next item, and a consumer that wanted to dilate the assembled atlas had to re-derive assemble_atlas's private UV→pixel rounding (and its vertical flip) to build a coverage mask, inviting off-by-one drift. Two new pure primitives close both gaps: atlas_pixel_rects(rects, size) is now the single source of truth for the normalized-rect → integer-pixel-rect mapping (flip + rounding); assemble_atlas itself places content through it, so a mask built from it is exact by construction. inset_atlas_rects(rects, size, gutter) shrinks each rect by a pixel gutter per side (per-axis inset capped at a quarter of the rect's extent, so tiny rects never lose more than half and degenerate rects pass through unchanged) — the returned rects remain valid scaleOffset values for sampling the inset content, and the freed border is the consumer's dilation margin. First consumer: mayatk's LightmapBaker.pack_atlas (inset + exact-mask dilate_image gutter fill; see mayatk CHANGELOG same date). Tests: test_img.py +7 (AtlasPixelRectsTest: full-canvas/flip/size-tuple/shared-edge-no-gap; InsetAtlasRectsTest: gutter-freed-all-sides/tiny-rect-protection/inset-stays-inside), full img suite green.

Don't miss a new pythontk release

NewReleases is sending notifications on new releases.