github m3trik/pythontk v0.23.0
pythontk v0.23.0

49 minutes ago
  • 2026-11-11 -- Fixed: a call failing while the next call restarts its session instance says why, instead of "cannot join thread before it is started" (core_utils/operation_catalog/session_operation_host.py _launch_bound, _log_tail). The new instance's log drain was published as _drain before it started, and the failing call, reading the log tail from its own thread, joined it in that window (Windows CI, intermittent). The drain is now started, then published, and the tail reads it once.
    • Test(s): red first, then green: test_session_operation_host.py new test_the_log_drain_is_published_only_once_it_runs (pins the order without the race: [True] before, [False] after); the module 38 passed; the racy test_a_call_waiting_when_its_instance_dies_runs_in_the_next_and_says_so 5 runs green.
  • 2026-11-11 -- Fixed: a texture PyMeshLab cannot read is said, not silently dropped (file_utils/mesh_ops.py MeshOps new _load_mesh, used by session and lods; _save_mesh). PyMeshLab swaps a texture it cannot read -- a missing file, or a JPEG on Linux, whose wheel ships no Qt image plug-ins -- for a blank dummy.png and says nothing, so a mesh op's output carried no texture with no word of it, and a save wrote that blank image beside the output. The load now warns (put the image beside it, or convert it to PNG), and the stand-in is never written. TestTextured's fixture texture is a PNG, so every platform runs the same checks (they failed on Linux CI: the JPEG never loaded).
    • Test(s): red first, then green: test_mesh_ops.py new test_a_texture_it_cannot_read_is_said; the module 49 passed on Windows and on Linux (WSL Ubuntu, the Linux wheel), where test_a_save_elsewhere_takes_the_texture_along and test_another_image_of_the_same_name_is_kept_and_said failed before; affected tests 226 passed, extapps 67.
  • 2026-11-10 -- LOD pipeline end to end, and its full review (file_utils/lod_chain.py, file_utils/mesh_convert/{export_verify.py, fbx_media.py, _fbx2gltf.py, _gltf2fbx.py, _mesh_convert.py, glb/lods.py, glb/edit.py, glb/reader.py}, mesh_ops.py, core_utils/engines/scene_export/{scene_exporter.py, scene_records.py}, net_utils/preview/kernel/{lods.js, surfaces.js}).
    • New verification gates (ExportVerifier, run with the others):
      • check_fbx_lods: each group an FBX names states a LodGroup with one threshold per switch, writes no level hidden, and agrees with the lods record (groups, levels, heights).
      • check_glb_lod_deviation: each MSFT_lod level measured against its LOD0 in world space (MeshOps.compare), warned past 10% of LOD0's size; bounded (24 groups, 250k LOD0 triangles), "not measured" without [mesh].
    • LodChain:
      • A level whose world bounds stray from LOD0's is a note (it pops at its switch); nodes may carry bounds.
      • New record_groups, the lods record's entries both hosts publish.
      • groups, plan and refine_selection are linear: 3000 groups took 8 s to refine, 6000 under one parent 12 s to plan.
    • FBX LodGroups:
      • New FbxMedia.set_lod_thresholds, which states the heights on a LodGroup written without them.
      • New FbxMedia.lod_heights, its reader; GlbLods.fbx_heights moved there, unreleased.
      • New FbxMedia.lod_group_rows, the one row table both writers emit. The GLB-to-FBX writer wrote one DisplayLevels row per level; it now writes one per threshold, as a DCC's writer does.
      • Fixed: an empty Properties70 a writer left without its NULL sentinel gains one when it gains rows. A strict reader (a DCC's FBX importer) refused the rewritten file.
    • GLB:
      • GlbEdit.graft_meshes declares a copied material's extensions in extensionsUsed. Without them the Khronos validator failed the file.
      • GlbLods.merge grafts only the meshes a chain keeps, so no dead geometry is left.
      • A malformed MSFT_lod.ids naming an ancestor no longer loops GlbReader.world_matrix.
      • GlbLods.apply walks paths iteratively; 3000 nodes deep raised.
      • Rejected heights and an unreadable FBX threshold now warn.
    • Viewer:
      • A LOD group switches by its distance from the object, not from the group's origin, which a DCC's group leaves at identity. A mesh 20 m from its group showed the cull up close.
      • Walkable surfaces read each group's LOD0 alone.
    • MeshOps:
      • An image of the same name beside the output is the mesh's own only when it is the source's; another is kept and said.
      • lods frees LOD0 once written.
      • The session's last_decimation states what the collapse applied.
    • SceneRecords.named_nodes(skip=) reads a generator once.
    • New SceneExporterBase._state_lod_variants, the USD variant report both hosts gave.
    • Application names left the new prose.
    • Test(s):
      • test_lod_chain.py: LevelBoundsTest, RecordGroupsTest, CostTest.
      • test_glb_lods.py: CheckFbxLodsTest (7), LodDeviationTest (3), the threshold writer (red on the sentinel), a distance-threshold group, the ancestor-ids cycle, a 3000-deep tree, dead geometry, the material extension.
      • test_mesh_ops.py: the foreign image, last_decimation.
      • test_scene_records.py: a generator skip.
      • test_preview_viewer_live.py: test_a_lod_group_switches_by_its_distance_from_the_object (red: level 2 at 1.5 m).
      • Most written red first. The surfaces walk has no test: the page exposes no surface list.
  • 2026-11-10 -- LOD review fixes (file_utils/lod_chain.py, file_utils/mesh_convert/glb/lods.py, glb/edit.py, mesh_ops.py, core_utils/engines/scene_export/export_profile.py, net_utils/preview/kernel/lods.js, __init__.py). From the P6 review:
    • Level numbers are bounded. LodChain.groups checks a level number against the 8-level limit before sizing anything by it: x_LOD20000000 took 116 s and 2.5 GB, and a building model's Wall_LOD200 raised 300 warnings. Each is now one issue.
    • New LodChain.kept_heights. When a write ships only some of a group's levels, each kept level keeps its switch height and the last one keeps the group's cull. Both hosts' records and GlbLods.renumber use it; removing the last level used to raise the cull.
    • GlbLods.expand leaves a group past 8 levels as written. The GLB-to-FBX conversion used to fail on one.
    • GlbLods.merge works on a copy and replaces the destination only when the merge is whole, so a refusal writes nothing. A skinned node keeps its one mesh.
    • MeshOps.lods validates its ladder before it writes anything.
    • Textured decimation names the options its UV-keeping collapse cannot apply.
    • ExportRun.LOD_MODES is now LodChain.MODES.
    • New root exports: LodsRecord and LodGroupEntry.
    • Task order: prepare_lods runs after the name repair, so its plan never holds a path the repair renamed.
    • The viewer retunes each LOD's switch distances to the camera that renders the frame. A start camera or a headset sets its own lens, and the fov was read once at load.
    • Wording: the docs name no application (check_layers rule 6).
    • Test(s):
      • test_lod_chain.py: BoundsTest, test_kept_levels_keep_their_own_switch_and_the_groups_cull.
      • test_glb_lods.py: the last-level cull, the level limit in expand, merge refusals that write nothing (including a textured level), a skinned node.
      • test_mesh_ops.py: test_a_bad_ladder_is_refused_before_anything_is_written.
      • test_export_profile.py: the mode tie and the new task order.
      • test_export_snapshot.py: lods among the export-scoped records, missed in P4.
      • test_preview_viewer_live.py: the switches follow the rendering lens.
      • All written red first.
  • 2026-11-10 -- Fixed: decimating a textured mesh keeps its UVs, and a save beside the source leaves its texture alone (file_utils/mesh_ops.py new _MeshSession.decimate and the decimate_quadric_texture op; MeshOps.decimate, MeshOps.clean and the session's save). PyMeshLab's plain quadric collapse ignores UV seams: on a seamed atlas it left 6% of the UV area (the texture smeared across every face). A mesh with UVs and a texture now collapses with the texture-aware filter (94% of the UV area, no flipped face, measured), and a mesh with UVs but no texture warns that they will not survive and how to keep them (that filter needs every face to name a texture). A MeshLab save writes the mesh's textures beside the output, so a stage writing beside its source re-encoded the source's own JPEG each time; the save now leaves an image already there as it is (written elsewhere, the textures still go along). MeshOps.decimate with curvature_weighted or preserve_topology on a textured mesh drops those two: the texture-aware filter has neither.
    • Test(s): test_mesh_ops.py TestTextured (red first: the UV area at 6-59%, the source JPEG changed, no warning), 5 passed.
  • 2026-11-10 -- MeshOps.lods: a LOD chain of any mesh file, headless (file_utils/mesh_ops.py). <stem>_LOD0 (the mesh as it is) and each lower level <stem>_LOD<n> beside it, each a quadric decimation of LOD0 measured against it (compare's peak and mean). Coarseness is a ladder (levels, ratio: LodChain.ladder) or rising deviation budgets, met by bisecting each level's triangles on the measured peak: no simplifier's own error bound held in the LOD bake-off. A textured mesh keeps its UVs.
    • Test(s): test_mesh_ops.py TestLods (red first: no attribute), TestTextured.test_a_textured_lod_chain_keeps_its_uvs.
  • 2026-11-10 -- GLB to FBX carries LOD groups (file_utils/mesh_convert/glb/lods.py new GlbLods.expand; _gltf2fbx.py writes a LodGroup model and attribute, MSFT_lod among the understood extensions). The way back from GlbLods.apply: each MSFT_lod group becomes <base> over <base>_LOD0..n (another writer's group gets a <base> node in LOD0's place and the level names), and the FBX carries it as Maya writes it: a LodGroup model whose attribute states the switch heights in percent. A second instance's numbered tail goes on the group, its levels named after it (bolt_001_LOD0). Measured: Maya 2025 imports it as a lodGroup at the GLB's heights (60/30%), the same as its own export.
    • Test(s): test_glb_lods.py ExpandTest (red first), test_gltf2fbx.py test_a_glb_lod_group_lands_as_an_fbx_lod_group, test_a_glb_lod_group_without_heights_takes_the_default_ladder.
  • 2026-11-10 -- One LOD GLB from GLBs of one tree at falling detail, a CAD import's profiles among them (glb/lods.py new GlbLods.merge; glb/edit.py new GlbEdit.graft_meshes; _mesh_convert.py new MeshConvert.merge_glb_lods). Each mesh node of the finest GLB becomes a LOD group, its levels the matching nodes of the others that draw fewer triangles (a mesher's floor leaves a level out for that node), stated in MSFT_lod; instances keep sharing each level's mesh, and trees that differ are refused. graft_meshes copies meshes between GLBs with their accessors, bufferViews (each once, stride and all) and materials (an equal one shared). A CadImport record is a source of its own (MeshConvert.merge_glb_lods(CadImport.convert(step, profiles=("hero", "realtime")))): its levels are tessellations of the exact surfaces, not decimations. The Khronos validator passes the merged GLB with 0 errors and 0 warnings.
    • Test(s): test_glb_lods.py MergeTest (red first: no attribute), 5; test_cad_import.py test_the_profiles_merge_into_one_valid_lod_glb.
  • 2026-11-10 -- New record lods (core_utils/engines/scene_export/scene_records.py SceneRecords.LODS; file_utils/lod_record.py new LodsRecord / LodGroupEntry; the generated schema, DELIVERABLE_FORMAT.md and unitytk's LodsRecord.g.cs). Per LOD group its node, its levels (LOD0 first) and each level's switch height, for unitytk: Unity's FBX import merges every level-named mesh of a file into one LODGroup on its root and drops the FBX's thresholds (measured, Unity 6000.3). A GLB states LODs in MSFT_lod and USD in a variant set, so neither carries it. SceneRecords.named_nodes gained skip=: Generate Missing does not read the lods record an earlier export stored as a node it must leave alone.
    • Test(s): test_scene_records.py TestNamedNodes (skip).
  • 2026-11-10 -- LodChain.variant_plan: the USD variant set each LOD group becomes, as plain data a host's USD writer authors (file_utils/lod_chain.py, new USD_VARIANT_SET). A variant set LOD with variants LOD0..n (Unreal's interpret_lods), one level each, LOD0 selected; pxr stays in the hosts.
    • Test(s): test_lod_chain.py VariantPlanTest.
  • 2026-11-10 -- The WebXR preview switches a GLB's LOD groups as the camera moves (net_utils/preview/kernel/lods.js new; kernel/load.js, kernel/specs.js). three's GLTFLoader does not read MSFT_lod and draws LOD0 alone (the spec's fallback); the page now builds a THREE.LOD per group after the parse, its switch distances from the file's MSFT_screencoverage heights at the camera's field of view (the cull an empty last level), before every pass that binds by node. The model's triangle count is what is drawn up close: LOD0's.
    • Test(s): test_preview_viewer_live.py test_a_lod_group_switches_levels_with_the_camera (headless Edge: three levels and the cull, levels 0/1/cull by distance, LOD0's 64 triangles, no console error); the live viewer suites 440 passed (behaviour unchanged otherwise).
  • 2026-11-10 -- A GLB's readers and gates know MSFT_lod (file_utils/mesh_convert/glb/lods.py new GlbLods.parents; glb/reader.py GlbReader.parent_of; export_fingerprint.py node keys and a node's lod entry; export_verify.py new check_glb_lods). A group's lower levels, kept out of the node tree, read under their LOD0's parent (one parent map for the reader and the fingerprint), the fingerprint names each group's levels, and the verifier fails a group whose ids name no node, whose levels are still in the tree (every reader draws them) or whose heights do not fall level by level.
    • Test(s): test_glb_lods.py ReadersTest, VerifyTest (16 in all).
  • 2026-11-10 -- Generate Missing and LOD Output for the Scene Exporter's LODs row (export_profile.py LOD_OPTIONS gained Generate Missing, new LOD_OUTPUT_OPTIONS, ExportRun.lod_write_back, the generate_lods task after the name hygiene and in OBJECT_SET_TASKS; file_utils/lod_chain.py new LodChain.ladder and the generation defaults; scene_records.py new RecordSpec.named_nodes / SceneRecords.named_nodes). from_tasks arms generate_lods for Generate Missing alone; ladder gives each generated level's triangle target (half the last, stopping above a floor); named_nodes lists the scene names a record holds, which the generation leaves alone (a regrouped or renamed node would leave the record naming nothing).
    • Test(s): test_export_profile.py test_generate_missing_arms_the_generation_and_its_output_mode; test_lod_chain.py LadderTest, Generate Missing in PlanTest; test_scene_records.py TestNamedNodes.
  • 2026-11-10 -- A GLB states each LOD group in MSFT_lod (file_utils/mesh_convert/glb/lods.py new GlbLods; _mesh_convert.py new MeshConvert.apply_glb_lods; _fbx2gltf.py the pass in the FBX-to-GLB chain, and _remove_glb_nodes renumbers MSFT_lod ids). FBX2glTF writes a group's <base>_LOD0..n levels as sibling nodes, which every glTF reader drew at once. The pass gives each well-formed group's LOD0 node extensions.MSFT_lod.ids (the lower levels, out of the node tree) and extras.MSFT_screencoverage (each level's switch height, the cull last: the source FBX's LodGroup thresholds where it states them in percent, else the default ladder), listed in extensionsUsed only, so a reader without it draws LOD0 alone. It runs after every pass that binds by name in the node tree, and a later node removal keeps its ids valid (a removed level takes its id and its height). A malformed group is left as written. Measured end to end on a Maya FBX: its lodGroup (50/20%) and a Generate LOD Meshes group (64%) both came out stated, and the Khronos validator passed the GLB with 0 errors and 0 warnings. The pass chain is still hand-ordered, so this is one more placed step; the missing pass table is logged in FUTURE.md.
    • Test(s): test_glb_lods.py (new, red: no module), 10 passed.
  • 2026-11-10 -- The Scene Exporter reads the scene's LOD groups on every export, and a LODs row decides what ships (core_utils/engines/scene_export/export_profile.py new ExportProfile.LOD_OPTIONS, RETIRED_LOD_ROW, ExportRun.lods / LOD_MODES, the prepare_lods task in TASK_ORDER and OBJECT_SET_TASKS, check_geometry_lod_suffix out of CHECK_DEPENDENCIES; file_utils/lod_chain.py new LodChain.plan, refine_selection, names_levels, MODES). ExportRun.from_tasks pops the LODs mode (As Authored, the default, or Most Detailed Only) and arms prepare_lods with it on every export, as it arms the take split on every FBX write; a preset turning the retired Check Geometry LOD Suffix row off is told once that every export now reads the groups. LodChain.plan is the one decision both hosts apply (the valid groups, the levels the write leaves out, the report lines); a host only lists its nodes, after names_levels, a name scan that spares a scene with no level-named node any further work. refine_selection serves writers that carry each selected node's whole subtree (Maya's FBX and USD export): a selected node above a dropped level is replaced by its children, so Most Detailed Only holds in every format with no edit of the written file (measured: Maya's FBX export of a selected leaf writes its ancestors and their LOD group, never its siblings).
    • Test(s): test_export_profile.py test_the_lods_row_is_a_mode_that_arms_prepare_lods, test_the_lods_table_names_each_mode, test_prepare_lods_filters_the_export_set_before_any_check_reads_it, test_a_saved_lod_suffix_check_turned_off_is_said_once (red), six exact-task assertions gained prepare_lods, 81 passed; test_lod_chain.py PlanTest (5) and RefineSelectionTest (3), red: no such methods.
  • 2026-11-10 -- Added LodChain: the one level-of-detail convention every carrier writes (file_utils/lod_chain.py, root export). A LOD group is a node <base> whose children <base>_LOD0..n are its levels, most detailed first, at most 8: Unity's _LODX names, read in any case, with Maya's and Houdini's LOD_<n> containers read and noted (Unity reads neither). groups() reads plain node values into groups with their blocking issues (no LOD0, a gap, a level named twice, more than 8, triangles not falling, no node of their own) and informational notes; lone_levels(), lower_levels(); switch heights per level with the cull last (default_heights, check_heights), and FBX's percent thresholds (to_fbx_thresholds / from_fbx_thresholds). The base of the LOD pipeline (FBX LodGroup, glTF MSFT_lod, USD LOD variant set).
    • Test(s): test_lod_chain.py (new, red: no module), 22 passed.
  • 2026-11-10 -- A launched app no longer inherits a layered environment's packages (core_utils/handoff/app_handoff.py HandoffBridge.child_sys_path). uv run --with (how the MCP servers start) runs an ephemeral environment whose .pth files add the workspace venv and uv's cached archive, outside every prefix of the running Python, so they were handed to every instance the tooling launched, ahead of the host's own: an owned Blender 5.1 (Python 3.13) could not import its own pxr (DLL load failed on the venv's 3.11 build) and mayaUSD's modules died on the venv's USD 26.05. An entry inside a site-packages / dist-packages folder or a virtual environment (a folder holding pyvenv.cfg) is now dropped too; repo roots still pass. Tests: test_bridge 125 passed; the downstream bridge suites (uitk, mayatk, blendertk, tentacle) passed.
  • 2026-11-11 -- New: LightRig, the light rig engine both hosts bind: save, compare, swap and restore named rigs over a LightRigHost adapter, with PureLightRigHost (a scene in memory) as its default (core_utils/engines/lighting/light_rig.py; __init__.py DEFAULT_INCLUDE; light_rig_payload.py the payload's world is Y-up). mayatk's engine moved here unchanged: the canonical form, diff / compare / fingerprint, the light_rigs record, apply / swap back and their snapshot, and the library verbs. A binding sets HOST and its tables (HOST_TYPES, HOST_SETTINGS, HOST_FIXTURE_MODES, DIFFUSE_VISIBILITY, KELVIN_ATTRS); the neutral tables are built when the binding is defined, and one that leaves a contract name unmapped fails it. A neutral gobo is the host's (LightRigHost.gobo_filter); a light the host cannot build is reported in skipped.
    • Test(s): new test_light_rig.py (18: the binding's tables and their KeyError, kelvin and fixtures through a binding's names, save / applied / edited, apply and swap back toggling, swap back with nothing or several groups, an unbuildable light reported, the parent kept, carried sections, compare's defaults and override-only plugs, a follower yielding); test_light_rig_payload.py passes; surface_snapshot.json adds the three names.

Don't miss a new pythontk release

NewReleases is sending notifications on new releases.