github m3trik/pythontk v0.9.37
pythontk v0.9.37

5 hours ago
  • 2026-09-08 -- the constant-channel compaction is now part of every conversion, and stopped leaking one accessor a run (file_utils/mesh_convert/_mesh_convert.py, export_verify.py, net_utils/preview/viewer.html). Review pass over the day's GLB work. Wired: MeshConvert.compact_glb_animations runs inside fbx_to_glb's session after prune_glb_animations -- its own docstring already described that position, and a byte-exact saving that only an ad-hoc script performs is exactly the export/preview drift GlbPipeline exists to close. drop_glb_texture_fallbacks deliberately stays unwired: the pipeline already owns that decision one step earlier through optimize_glb_textures(ktx2_fallback=False), whose policy note says why it belongs to the consumer, so the new pass is documented as the RETROFIT for a finished file. The leak: a refusal is a property of the SAMPLER, so an output one clip accepted can be refused by a later clip's CUBICSPLINE -- and a clip left with nothing to collapse still minted a two-key time input no sampler would read. An accessor keeps its bufferView alive, so _compact_bin could not reclaim it and every re-run added another; measured on the new fixture at 5 accessors where 4 are live. Fail loudly: fbx_to_glb validates clip_mode before converting -- the rebuild runs under a warn-and-continue guard, so a typo caught only there downgraded silently to shipping Maya's lossy split takes. Gate: check_glb_images no longer WARNs about a missing PNG fallback when the file declares KHR_texture_basisu REQUIRED -- that is the shape drop_glb_texture_fallbacks produces on purpose, and warning on a declared contract every run trains the reader past the gate that would name a real defect. A truncated BIN is now declined, not rewritten short: _compact_bin slices each surviving view out of the buffer, and slicing past the end yields a shorter string in SILENCE while the view keeps the byteLength it declared -- so a file that arrived truncated came out merely lying about its offsets, no longer detectable as damaged. It checks every survivor is fully backed before writing anything and otherwise reclaims nothing; wiring the compaction is what made this run on every conversion, so declining is the only safe answer. Viewer: buildSequence requires BOTH bounds of every declared shot. The manifest reads them with .get, so a take the carrier wrote without an end reaches the page as end_frame: null -- and Math.max coerces that to 0, so the SPAN stays right and nothing gives the fault away: the shot is placed at its own start offset inside a span that never accounted for it (7.0s of a 5.00s sequence on the fixture), where the scrubber cannot reach it while the picker counts it as a shot you can watch. A shot the page was not told the end of is one it cannot place. test_mesh_convert +4 -- the accessor leak and the short buffer both red first, plus the first coverage of the pass running inside an ALREADY-OPEN session, which is the form the conversion uses and the one that repacks the BIN under the other passes (it pins that an image, which names its bufferView directly rather than through an accessor, still survives). test_export_verify +2, test_preview_viewer_live +1 (also red first).

  • 2026-09-07 -- WebXR preview: a shots-only deliverable now plays and scrubs as one sequence (net_utils/preview/viewer.html). With Animation Clips: Shots Only the file ships no continuous clip -- deliberately, since it holds the same performance the shots do -- and the page had no way to watch the whole thing. It now synthesizes a FULL SEQUENCE entry, first in the picker and addressable through playClip, driven by one playhead across every shot. The shots are placed at their AUTHORED frames rather than end to end: on the production assembly they sit 15 frames apart, and concatenating them would run the sequence ~3 s short and put every shot at the wrong time. A gap holds the previous shot's last pose, as the whole-timeline clip did, and a declared shot that bakes no curve still counts toward the span -- dropping it would end the sequence early. The segment action is posed by ASSIGNING its time and never advanced by the mixer, so two actions cannot accumulate time at once. Offered only when the file ships no continuous clip of its own: two ways to watch the same thing, one a reconstruction, is a worse picker than one. setupAnimation now reports the FILE's clip count on the load event rather than the picker's row count, so a script reading it still learns what the deliverable carries. Verified against the real page in headless Edge (test_preview_viewer_live +6): the span, the pose inside the second shot, the gap hold, and the empty-shot tail are all read back off the model.

  • 2026-09-07 -- a GLB can now ship the shots, the whole sequence, or both -- and the clip rebuild stopped leaking the clips it replaces (file_utils/mesh_convert/_mesh_convert.py, glb_clips.py, glb_pipeline.py). apply_glb_clips takes a mode from the new ANIMATION_CLIP_MODES, threaded through fbx_to_glb and GlbPipeline.build as clip_mode. The two halves hold the SAME performance -- the shots are cut from the sequence -- so a consumer that plays one never reads the other; measured on VDATS_ASSEMBLY, shots saves 66.8 MB and full 43.8 MB, and composed with the two compaction passes below the deliverable goes 215.29 MB -> 92.04 MB (-57%). full cuts nothing but still renames and stamps the stack, because zero_frame is what every later pass reads to place a clip in time and it does not become unknowable just because nothing was cut. The leak: the rebuild REPLACES the animations it supersedes -- Maya's own lossy split takes among them -- but dropping a clip frees nothing on its own, since an accessor keeps its bufferView alive whether or not anything still reads the accessor. Every conversion therefore shipped the discarded split takes' payload, and re-running the pass added another full set of shots (+38.8 MB a run, measured). _release_animation_payload now drops the orphaned accessors, renumbers what survives and lets _compact_bin collect: a second run returns the file to EXACTLY its first-run size. Accessors are named by a dozen different keys rather than one, so collector and renumberer share a single _map_accessor_refs walk and cannot disagree about a site, and the pass refuses outright on an extension that may hold accessor indices of its own -- the same bail-whole contract as the image prune. test_mesh_convert +5.

  • 2026-09-07 -- StrUtils.to_legal_name is the one owner of the objectName rule (str_utils/_str_utils.py). Every non-alphanumeric becomes _, positionally: one character in, one out. Deliberately NOT sanitize, which lowercases, collapses runs and strips trailing separators -- that would make "a b" and "a b" the same objectName, and this result is an IDENTITY two sides derive INDEPENDENTLY and must agree on: the widget uitk builds from a definition (SwitchboardNameMixin.convert_to_legal_name) and the key a headless reader looks that widget up by (ExportProfile.widget_key). It lives on the generic string class because uitk cannot own it (pythontk is downstream of nothing) and a Scene-Exporter class is the wrong owner for a rule the whole switchboard depends on; both callers now delegate. test_str covers it, and test/surface_snapshot.json records the new published name.

  • 2026-09-07 -- GLB delivery: two new compaction passes cut a production deliverable 26% (file_utils/mesh_convert/_mesh_convert.py). Measured on VDATS_ASSEMBLY, 215.29 MB. MeshConvert.compact_glb_animations collapses every animation channel that never moves to two keys: a clip must PIN the pose of what it does not animate (a viewer playing clip B after clip A leaves any node B omits wherever A left it), which is why a baked export writes every node into every clip -- but pinning needs two keys, not one per frame. 13,187 of 22,654 channels held a single value throughout, costing 16.60 MB to say a node stood still. Byte-exact: a channel collapses only when every element has the same bit pattern, the two keys span the clip's own ends so no duration changes, and a CUBICSPLINE sampler is refused outright (three elements per key, so two keys is six elements, not two). MeshConvert.drop_glb_texture_fallbacks drops the PNG/JPEG twin of every texture that also ships KTX2 and moves KHR_texture_basisu into extensionsRequired: written as an OPTIONAL extension, every map shipped BOTH encodings, so the KTX2 pass was adding 40.66 MB to the texture budget it exists to cut (the export log read 75.1 MB -> 79.5 MB). That one is a delivery decision, not a cleanup -- the result needs a reader with the extension. Only the animation pass is wired into the conversion (fbx_to_glb, after prune_glb_animations): it is byte-exact, so it needs no dial and no caller can forget it. The fallback drop is NOT, and deliberately -- the pipeline already owns that choice one step earlier through optimize_glb_textures(ktx2_fallback=False), whose policy note says why it belongs to the consumer (the preview page wires a KTX2Loader and passes False; an exporter's deliverable must stay importable and passes True). It ships as the RETROFIT for a finished file whose consumer turned out to be known, which is how the 215.29 MB -> 92.04 MB figure above was measured: the two composed by hand in one open_glb session, in 1.8 s. New _compact_bin collects orphaned payload by reading liveness off the whole file, which prune_glb_unreferenced_textures now shares -- 82 lines of duplicated repack deleted, and it collects payload orphaned by any earlier pass rather than only what its own images read. The sweep walks EVERY bufferView key, not just accessors: images name their view directly, and an accessor-only sweep read all 75.78 MB of them as garbage while this was being sized. The glTF accessor-layout tables are now single-sourced on MeshConvert (ACCESSOR_COMPONENT_TYPES, ACCESSOR_TYPE_COUNT) and read by GlbReader, which had its own copy. test_mesh_convert +12.

  • 2026-09-07 -- deliverable verification: a new clip_origin gate catches clips cut against the wrong frame, and the gates now read the scene's frame rate (file_utils/mesh_convert/export_verify.py). Reported: a shipped GLB played its shots "overlapping previous shots". Every existing gate passed it -- each clip kept the LENGTH its take declared and no shot was missing, so clips_vs_takes and cross_clips had nothing to say; the clips were simply cut from the wrong PLACE. ExportVerifier.check_clip_origin compares the whole-timeline stack's carried length against the clip_span["*"] its clips were cut against: the stack's length is independent of the published origin, so the divergence IS visible in the bytes (an earlier note claimed no in-file check could see it). On the reported file: "stack carries 4201f but clips were cut against a 4114f span (161-4275)". Also fps now defaults to the rate the sidecar publishes rather than a hardcoded 30 -- TaskManager.verify_deliverables passes only paths, so every frame each gate quoted was scaled by the ratio on a 24 or 60 fps scene. _undeclared_clips is shared with clips_vs_takes so the two cannot disagree about which clip is the stack. test_export_verify 47.

  • 2026-09-07 -- shot store: gap locks survive the shots around them changing, and undo (core_utils/engines/shots/shot_model.py). Reported: "I locked all gaps and they automatically became unlocked a few operations later." A lock names its gap by the flanking shot ids, and nothing ever unlocked it -- the pair simply stopped being adjacent: a shot inserted into a locked gap left both gaps that replaced it unlocked, a shot deleted or merged from between two locked gaps took both pairs with it while the gap that took their place opened unlocked, and an undone delete re-created the shot without its locks (the boundary snapshot carried shots and the ledger only). _rekey_gap_locks() now runs after every define_shot / remove_shot: a still-adjacent pair stands; a pair whose shots both exist becomes every adjacent pair between them (an inserted or split-off shot inherits the lock on both sides); a pair that lost a shot becomes the gap its survivor now flanks on that side; one that lost both is dropped. _restore_point / _apply_boundary_snapshot carry locked_gaps, so undo and redo put back the locks of their moment. Topology events only -- never update_shot, whose one-shot-at-a-time ripples pass through transient orders that would re-key a lock onto the wrong gap. test_shots_core 148.

Don't miss a new pythontk release

NewReleases is sending notifications on new releases.