Releases: m3trik/blendertk
Release list
blendertk v0.5.48
-
2026-08-02 — Color ID's Outliner channel now sets the object's outliner TEXT color, like Maya's (
OutlinerTint), and Set Per Color becomes an opt-in on both DCCs. The previous day's mapping — link objects into a color-taggedID_<HEX>collection — was correct about Blender's published API but diverged from mayatk: Maya's channel tints the object's text, and Blender exposes no equivalent (probed live on 5.1.2: noObject.color_tag/outliner_color/use_outliner_color, andSpaceOutliner's whole RNA surface is filters and display toggles — no tree, rows, or expansion state). So the color is painted. Newdisplay_utils/outliner_tint.py: the color is stored as abtk_outliner_colorcustom property (ordinary data — saves with the .blend, readable without any of the below), and aSpaceOutlinerPOST_PIXELdraw handler walks the outliner's internalTreeElementtree through the space's struct pointer, masks each tinted row's label with the theme background, and repaints it in the object's color at Blender's own layout position (text_x = te->xs + 2*UI_UNIT_X + 4*ufac,text_y = te->ys + 5*ufac, fromoutliner_draw.c). Offsets were established empirically by planting known object names and matching the resolved IDs (te->subtree+0x20,xs+0x30,ys+0x34,store_elem+0x38 →TreeStoreElem.id+0x08 →ID.name+0x28, tree head atSpaceOutliner+0xC0).TreeElementis runtime state with no API-stability guarantee, so the whole subsystem is built to fail closed: the offsets are re-validated against the live process before the overlay is ever allowed to draw (the walk must resolve realbpy.datanames, else it stands down), every dereference passes an OS memory-protection query (_MemGuard, region-cached — a bad offset yieldsNone, never a segfault in the user's session), and any failure disables the overlay and leaves the outliner exactly as Blender drew it. A disabled overlay costs nothing and loses no data: the colors live on the objects. Non-Windows platforms keep the stored colors and skip the painting (is_supported). Set Per Color (chk016, new on BOTH twins, off by default) takes over the grouping the Outliner channel used to do —ID_<HEX>objectSet in Maya (ColorId.add_to_color_set/get_color_set_color/remove_from_color_sets, stampedmtk_color_id), color-tagged collection in Blender — as a grouping aid orthogonal to the four color channels. Both are stamp-keyed, never name-keyed, so a user's ownID_*set/collection is never adopted, retagged, or swept, and a recolor moves rather than piles up (emptied containers are garbage-collected; verified in mayapy that deleting an objectSet leaves its members alive). All four channels plus the new one now match mayatk by objectName, label, and capability —compare_panel_surface --panel color_id0 untriaged / 0 prop deltas. Three details the overlay gets right because getting them wrong is visible or unsafe: the mask uses the row background Blender actually drew — plain back, or the selection/active highlight, keyed on selected (an active-but-unselected row has no highlight, so keying on "is active" alone paints a box where Blender drew none); the font comes fromui_styles[0].widget.points, not a hardcoded 11, so a user's UI font size still lines up; and a stand-down from inside the draw callback defers thedraw_handler_removeto a timer rather than mutating Blender's handler list mid-iteration.test_color_id.py+13 checks (stored color round-trip, select-by,tinted_objects, reset, None-safety, and the enable/disable idempotency + "colors are readable regardless of the overlay" fail-closed contract) → 64/64; new GUI harnesstest/outliner_tint_gui_check.pycovers what--backgroundcannot (draw handlers never fire headless): live-process calibration reachingstatus == "ok", the walk resolving every planted object at distinct row positions, selected/active/unselected rows all masked correctly, a deliberately corrupted offset standing the overlay down with a recorded reason instead of crashing (colors intact), and a screenshot → 16/16. -
2026-08-02 — Maya bridge send: hierarchy arrived as locators, real materials arrived gray, and the launched Maya inherited Blender's OCIO. All three fixed (live-verified end to end).
- Parent Empties → plain Maya groups. Blender Empties travel as FBX nulls and Maya's importer gives EVERY null a locator shape, so a sent group tree arrived as locators all the way down. The
importtemplate now strips the locator shape from every imported transform whose Empty has children (a Blender Empty is a Maya group); a CHILDLESS Empty stays a locator — it marks a point, and a shapeless leaf transform would be invisible. Scoped to the import's new nodes (pre-existing user locators untouchable), skips multi-shape transforms. The import itself went deterministic while at it:FBXResetImport+ pinnedaddmode +returnNewNodes(mirror of the pull engine's_import_fbx— sticky global import options and the factorymergemode could retarget animation onto same-named scene nodes). - Native material rebuild via the pull direction's sidecar contract. Blender's FBX exporter only carries images wired (almost) directly into Principled sockets — packed ORM/MSAO through SeparateColor, AO multiplies, node-group plumbing export as NOTHING, so production materials (exactly what the Maya→Blender direction builds) arrived gray.
MayaBridge._producenow writes the same.manifest.jsonthe pull has always used (in-process collector: whole-node-tree walk incl. groups, UDIM flatten-to-first-tile, file-less entries for packed-only images so Maya warns BY NAME,scene_materialsfor the rename-on-clash guard; kept in step by hand with the dependency-free copy in mayatk's pull template), and the import template replays it through the ONE existing applier,mayatk.BlenderSceneImport._apply_texture_manifest(GameShader → standardSurface, the established template→paired-engine contract from both_bake_scene.pytemplates) — guarded, so a Maya without mayatk keeps the FBX materials with a console line saying so. Skipped whenINCLUDE_MATERIALSis off. - Launch env. The launched Maya inherited Blender's whole environment; an
OCIOpointing at Blender's bundled v2.5 config fails Maya 2025's color-management init on every send ("This version of the OpenColorIO library (2.3.2) is not able to load that config version")._SPEC.launch_env→MayaBridge._launch_envstrips OCIO via the newptk.AppLauncher.handoff_envexactly when it points inside Blender's own install (root frombpy.app.binary_path; import-safe outside Blender); a studio config elsewhere inherits untouched. - Verified live: probe scene (
grp > sub > cube+ leaf Empty + a Mix-routed texture the FBX provably drops) exported through the real producer, imported in mayapy through the real rendered template — arrives asgrp/subplain groups,leaf_markerlocator, and a rebuiltstandardSurfacewith the texture wired.test_maya_bridge.py+10 → 25 (template wiring pins; manifest schema incl. shared-datablock merge, packed → file-less, clash guard; launch-env all three branches). scene_import (74) / unity_bridge (15) re-run green.
- Parent Empties → plain Maya groups. Blender Empties travel as FBX nulls and Maya's importer gives EVERY null a locator shape, so a sent group tree arrived as locators all the way down. The
-
2026-08-02 — Bridge hand-offs flattened the scene graph: the shared FBX export set was mesh-only, so every Empty was dropped and its children re-rooted.
fbx_utils._EXPORT_DEFAULTSpinnedobject_types={"MESH"}, and Blender's FBX exporter does not just omit an excluded type — it re-parents that object's children to the nearest included ancestor, i.e. to the root. Agrp > sub > meshchain therefore arrived in Maya (and Unity) as parentless meshes: total, silent loss of the scene graph on every send. Every other consumer had already discovered this and overridden it locally (Substance and Marmoset pin{"MESH","EMPTY"}, the Scene Exporter["EMPTY","ARMATURE","MESH"], tentacle's scene slot{"MESH","EMPTY","OTHER"}) — the bridges were the ones left on the broken default.EMPTYis now in the shared default (a Blender Empty is a Maya group; hierarchy is not an opt-in), andBlenderExportMixin._fbx_options— the producer behind the Maya and Unity bridges — widens it to{"MESH","EMPTY","ARMATURE","OTHER"}, covering the other two silent losses: skin deformation (the mirror of mayatk'sFBXExportSkins -v true) and curve/text/metaball geometry. Cameras and lights stay out; the hand-off ships assets. Verified live on 5.1.2 in both directions — the Blender→Maya round trip now returnsgrp > sub > meshintact, and the Maya→Blender pull (whose MEL flag battery was always correct) is unchanged.test_fbx_utils.py+3 (parenting round-trip through the defaults; the hand-off set; and aBlenderExportMixin._export_fbxround trip proving agrp > armature > skinned meshchain survives the real producer). Substance and Marmoset are untouched — their_producecallsexport_selection_fbxdirectly with their own options, so only the two DCC-to-DCC bridges read_fbx_options. bridges / maya_bridge / unity_bridge / scene_import / scene_exporter / emissive_groups / rizom suites re-run green. -
2026-08-02 — Workspace Editor review: a rule added in Nice Names view wrote the LABEL as the rule name, table edits died after a UI reload, and the root field acted on every keystroke. Companion to the mayatk half (see its CHANGELOG for the shared model work).
- Nice-name rule keys. A row added via Add New File Rule has no stored key, so
_key_atfell back to its cell text — and in Nice Names view the user is looking at labels, so they type one. "Render Data" went intoworkspace.melverbatim asworkspace -fr "Render Data" …, a rule name Maya does not recognize. New_key_for_labelreverse-mapsptk.RULE_NICE_NAMEScase-insensitively; an unknown label still passes through as a custom rule. - Signals re-wire on every
_init.tbl000's `itemChang...
- Nice-name rule keys. A row added via Add New File Rule has no stored key, so
blendertk v0.5.45
-
2026-08-01 — Exporter blind spots: duplicate-material merges are now VERIFIED, NLA/data-level animation reaches the bake range, and the selection funnel can no longer silently drop (or be killed by) unshippable objects. Three fixes, mirrors of mayatk's new two-phase duplicate design where one exists. (1)
MatUtils.find_materials_with_duplicate_textures(materials, strict=False, verify=True)is now two-phase (mirror ofmtk's): phase 1 fingerprints on(surface shader idname, {(surface socket, texture id)})— TEX_IMAGE nodes are collected through nested node groups (previously invisible) and attributed to the surface-shader input they feed, so one shared detail/normal map no longer merges two different materials (confirmed false positive:reassign_duplicate_materials(delete=True), the default-on exporter task, DELETED one of them); phase 2 (verify=True, default) pairwise-verifies each candidate against its group's keeper — same shader type, equal unlinked shader inputs (1e-5), and per-slot identical color space,ShaderNodeMappingplacement, and image CONTENT (size + partial hash via the new_texture_content_id, library-aware paths).reassign_duplicate_materials(..., verify=True)re-proves each group right before its destructive merge. (2)AnimUtils.get_animated_extent/has_nla_or_data_animation:_actionsreads onlyo.animation_data.action, so NLA-strip-only or shape-key/data-level animation was invisible while the FBX write bakes the evaluated scene —set_bake_animation_rangenow covers active-action fcurves ∪ non-muted NLA strip extents (scene time) ∪ data/shape-key fcurve ranges, andcheck_untied_keyframes/check_floating_point_keys/check_framerateWARN once per run ("NLA/data-level animation present — only validates active object actions") instead of staying vacuously green (edit tasks deliberately NOT extended to strips: strip actions may be shared/library-linked with their own frame mapping). (3) Export funnel content loss:FbxUtils.exportselected with unguardedselect_set— hidden objects silently vanished from the FBX and a view-layer-excluded member RAISED and killed the export; unselectable members are now collected + WARNED (count + names; newstrict=Trueraises instead), the selection restore tolerates a no-longer-selectable prior member,SceneExporterpre-filters the export set with an INFO log (the funnel warning is the backstop;check_hidden_geometryremains the failing gate), andexport_data_nodelinks a collection-hidden/excludeddata_exportcarrier to the scene root for the write (deferred unlink after) so its metadata ships regardless.test_mat_anim_utils.py+12 checks (socket identity, tiling/content/colorspace/settings near-misses each NOT duplicates, node-group texture seen, true duplicates merged, reassign's own verify gate) → 160/160;test_smart_bake.py+11 (NLA-only strip extent reaches the scene range, muted strips skipped, shape-key extent, once-per-run check advisory) → 138/138;test_scene_exporter.py+10 (funnel drop warning + strict raise, excluded member no longer fatal, pre-filter INFO + FBX content, excluded-collection carrier ships + root link removed) → 58/58. -
2026-08-01 — Texture checks become storage-aware (library / packed / UDIM), and the glb-only sidecar no longer rolls forward on a failed conversion. Three fixes from the exporter tasks/checks audit. (1)
_MatUtilsInternal._abspathis now library-aware: an image linked from a library .blend stores its//path relative to the LIBRARY file, so resolving withoutlibrary=img.librarypointed every linked texture at the wrong directory — false "missing" incheck_valid_paths/get_image_records, wrong duplicate fingerprints and size probes for every_abspathconsumer. (2) Packed + UDIM images in the three texture checks (mirrors mayatk's<UDIM>-collapse semantics): PACKED images are skipped bycheck_valid_paths,check_absolute_paths, andcheck_texture_file_size— the FBX embeds them from memory, so their (often stale, absolute) bookkeeping path never ships; TILED (UDIM) images — previously invisible tocheck_valid_paths(get_image_recordsis FILE-only, so a deleted tile set passed) — are validated by probing the first declared tile on disk (tiles[0].number, 1001 fallback), and size-gated at their largest existing tile via the new_MatUtilsInternal._udim_first_tile_path/_udim_tile_paths(previouslyos.path.getsizeon the raw token path raisedOSErrorinto a silentcontinue, letting multi-GB tile sets through unmeasured). (3)_write_exportwrites the scene-data sidecar only after the deliverable is confirmed: in glb-only mode it was written before the FBX→GLB conversion, so a failed conversion produced no deliverable but still rolled the hierarchy-diff baseline forward (same ordering bug as mayatk's).test_scene_exporter.py+10 checks (packed passes both path checks, tile-less UDIM fails / first-tile UDIM passes validity, largest-tile size gate, failed-glb-leaves-no-sidecar + success-writes-it) → 48/48;test_texture_path_editor.py+3 (a linked image resolves against its library and reports as existing) → 16/16. -
2026-08-01 —
Naming.renamecollapses separator residue after strip/replace formatting (mayatk parity). Stripping a token from'a__tok__tokB'left'a____B'; now collapsed to'a_B'via the newptk.collapse_delimiter_runs— an explicit__typed in the pattern is honored. Mirror of the mayatk fix root-caused from the VDATS mangled-shape-name incident (mayatk CHANGELOG 2026-08-01 has the full story; the uninstance scratch-token source is Maya-only, so only the rename half applies here). -
2026-08-01 — Scene Exporter: the smart_bake session is now actually RESTORED after export (first fix from the tasks/checks audit). The task stored
result.session_idbut nothing in production ever consumed it, so every export with Smart Bake on (default) permanently muted the user's constraints/drivers and left the baked Action in place — despite the task docstring's and tooltip's "restorable" contract.perform_export's outerfinallynow mirrors mayatk's:SmartBake.restore(session_id)on every exit path (successful write, failed check, raising task), restore-failure surfaced at WARNING with the session id (the session stays in the manifest for a manual retry), and exceptions never mask the export result. Also: a check-blocked export now warns that task edits (material cleanup, key snapping/tying, path rewrites) remain in the scene — checks run after tasks and there is no automatic rollback; andcheck_hidden_geometry's message + tooltip now state the actual Blender hazard: the selection-based funnel (use_selection=True) silently DROPS hidden meshes from the FBX — the old Maya-inherited message claimed they "will be exported", the exact inverse.test_smart_bake.py+10 exporter-level checks (_run_exporter_bake_restore_checks: success path and blocked-export path, both asserting the constraint unmutes and no session is left) → 127/127. -
2026-08-01 —
BlenderUiHandler+BlenderNativeMenusjoin the package namespace (mayatk parity). mayatk exposesmtk.MayaUiHandler/mtk.MayaNativeMenus; blendertk exposed neither, sotcl_blendercomposed its handler set withmtk.MayaUiHandler's twin spelled as a deep module path — the two DCC hosts reading differently for no reason, in a package whose whole contract is mirroring mayatk's names. Nowbtk.BlenderUiHandler/btk.BlenderNativeMenus, class-only like their Maya counterparts. -
2026-08-01 — Scene Exporter now refreshes every metadata producer at export:
FbxUtils.run_export_preparers+_KNOWN_PRODUCERS(mayatk parity), so a mesh deleted after the last bake can no longer ship a staledata_exportmanifest. From the DCC→Unity data_nodes architecture review. Blender has no before-FBX-export event, so authoring-time publishes were the only refresh —export_data_nodeclaimed "the carrier is already current at export", which scene edits since the last bake/commit quietly falsified. The task now runs the new producer registry (shadow / emissive_groups / lightmap —LightmapBaker.refresh_export_metadataadded as the public no-arg entry point, mirroring the other producers) via_refresh_scene_data_nodebefore folding the carrier in; producers are isolated and clear-on-empty, and non-Scene-Exporter export paths still ship the authoring-time state (documented as the remaining divergence from mayatk's session hook). Also:DataNodes._set_stringhonors its documented return contract (Nonewhen an empty value had nothing to clear — it returned the carrier name), anddocs/data_nodes.md's channel table gains theshadow_metadata/emissive_groupsrows it was missing (plus de-staled Shots wording).test_scene_exporter.py's carrier round-trip now authors a real lightmap marker and publishes through the producer instead of hand-stamping producer-owned state (which the refresh correctly regenerates away — that's the feature). Suites: emissive_groups 60/60, lightmap_baker 38/38, node_utils 58/58, shadow_rig 63/63, smart_bake 117/117, scene_exporter 38/38.
blendertk v0.5.42
-
2026-07-30 — no Blender analogue of mayatk's intermediate-shape freeze bug (documented, verified). Maya's
cmds.instanceshares every shape including the deformer's intermediate (orig) shape, which blocksmakeIdentityforever (see mayatk's CHANGELOG). Blender has no intermediate datablock — modifiers evaluate off the singleobj.data— sopreserved_instances/uninstancehave nothing equivalent to fork, and thedelete_historyopt-in the Maya twin needed is deliberately absent (delete_historywas already a documented no-op here). Suites re-verified unchanged:test_node_utils.py/test_xform_utils.pyPASS headless. -
2026-07-30 —
NodeUtils.preserved_instances+freeze_transforms(instance_strategy=...)(mayatk parity — see its CHANGELOG for the mechanism and the failure it fixes). Linked-data twin of the Maya context manager: a group is the objects sharing one datablock, the master's "fork" isdata.copy(), and restore re-points every sibling at the master's post-op data with its world matrix compensated (B = post_world⁻¹ @ pre_world, siblingW → W @ B⁻¹) — writingmatrix_worldabsorbs parents / parent-inverse / deltas in one assignment, so the Maya twin's pivot re-pinning and instance-edge surgery have no analogue here. The orphaned original datablock is removed and the fork takes over its name. Triage (skipped up front, reported, untouched): library-linked objects/data, and siblings whose transforms are driven (drivers, action fcurves, unmuted constraints — compact yes/no twin oftransform_diag._driving_connections).freeze_transforms— wheretransform_applyrefuses multi-user data outright, so "skip" was the only behavior — gains the sameinstance_strategy="skip"|"preserve"|"uninstance"(default unchanged).fix_non_orthogonal_axesdeliberately does not take the option (divergence documented in its docstring): the Blender fix writes only transform channels, never shared geometry, so linked duplicates were already fixed in place with their instancing intact.test_node_utils.py+11 checks,test_xform_utils.py+7; both suites PASS headless. -
2026-07-30 —
libpyside: Failed to disconnect … from signalconsole spam from the Reference Manager header and the Channels table (mayatk twins fixed identically). User-reported from a live session (reference_manager.py:246,refresh_requested()). Two defects, both in idempotent signal re-wiring — mandatory here because the QWidget outlives the slots instance across a reload. (1) Blanketsignal.disconnect()on a signal with nothing attached: PySide reports that throughwarnings.warn, not an exception, so the surroundingexcept (RuntimeError, TypeError)never suppressed it and every first call printed the line — the header'srefresh_requested, and channels' fourcellScrub*/cellWheelScrolledbindings (which, unlike thecellChanged/customContextMenuRequestedblankets beside them, have no owner but us). Both now drop only their own prior connection, tracked per (widget, key). (2) The tracked-disconnect primitive had the same hole:_rewire_signaldropped its storedQMetaObject.Connectionthrough the signal-instancedisconnect(), which expects a slot — handed a Connection it can't match (the reload case: PySide breaks the binding when the old slots instance is collected) it warns and returns rather than raising, so theexceptwas dead code and the warning would simply have come back wearing a Connection repr. Verified against live PySide6, then switched to the staticQObject.disconnect()— the API that actually takes a Connection — with a falsy (already-broken) Connection skipped outright.test_blender_ui_handler.py226/226 (its single-connection regression unchanged) with a warning-free run,test_channels.py56/56,test_reference_manager.py64/64. -
2026-07-30 — Maya-scene import robustness: tolerant scene open,
.mbdriver probe, namespace-safe translated shaders, and collision-safe visibility replay. Four hardenings of the pull-direction conversion, each closing a real production edge. (1) Tolerant open (both conversion templates):cmds.file(open=...)raisesRuntimeErrorover missing renderer plugins / unknown nodes even when the scene content loaded — routine for production scenes on a vanilla mayapy — and that killed the whole conversion;_open_scenenow verifies viacmds.file(q, sceneName)that the scene actually loaded and tolerates the noise (printed), while a genuinely failed open still raises. (2)scene_has_complex_animationnow answers for.mbtoo (was a blindFalse): node type names are stored as plain byte strings in the binary, so a chunked, overlap-safe token scan over the same_DRIVER_NODE_TYPES(+ the<node>_visibilitycurve name) gives binary scenes the same bake-vs-raw browser signal — heuristic by contract, since a false positive only costs an unneeded bake attempt (the Maya-side probe re-decides under"auto"). (3) Namespace-safe created nodes (both templates): a referenced scene's materials are namespaced (loadReferenceDepth="all"), andf"{mat}_fbxsafe"put the translated phong INSIDE that namespace — its exported name's colon then at the mercy of FBX name mangling, silently missing the Blender-side slot match;_ns_safeflattens colons so created phong/standardSurface/bump nodes live at root under names that round-trip verbatim. (4) Visibility collision handling:_collect_baked_visibilitykeyed the manifest by short name and silently last-writer-won on duplicates — and the Blender-side replay matches by short name, so ONE object's curve would land on BOTH; differing duplicates are now dropped loudly (identical ones merge), no keys beating wrong keys. Also: the four engine entry points expand~beside$VAR.test_scene_import.py+10 checks (incl. a behavioral run of the extracted collector against a stubcmds, and.mbscans with a chunk-boundary-straddling token), verified failing pre-fix via stashed-source run; suite PASS under both the venv and headless Blender 5.1. -
2026-07-30 —
MatUtils.find_unassigned(mayatk parity — see its CHANGELOG for the Maya-side subtlety). Objects carrying no material: no material slots at all, or every slot empty. Blender has the real state Maya has to synthesize — nothing here joins a default shading group — so there's noinclude_defaultaxis; objects that can't hold materials (empties, lights, cameras) are not reported, since they aren't unshaded geometry. Sameobjects=Nonepool convention and object-level scope asfind_by_mat_id, whose complement it is. Drives tentacle's materials "No Material" select mode.test_mat_anim_utils.py+6. -
2026-07-29 — RizomUV temp payloads move onto
ptk.TempArtifacts, and the no-save error diagnoses itself (mirrors mayatk — see its CHANGELOG for the diagnosis). This twin used the same two fixed names as the Maya bridge (rizomuv_exported.fbx/riz_uv_script.luain the system temp dir), so the two panels were racing each other for one script file — and RizomUV re-reads the-cfiscript after launch, so whichever run got overwritten exited 0 without ever reachingZomSave. The round-trip now allocates both payloads from a"scoped"store (clean run deletes them, a failure keeps them so the script can be opened in RizomUV's Script Editor) and the one-waysendfrom a"detached"one (its session may outlive us, so allocation age-sweeps instead of deleting) — replacing a hand-rolledtime.time_ns()tag thatTempArtifactsdocuments as not unique enough alone on Windows._no_save_diagnosisreplaces the old message's promise of a Lua traceback (RizomUV prints nothing in-cfimode) with the script/FBX paths — both rendered OS-native, they used to mix separators — a comparison of the on-disk script against what was written, and a pointer at RizomUV's Script Editor._release_temp_payloadsre-allocates on the next run, sincecleanupuntracks what it removed and a cached path would leak.test_rizom_construction.py+10 → 34/34. -
2026-07-29 —
m_framestep cycle + clip fitting (mayatk parity — see its CHANGELOG for the detail);CamUtilsgainsget_view_state/set_view_state/fit_camera_clipping. Same contract:m_frame(steps=2, adjust_clipping=True), first press frames, further presses withinFRAME_STEP_TIMEOUTstep in, the last returns the view exactly where it started, and a pause collapses the cycle to a plain there-and-back toggle (sharedptk.StepToggle). Blender-side differences: the ideal distance comes fromview3d.view_selecteditself (no per-element fit factors) with the step ramp applied as a dolly onview_distance, and the state snapshotted/restored is the RegionView3D (location / rotation / distance / perspective) plus the viewport's clip planes —SpaceView3D.clip_start/clip_endis what a free view actually clips against, not the camera dataadjust_camera_clippingwrites — so the fit widens whichever surface is doing the clipping (the lens while locked to a camera, else the viewport's planes) against that surface's own current values, and the snapshot carries the lens planes too or the widening would outlive the view it was made for. Smooth view is suppressed for the framing op, which otherwise animates the view over the next ~200 ms and would fight the step dolly.fit_camera_clippingmeasures depth fromview_location/view_rotation/view_distancerather thanrv3d.view_matrix, which is only refreshed at draw time and is still the previous view right after a framing op.test_macros.py+8,test_cam_utils.py+9. -
2026-07-29 — Shell Xform tri-state snap (off / grid / shell) +
UvUtils.get_neighbor_shell_bounds(mayatk parity — see its CHANGELOG for the detail). Same 3-stateActionOptioncycle and the sameptk.MathUtils.next_clear_offsetgeometry, so the two panels behave identically. The pool is every mesh whos...
blendertk v0.5.39
-
2026-07-29 — Scene-data sidecar (mirrors mayatk's rename, plus the exporter-side write Blender never had).
HierarchySidecar→SceneDataSidecar(scene_data_sidecar.py, registered asbtk.SceneDataSidecar; old module = import shim): the manifest is now format v2 — hierarchy diff baseline +data_exportchannel snapshot in one.{stem}.scene_data.json, withread_data()and themigrate_legacy()promotion of v1-named sidecars (mayatk's changelog has the full design rationale). New here: the SceneExporter actually writes the sidecar post-export. mayatk maintained its manifest via the hierarchy check's task-manager hook, but blendertk's exporter had no sidecar wiring at all — the check is still a disabled placeholder (TODO(blender-parity), tooltip updated to say precisely that). The write lives on the engine (_write_scene_data_sidecar, same reason as_create_glb: blendertk'sTaskManagercarries noexport_path), is gated on the carrier having actually shipped (present in the export set — a hidden carrier excluded from a selected-mode export records nothing) or an existing manifest to maintain, and is best-effort — the record can never break the export it records. The engine's native_VERSION_SUFFIX_REre-implementation (its comment claimed the sidecar class "lives in the unported hierarchy_sync subsystem" — stale since the sidecar port) is deleted in favor ofSceneDataSidecar.VERSION_SUFFIX_RE.test_scene_data_sidecar.py(wastest_hierarchy_sidecar.py) 43/43 headless, incl. v2-format / v1-compat / name-migration checks. -
2026-07-29 — Scene Exporter's missing-file check no longer fails an export over textures that never ship (mirrors mayatk).
check_valid_pathswalked everyFILEimage datablock in the .blend — its docstring even called the whole-file scope deliberate, "matching Maya's version". The Maya version turned out to be the bug (see mayatk's entry: an HDR environment map and orphaned duplicate-material file nodes flooded a real export log), so both sides move to the export scope. Image scope is now_get_export_images()— the datablocks feeding the materials assigned toself.objects— which drops the World/Environment-Texture HDR and the zero-user images a duplicate-material cleanup leaves behind. Linked libraries stay whole-file: they are the analogue of Maya's scene references, not of a texture. The scoping is applied by filteringMatUtils.get_image_records()against the export set rather than re-deriving "is a FILE image whose abspath exists" locally — that predicate belongs toget_image_records, and a second copy of it in the check would be free to drift.test_scene_exporter.py+2 (an unassigned broken image is ignored; a missing texture on an export material still fails). -
2026-07-29 — The "blendertk panels get a hide button" rule is now a default the UI Browser can override (mirrors mayatk).
BlenderUiHandler.apply_styleshardcodedstyle["header_buttons"] = ("menu","collapse","hide")for anything taggedblendertk— a fixed tuple no user preference could outrank, and one of the three reasons the UI Browser's window-persistence setting had no effect (uitk's changelog has the diagnosis). Replaced by uitk's newdefault_persistence(ui)hook: blendertk-tagged UIs declarePERSISTENCE_STICKY, everything else falls through to the basePERSISTENCE_TRANSIENT— identical defaults, now at the bottom of a precedence chain the browser's global setting and per-window "Open as" override sit above. The gesture-scoped panels that ask for a pin button inheader_initare unaffected (the handler leaves a self-configured header alone unless there's an explicit choice, and then swaps onlypin/hide).test_blender_ui_handler.py+3 (default is sticky, an override outranks it,apply_stylesis no longer overridden). Also fixed a stale assertion in that suite (pre-existing, unrelated): it pinnedUnityBridgeSlots.MODE_LABELSto the single{copy: "Copy to Project"}entry from before the panel-native "Manage Unity Scripts" mode shipped — the mayatk twin carries the same two modes, so parity holds and the test, not the code, was wrong; it now asserts both modes plus the still-retired Unity Studio / existing-project ones. Suite back to 226/226. -
2026-07-29 — 25 panels declared fixed-vertical (mirrors mayatk): no more stretching into dead space above the footer. Same sweep and same classification as the mayatk entry — a panel is locked only when its trailing spacer is the sole vertical absorber. Those panels' spacers are now
sizeType=Fixedwith each group box declaringvsizetype="Fixed", making Qt's ownQLayout.maximumSize()finite rather than leaning on uitk's container see-through; seven untuned Qt Designer 20x40 spacers are tuned to the 10px norm (aFixedspacer's hint is its exact, unresizable height). Panels with real growable content stay resizable. Parity with mayatk is preserved panel-for-panel; verified through the real loader (finite native cap, ~10px footer gap, exact collapse/expand round-trips). -
2026-07-29 — Shell Xform gains
Gather to Tile, and its snap now respects the tile border padding (parity with mayatk).UvUtils.gather_to_udimmirrors the Maya twin: stray islands return to the target UDIM by whole-tile offsets that keep their sub-tile position, clamped inside the shared border margin, with the target decided by majority vote when no UDIM is given. Islands are resolved in two passes (vote, then apply) because the object-mode bmesh is freed when the edit callback returns, so island references must not escape it. The vote pass runs only when no UDIM is given, and the vote itself isptk.MathUtils.majority_tile. The move pad's grid snap lands islands one margin inside each grid line instead of on it._island_boundsfactored out of_island_bbox_centerbacks both. A partial selection moves the whole island — free here via_target_islands, and now asserted, because the Maya twin had to be fixed to match.
blendertk v0.5.36
- 2026-07-29 — The Unity Bridge offers to install the optional
unitytkinstead of failing to import, andCoreUtils.ensure_packagesgeneralises the Pillow provisioning._unity_bridge.pyimportedunitytkat module scope, so a user without the optional package hit an ImportError when the module was merely loaded — before the panel existed to explain it. The import is now deferred behindUnityBridge._deliverer_cls(), and the panel'smake_bridgecallsensure_optional_package("unitytk", feature="Unity Bridge")(see uitk's changelog) — prompt, install, proceed; decline and the panel simply reports no engine.list_delivery_modesbecame a classmethod so it resolves the deliverer the same lazy way.CoreUtils.ensure_packages(packages, add_to_path=True)is the general form ofensure_image_deps: same Blender provisioning policy (install into the per-version user-modules dir, already onsys.path, driven against Blender's bundled interpreter —sys.executableisblender.exeand cannot be pip'd) with no assumption about what is being installed.ensure_image_depskeeps its Pillow default and PIL-rebind and now delegates to it, so the shared body lives once; both are backed by_CoreUtilsInternal._ensure_packages.BlenderBridgeSlotsBaseoverrides_install_optional_packageto route through it — necessary because uitk's default ispip install --user, which would succeed and still be unimportable, Blender's user-site not being onsys.path. Verified:test_unity_bridge.py15/15,test_core_utils.py13/13, and the package still imports withunitytkblocked (only the Unity panel is affected).
blendertk v0.5.31
-
2026-07-28 — The Qt-only suites now actually run:
run_tests.pyre-runs a skipped suite under the workspace.venv, which immediately surfaced a brokentest_blender_native_menus. Headless Blender ships no Qt binding, so seven suites (test_blender_ui_handler,test_blender_native_menus,test_rizom_construction,test_hierarchy_tree_utils, the shot panel/slots harnesses) self-reported===RESULT: PASS=== (skipped)and the runner printedSKIPand moved on. They were documented as.venvtargets, but nothing executed them automatically — 287 checks that only ever ran when someone remembered to run them by hand.run_suitenow takes an optionalpython=, andrun()retries any skipped suite under.venv(labelled[venv]in the output), accumulating totals after the retry so the venv result is the one counted; a suite that skips in both environments still reports SKIP. Blender remains the default — the session-safety rule governs thebpysuites, and these carry nobpyimport. The gap was hiding a real failure:test_blender_native_menuspatchedblendertk.ui_utils.menu_harvest.refill_qmenu, a module-level target that stopped existing when the module migrated to theMenuHarvestclass (production correctly callsmenu_harvest.MenuHarvest.refill_qmenu).mock.patchresolves its target string at call time, so the stale name passed import and collection and only raisedAttributeErrorwhen the suite was finally executed — the same moved-symbol class the ecosystem has been bitten by before. Repointed tomock.patch.object(menu_harvest.MenuHarvest, "refill_qmenu"), which binds the attribute at patch time and so cannot silently rot again. -
2026-07-28 — Smart Bake spacer parity (see mayatk's CHANGELOG for the detail).
smart_bake.ui's trailingverticalSpacerflipped from the implicitExpandingtoFixedat height 0, matching the Maya SSoT, so the collapsed output group snaps flush to the footer instead of leaving a dead band. The underlyingadjust_height_bydouble-count lives in uitk and is fixed there for both DCCs. Measured through the realBlenderUiHandlerload (Qt-only, no bpy): collapsed gap 2 px in every state, window snapping to 254 withmaximumHeightlocked when collapsed and free when expanded — identical to the Maya panel.test/test_smart_bake.py14/14. -
2026-07-28 — Shell Xform move-pad scope + snap (mayatk parity — see its CHANGELOG for the detail).
gridMoveand the<customwidgets>block are lifted verbatim from the Maya SSoT, so the two move pads cannot drift by hand-editing;parity_mapneeded no new entry (sweep clean). NewUvUtils.get_uv_bounds(objects) -> (u_min, v_min, u_max, v_max)mirrors mayatk's, read off the active UV layer via_uv_readas a running min/max rather than collecting every loop UV — the move pad calls it per arrow press. The arrow slots now route through_move, which resolves the scope, reads the cached snap toggle, and callsptk.MathUtils.step_offset— identical logic to the Maya twin._snap_enableddeliberately does not import the Qt option-box internals: headless Blender has no Qt binding, andshell_xform_slot_check.pydrives_movethere (+9 checks: three scopes, opposite-arrow cancellation on both axes, Selection Bounds, snap on/off).test_uv_utils.py+3 (bounds order, bounds track a move, empty selection → None), and the slot check asserts the_MOVE_SCOPESshape now that the combo is built from it rather than from the.ui. -
2026-07-28 — Render Opacity's Fade Direction tooltip lost half a bullet to an unescaped
<."… ≥ 0.5 → fade out; if < 0.5 or no key → fade in."is handed to Qt, which auto-detects the surroundingfmt()output as rich text — so the bare<opened a tag and everything up to the next>(the bullet's own</li>) was swallowed: the Auto entry rendered as a truncated fragment. Now<, with a comment saying why. Caught by a static pass that evaluates everyfmt()call in the ecosystem and parses the result; all 224 literal tooltips across mayatk / blendertk / tentacle / extapps now come out well-formed (the pass also has to whitelist named entities like /—, which Qt renders but XML doesn't predefine). -
2026-07-28 — All 42 panels reach the tooltip DSL through
self.sb.tooltipinstead of importinguitk.widgets.mixins.tooltip_mixinby path (mirrors mayatk's sweep — see its CHANGELOG for the method and the verification). 41 modules here dropped the import, including the guardedtry/except ImportErrorform inarnold_bridgeandlightmap_bakerand theirif TooltipFormat is not None:branches. Mechanical + AST-verified; no behavior change, registries unaffected. -
2026-07-28 — Panel tooltip pass (mirrors mayatk's — see its CHANGELOG for the detail). Emissive Groups' 13 tooltips rebuilt through the new
self.sb.tooltipSwitchboard passthrough (uitk CHANGELOG), which also retires this module'stry/except ImportErrorTooltipFormatguard and theif TooltipFormat is not None:branch around the header help — the panel is a uitk slots class, so the guard could never fire. Same content fixes: the Encoding combo states the 4-group cap as a combo-level note (it comes from the manifest's RGBA slot channels, so it binds both encodings — not a vertex-color property), Remove Group bolds the retired-slot contract, Make Weights Keyable splits the two DCCs' curve transports into a section, Remove Keyable Weights drops the shoutyINCLUDING..uitext: Mirror'schk007Instance tooltip gains the disabled Bounding-Box-(center)-pivot restriction and names the bake path; Cut On Axis's four weighted-spacing fields (Spacing / Distribution / Bias / Curve) say what their values actually do, written from a measuredProgressionCurvesrun — which caught a first draft that had Bias backwards and mis-described Ease In-Out and Smooth Step. Thesmooth_step-ignores-Curve discrepancy intoggle_weight_uiis left standing and documented in mayatk's entry. Text and structure only — the files stay character-identical to their mayatk twins where the parity contract requires it (sweep: 0 deltas). -
2026-07-28 —
UvUtils.auto_unwrap+ a publictransfer_uvs(mayatk parity).auto_unwrap(objects, method="hard"|"organic", …)mirrors mayatk's: each mesh exports to OBJ, is unwrapped byptk.UvUnwrap(Ministry of Flat for hard surface, Boundary First Flattening for organic) and gets its UVs transferred back, inside one undo step with the prior mode/active/selection restored and every imported object plus orphaned mesh datablock removed. Per-object failures roll that mesh back viaget_uv_coords/set_uv_coordsand are reported rather than raised; the engine executable is resolved before the scene is touched, so a missing engine can't leave a half-run. Both engines preserve topology exactly, so the transfer is an exact per-loop copy and nothing is triangulated. Layout is per engine — Ministry of Flat's own island arrangement is kept and only scaled into the tile by the new data-level_fit_uvs_to_tile(no operator, so it runs headless), while BFF, which only flattens, gets the full_pack_shellspass.transfer_uvsis now public, closing a standing parity gap: the exact-copy /data_transferspatial-fallback pair logic the RizomUV bridge carried privately is now one implementation onUvUtils, and the bridge delegates to it (keeping only its logging). Verified against both real engines in Blender 5.1 (topology preserved, UVs inside 0-1, no leftover objects);test_uv_utils.pygreen, rizom round-trip green. -
2026-07-28 (parity batch) — Thirteen Maya-parity gaps closed in one sweep: every ledgered
pendingitem built (or decisively closed), plus three rolled subsystems Blender doesn't ship. New engines:SelectionOrder(ordered OBJECT selection — a depsgraph-diff click tracker +reorder_objectssort methods; Blender natively records component order only) backing the un-hidden Reorder Selection menu;CameraVisibility(per-camera exclusive/hidden isolate sets as camera id-props, derived fromscene.camerawith an exactly-reversible stash + optional msgbus auto-re-apply — Maya's SetExclusiveToCamera family) backing six new cameras-menu leaves; and theNurbsUtilsCV-deformation family (bend_curvearc-length-exact circular bend,curl_curve,scale_curvature,straighten_curve,rebuild_curveuniform arc-length CV redistribution,extend_curveend-tangent extension — pure control-point math; the probed SIMPLE_DEFORM route stays refuted) backing six formerly-dead nurbs list leaves. Engine upgrades:EditUtilsgainedpropagate_normals/conform_normals/extract_reversed_faces(Maya's five polyNormal modes now 1:1 in the normals menu),combine_objects/separate_objectsgaineduninstance=True(both linked-duplicate corruption hazards VERIFIED real headless — join mutates the sibling sharing the active's datablock; separate deletes sibling faces), andcut_along_axisgainedspacing/distribution/weight_bias/weight_curvevia the sharedptk.ProgressionCurves(linear default reproduces the old even-fill exactly).UvUtilsgainedscale_uvs(whole-map scale about a pivot) +transfer_uvs_to_similar(bbox-volume+vertex-count fan-out with TOPOLOGY data-transfer — the analogue of mtk's component-space transfer; linked duplicates of the source skipped).XformUtils.transfer_pivotgainedmirror=(reflect the source origin across a world axis-plane before the ORIGIN_CURSOR snap).TelescopeRiggainedaim_axis(signed x/y/z → Damped-Track enums + driven scale channel + off-axis locks, mirroring mtk's_resolve_axis) with the Aim Axis combo added to its.ui. Decisively closed, not built: Bake Inherited Visibility —Collection.hide_viewport/hide_renderare NOT animatable (is_animatable=False; keyframe_insert and driver_add both refuse, verified live 5.1), so no animated inher...
blendertk v0.5.28
- 2026-07-26 — Encapsulated
_mat_utils.py's 15 loose module-level helpers onto a_MatUtilsInternalbase (mirror of mayatk); closes a blendertk-vs-mayatk divergence.MatUtilsandMatUpdaternow inherit_MatUtilsInternal(the gold-standard_<Class>Internalpattern), so_is_gp_material/_abspath/_principled_node/_resolve_images/ … are@staticmethods reached as_MatUtilsInternal._x(...)rather than loose module functions. The cross-module importers (mat_manifest,render_opacity,scene_exporter/task_manager) and thetexture_path_editortest were updated to match; module constants (SHADER_TEMPLATES,_PBR_DIRECT, …) stay module-level. No behavior change — private helpers only, no public-surface change beyond the two class base annotations. Verified in fresh Blender 5.1:test_texture_path_editor(13/13),test_render_opacity(26/26),test_scene_exporter,test_mat_anim_utils— all PASS.
blendertk v0.5.26
- 2026-07-25 — BREAKING: removed the flat wrappers
btk.import_maya_scene,btk.bake_maya_scene, andbtk.auto_instance— call the classes instead (mirrors mayatk). Migration:btk.import_maya_scene(p, **kw)→btk.MayaSceneImport().import_scene(p, **kw);btk.bake_maya_scene(p, **kw)→btk.MayaSceneImport().bake_scene(p, **kw);btk.auto_instance(objs, **cfg)→btk.AutoInstancer.run_once(objs, **cfg)(new@classmethod, config via**config). Internal callers (reference_manager) + tests migrated — theui_handlerreference-routing mocks now patch the instance methods (MayaSceneImport.import_scene/bake_scene) rather than the deleted module functions. Verified in fresh Blender 5.1:test_auto_instancer,test_scene_import, andtest_blender_ui_handler(222/222), 0 failures.
blendertk v0.5.24
-
2026-07-25 — DCC-bridge panels: fixed parameter show/hide after the params-class refactor (mirrors mayatk). The Maya- and Unity-bridge slots returned the bare
parametersmodule asparams_module, butreferenced_keysmoved onto theParametersclass — so opening the panel or switching template raisedAttributeError. Now returnParameters(re-exposingPARAMSas a class attribute). The RizomUV visibility proxy was still calling the deleted module-levelreferenced_keys/strip_unsupported; it now routes throughParametersand expands includes before stripping. Headlesstest_rizom_roundtripnow skips→PASS when the Blender interpreter ships no Qt binding (the shareduitk.bridgeparam stack needs Qt) instead of hard-failing at import. -
2026-07-25 — Reference Manager: prompt Bake vs Import Raw when a foreign scene has driven animation. Follow-up to the smart-bake conversion below. Instead of silently deciding (
smart_bake="auto"), opening / referencing / importing a foreign.mawhose animation a raw round trip would lose now asks the user first — the conversion-time counterpart of the existing unsaved-changes confirmation: "«scene» has driven animation (constraints, set-driven keys, inherited visibility) that a raw import would lose. Bake it into keyframes? (No imports the raw contents.)" — Yes → bake (smart_bake=True), No → raw (False), Cancel → abort. The trigger is a newMayaSceneImport.scene_has_complex_animation(path)— a cheap text scan of the ASCII.mafor driver node types (constraints / expressions / IK / motion paths / set-drivenanimCurveU*) plus keyed visibility (Maya's<node>_visibilitycurve), early-exiting on the first hit, so the panel decides without launching Maya. It mirrors the Maya-side_detect_complex_animsignals (the authoritative check still runs during the actual bake). Scenes with no driven animation never prompt (fall through to"auto");.mb(binary, unscannable) and.fbx(already baked) also fall through —.mbstill bakes if the Maya side detects driven animation. Wired into all three foreign-conversion entry points (_open_foreign_as_new,_reference_foreign_paths,_import_foreign_paths), resolved per-scene before the wait cursor (so the modal shows a normal cursor) and dropping any cancelled scene from a multi-select batch. Blender-only (mayatk references.manatively — no conversion, no such choice). Tests:test_scene_import.py+7 (the scan across constraint / SDK / keyed-vis / plain-key / static /.fbx/ missing) andtest_reference_manager.py+4 (Yes/No/Cancel → True/False/None, static →"auto"no-prompt) — venv + Blender PASS. -
2026-07-25 — Maya→Blender conversion: optional SmartBake pre-pass + visibility sidecar recovers inherited visibility, the one animation the round trip actually lost.
MayaSceneImport.import_scene/bake_scene(and so the Reference Manager's "Open" of a foreign.ma/.mb) gained asmart_bakeoption — default"auto". Motivation (measured on a live 175 MB animated aircraft scene,C130_FCR_Speedrun_Assembly): the FBX route already bakes constraints / SDKs / IK / motion paths to transform channels viaFBXExportBakeComplexAnimation, and Blender imports those — but inherited visibility (a child mesh whose show/hide comes from an animated ancestor LOC — 48 such channels in that scene) is lost twice over: Maya's FBX exporter never writes a visibility curve for the child, and Blender's FBX importer drops visibility animation even when it is present (verified empirically — a directly-keyed node imports with nohide_*animation at all). So the fix has two parts: (1) a pre-pass that reuses mayatk's existingSmartBakeengine (nondestructive override layer;bake_inherited_visibilitysamples the effective ancestor×self visibility onto each child mesh) — not a reimplementation; and (2) because FBX→Blender can't carry visibility, the baked values travel in the conversion's existing.manifest.jsonsidecar — avisibilitysection beside thematerialsit already carries (one sidecar per FBX, not one per concern) — and are replayed Blender-side ashide_render/hide_viewportkeyframes with CONSTANT interpolation, through a shared_apply_visibility_manifestused by both the direct-import path and the.blendbake template — shifted by the FBX importer'sanim_offset(default 1.0) so the show/hide stays frame-aligned with the transforms (live-verified at 30 fps: transform + visibility both land on frames [2, 31] for Maya frames [1, 30], not desynced by one)."auto"runs only when a cheap dependency-free probe in the Maya template (_detect_complex_anim: any constraint / expression / IK / motion path / SDK curve / animated-visibility) detects driven animation, so static scenes pay ~0 and the core conversion stays Maya-modules-only;Trueforces it,Falseis the prior behaviour. mayatk (+ pythontk) is injected on the child mayapy'sPYTHONPATHby the driver only when active (_smart_bake_syspath, with an explicitmayatk_pathconstructor override); without mayatk the guarded import degrades to the plain bake.smart_bakeis in the conversion cache key, the manifest travels with the cached FBX (as before), and the template edits invalidate stale bakes. Proven end-to-end through the real bridge on a synthetic scene:import_scene(smart_bake=True)on a child whose visibility is inherited from a keyed parent yieldshide_render+hide_viewportkeys at the right frames with CONSTANT interpolation in Blender; without smart_bake it has none (transform + plain-key animation preserved either way — additive, non-regressive). FBX-only (inert for the USD route, which bakes visibility natively). Tests:test_scene_import.py+20 (template hygiene, per-mode render literals, path resolver incl. the namespace-package trap,PYTHONPATH-injection gating via the env-capturing_run_scriptseam, cache-key identity, merged-manifest visibility wiring across import/bake-template, and graceful no-bpy/missing-manifest degradation) — venv PASS. -
2026-07-25 — Reference Manager (mirror of mayatk): icon columns pinned right; NOTES moved to the middle. FILES + NOTES already stretched, but the icon action columns weren't pinned to the right edge (NOTES sat rightmost). NOTES is now moved to the visual middle via
header.moveSection(re-applied each populate, sincewidget.add/setColumnCount(0)resets the header's visual order), andsetStretchLastSection(False)guards the now-rightmost Display column from stretching. Visual-only — everyCOL_*logical index is unchanged. Live Blender confirmation owed (geometry-only). -
2026-07-25 — Test fix:
test_blender_ui_handler.pyaudio_clipstb001check asserted the wrong wiring. It expectedtb001.option_box.find_option(ActionOption), buttb001_initfolds its Reveal In Sequencer / Sync Scene Range actions into the option-box menu asQPushButtons (btn_reveal_sequencer/btn_sync_range) — mirroring Maya, which also uses menu rows, not registeredActionOptions.find_optiononly seesadd_option(...)registrations, so the check could never pass (stale since the tb001 menu refactor). Now checks the two menu buttons, matching the siblingcmb000check. Behaviour was correct all along; the assertion was wrong. -
2026-07-25 — Reference Manager (mirror of mayatk): the Folder Structure field's hover tooltip is now self-documenting and live. Hovering
txt_subfolder_structureshows the field's purpose, every supported placeholder with its meaning ({scenes}/{name}/{workspace}/{suffix}— listed even when the current pattern uses only some), each one's value resolved against the current workspace + open.blend, the real absolute save directory, worked examples, and a live{scene}-typo warning — built on the new uitkTooltipFormat.placeholder_preview+ pythontkStrUtils.resolve_placeholders(Qt imports deferred into the method, per the headless rule). The help text is folded into the bound tooltip becausetooltip.bindreplaces the staticsetToolTip. The{scenes}scene-rule lookup shared by the folder-structure filter and the preview is unified behind_scenes_folder(workspace). Panel parity unchanged (0 untriaged). Needs published pythontk + uitk. -
2026-07-25 — Option-box declutter (mirror of mayatk): folded standalone action icons into their existing dropdown menus. Reference Manager
txt000(Root Directory) now offers Set Directory… as the first row of its option menu instead of a separate folder icon (the pin/recent-directory button is untouched). Audio Clipscmb000folds its browse icon into the Clips menu as Add Clips…, andtb001(Move To Current Frame) — which previously had no menu, only two icons — now carries a menu with Reveal In Sequencer / Sync Scene Range, matching Maya's now-foldedtb001(parity fix). Behaviour unchanged; parity ledgered intentacle/docs/parity_map.py. -
2026-07-25 — Reference Manager:
tbl000_initconstruction now precedes_wire_table_signals(mirror of the mayatk crash fix). The mayatk panel's real launch-crash root cause was_wire_table_signals→TableActions.addsizing a column beforesetColumnCountcreated it — a native access violation on Qt 6.5 (root-caused viafaulthandlerin a self-driven fresh GUI Maya; the newer.venvQt tolerates it, which is why offscreen suites passed). This panel shared the hazard: it never callssetColumnCountat all (columns arrive later viaTableWidget.add), soactions.addalways ran against a 0-column table and only survived on Qt's tolerance. uitk'sTableActions._apply_sizingnow guards out-of-range columns at the source (sizing re-applies via_reapplywhenTableWidget.addbuilds the columns — see uitk CHANGELOG), and this panel'stbl000_initis reordered to match mayatk's construction-then-w...
blendertk v0.5.21
-
2026-07-18 —
anim_utils.shotsdead-surface removal (mirror of mayatk's shots improvement pass). Dropped the never-calledShotManifestController.browse_csv(browsing is wired exclusively through_browse_csv_option) and the never-passedprevent_overlapparam +_push_overlapping_objectsonShotSequencer.move_object_in_shot— grep-confirmed zero call sites in both packages; removed symmetrically so the parity surfaces stay matched.test_shot_sequencer.py65/65 andtest_shot_manifest.py24/24 under real Blender 5.1. Registry regenerated. -
2026-07-18 —
ShadowRig.delete_rigs()/.delete()— full rig teardown (mirror of mayatk's new API). Removes per plane: the plane + its*_shadow_grpempty (only when it holds nothing else — never a user's own parent), the contact empty — found via a newshadowContactID-pointer custom prop stamped at create (rename-proof; name-convention fallback for rigs built before it) — and the material + silhouette image datablocks once nothing else uses them (drivers die with their datablocks);delete_textures=Truealso removes the PNG from disk; theshadow_metadatachannel is republished (cleared when the last rig goes). Targets and the sharedshadow_sourceempty survive; works on live and baked rigs. Pre-existing bug fixed red-first en route:find_shadow_planesdouble-listed a plane when the selection overlapped (group + its plane child — the group'schildren_recursivere-lists it; proven['Box_shadow', 'Box_shadow']), which was a harmless no-op forbake_planesbut crasheddelete_rigswith aReferenceErroron the second, already-removed entry — now deduped viadict.fromkeys(mirror of mayatk) and hardened against stale refs, so a doubledelete()no-ops like Maya's.test_shadow_rig.py+10 checks (teardown set, target+source survival, PNG removal, channel clear, baked-rig teardown, overlapping-selection dedup + survival, stale-ref no-op) — 63/63 under real Blender 5.1. Registry regenerated. -
2026-07-18 —
ScriptJobManagermirror sync (mayatk hardening pass): shared-widget cleanup bug fixed,suppressed()context manager, visible listener errors. (1) Bug (same as mayatk's):connect_cleanupkeyed connected widgets byid(widget)alone, so a second owner registering cleanup on the same widget was silently dropped and its subscriptions leaked on widget destroy — now tracked per(widget, owner)pair. (2) Newsuppressed(*tokens)context manager (mirror of mayatk) replaces the manual suppress/try/finally/resume dance (e.g.render_opacity_slots); skipsNonetokens, preserves prior suppression on exit; suppression is counted (suppress increments / resume decrements, mirror of mayatk) so nested blocks compose. Documented divergence: resume is immediate here —bpy.app.handlersfire synchronously inside the mutating call, so everything raised in the block has already been silently dispatched by exit; mayatk defers resume to idle because Maya queues scriptJob dispatch until then. (3) Listener exceptions log at WARNING with traceback instead of DEBUG.test_script_job_manager.py+5 checks (suppressed silence/restore/prior-state, per-pair idempotence, both-owners cleanup) — PASS under real Blender 5.1. -
2026-07-18 — Workspace Editor UX polish (6-point pass) + tentacle Workspace-tab parity tweaks. Follow-up refinements to the Workspace Editor redesign. Editor panel: (1) the reset/remove action-icon columns are now pinned to the far right — LOCATION is a native
QHeaderView.Stretchsection (re-applied after every table rebuild, sinceadd()resets header state) so it fills the width instead of leaving dead space, RULE sizes to contents, the icon columns stay Fixed at row height. (2) The Set As Current header-menu button is gone — selecting/browsing to an existing project root (or building a fresh one with the first rule edit) now auto-pins it as the current workspace via a small_set_currenthelper called from_on_root_changed+_write(the root selection is the Set-Project action, so the button was redundant). (3) The panel now opens persistent —config_buttons(..., "hide")instead of"pin", so it shows and stays open like scene_exporter / texture_path_editor rather than auto-hiding on gesture-key release. tentacle Workspace tab (both DCCs, main.py): (4) the editor row is relabeled "Edit Workspace" (was "Workspace Editor"); (5) the current-workspace row is now a titledSeparator("Current Workspace")followed by the bare workspace-name row (was a"Current Workspace: <name>"text prefix) — verified theExpandableListaccepts aSeparatorinstance (mouse-transparent, non-interactive) offscreen; (6) the Blender dir-browser is now folders-only (dispatch guardos.path.exists→os.path.isdir), matching Maya's tree — the earlier files-too behavior is retired. Tests:test_blender_ui_handler.py+3 (hide-not-pin, LOCATION-stretch/icons-fixed, auto-set-current) → 201/201, pin cleared in the suite's finally;test_main_workspace{,_blender}.pyupdated for the label + separator + dirs-only needles (26 passed/3 skipped). Parity sweep 0, workspace suite 37/37 under real Blender 5.1. -
2026-07-18 (latest) —
StyleSetter.install()now purges the retiredDefaulttheme phantom. The pre-2026-07-05 backup/restore design left aDefault.xml(a byte-identical copy of the shipped Maya theme) plus itsDefault_Backup.jsonsidecar in Blender's per-userpresets/interface_themedir; becauselist_templates()scans that dir, the tentacle Themes combo showed a bogus "Default" entry that was really just Maya under another name.install()(run on combo init) now removes both — the.xmlonly when byte-identical to a shipped style, so a user's own theme legitimately named "Default" is untouched, and the.jsonsidecar unconditionally (Blender never writes.jsoninto a preset dir). Self-healing on upgrade; a fresh user never had them. Covered by 4 new checks intest_style_setter.py. -
2026-07-18 — The window-drag/resize/snap freeze is fixed FOR REAL in tentacle's Qt pump (posted-events-only pumping — see tentacle's CHANGELOG); this supersedes the entry below: the
QtDockmodal-suspend guard is REVERTED and the Win+Shift+Arrow workaround is obsolete. The guard answered a misdiagnosis: the livelock was tentacle'sprocessEventspump dispatching Blender's own native messages from nestedWM_TIMERcontexts (py-spy-proven; the newtentacle/test/blender/console_frame_resize_check.pyfroze on Win11 snap-release with NO console docked at all, same-monitor, 96→96 dpi), so un-embedding the child protected nothing — while costing real regressions: the console vanished for every drag's duration, the suspend/resume did Qt window-system calls (fromWinId/setParent/show) from bpy timers in exactly the hazardous nested contexts, and an unbalanced suspend (freeze or probe flicker skipping the resume) stranded the widget hidden over a bare Info Log area — the field report "the script_output window is just blank" after a resize. Removed:QtDock._modal_guard/_suspend_embed/_resume_embed/GUARD_INTERVALandBlenderWindow.modal_move_size_active(its sole caller); the child now simply stays embedded through native size/move loops — measured safe across edge drags, 8 ms resize storms, half-snap and snap-maximize releases with the fixed pump (frame harness all legs green on both monitors incl. a real-crossdrag-prehistory leg;console_dock_check34/34; cross-DPIconsole_dpi_drag_checkA0/A/B all green). The dead mid-session strip border bug below remains open and unrelated. Registry regen still deferred to a clean-tree pass (dropsmodal_move_size_activewhen it runs). -
2026-07-18 — [SUPERSEDED by the entry above — the guard shipped here is reverted; the root cause was tentacle's pump, not the DPI transition, and the child's
WM_DPICHANGED_AFTERPARENTtraffic was never the trigger.] Cross-DPI window-drag hard freeze root-caused (live report: "Blender freezes when resized");QtDocknow suspends the embed for any native size/move loop's duration. The user's freeze — title-drag the Blender window onto a monitor with a different scale factor and snap-maximize there → every bpy timer stops forever while the window still answersWM_NULL(the OS never flags Not Responding; kill required) — reproduces with a bareQApplicationand zero windows and NOT in factory Blender: an upstream-class livelock in the nested native message dispatch of a per-monitor-V2 DPI transition whenever the Qt runtime shares GHOST's thread (py-spy native stack: re-entrantwglSwapBuffers/wndproc layers with aprocessEvents → DispatchMessageWthat never returns). Deterministic minimal repro + regression sentinel:tentacle/test/blender/console_dpi_drag_check.py(its docstring records the seven mitigations tested and ruled out — DPI env vars,AA_PluginApplication, pump quarantine/bounding/InSendMessagegating); the programmatic path is proven clean byconsole_dpi_move_check.py, so Win+Shift+Arrow (or equal monitor scale factors) is the user workaround. Shipped defense-in-depth:QtDock._modal_guard(new 0.02 s poll on the newBlenderWindow.modal_move_size_active, cached-probe idiom) un-embeds the hosted child within ~190 ms of a window drag/resize starting — before a monitor crossing can happen (~250 ms+) — and re-embeds + re-glues on loop exit, keeping the child'sWM_DPICHANGED_AFTERPARENTtraffic out of the storm; Qt is pump-gated for the loop's duration anyway, so the hidden child costs nothing functional.console_dock_check.py34/34 green with the guard. Separate OPEN bug found en route (probes:test/temp_tests/dock_heal_check.py/dock_settle_variants_check.py; battery:tentacle/test/blender/console_resize_check.py, splitter legs expected-FAIL until fixed): adock_editorstrip docked MID-SESSION has a border Blender's interactive edge hit-t...