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-wire order for structural parity. Offscreen suite 222/222. -
2026-07-25 — Reference Manager: fixed a live-Maya crash, made the Open icon a toggle, and enforced open/reference mutual exclusion. Live report. (1) Crash (Maya): the prior turn's
_wire_table_signalsre-wire used a blanketsignal.disconnect()(no args) oncustomContextMenuRequestedand the item delegate'scloseEditor— which strips the widget's OWN internal connections (the table wirescustomContextMenuRequested→_show_context_menuin its__init__; Qt wirescloseEditorfor the edit lifecycle), breaking the right-click menu and leaving a dangling editor on the nextclear(). Proven under the.venv(a preciseQMetaObject.Connectiondisconnect preserves the internal wiring; a no-arg one removes it). Both panels now re-wire through a new_rewire_signal(widget, signal, slot, key)helper that stores the per-(widget, key)Connectionand drops only the panel's own prior connection — still reload-safe (a fresh slots instance replaces the dead binding) without touching anything internal. (blendertk never hit the crash — it only re-wireditemDoubleClicked/itemChanged, which carry no internal connections — but is unified on the same helper.) (2) The Open icon is now a toggle (mirror of the reference icon): clicking it on the currently-open scene closes it — a new empty scene via the newbtk.new_scene()(Mayafile -new), guarded by an unsaved-changes confirmation — instead of re-opening. Works for foreign rows too: currentness is filepath-authoritative (_is_currentmatches a native row's own file, or a foreign row's deterministic_foreign_scratch_pathbake), so a second Open on a baked-open scene closes it. (3) Open and Reference are now mutually exclusive: opening a referenced file makes it the open scene — loading replaces the whole session, so the reference (a library link in the previous scene) is discarded for free, with no explicit un-reference (which would wrongly persist if the user then declined the unsaved-changes prompt); referencing the open scene first closes it (a scene can't be referenced into itself). (4) Fixed a pre-existing regression where_open_pathranimport bpyunconditionally at the top (crashing the.venvdegradation path before reaching the foreign branch) — the unsaved-changes guard is now the.venv-safe_confirm_discard_unsaved. Tests:test_reference_manager.py+1 (new_scene, real bpy — 60/60);test_blender_ui_handler.py+4 (toggle-close / open-drops-reference / reference-closes-current /.venvgraceful foreign open — 221/221). Mirrored to mayatk (controller.new_scene, the same slot helpers + toggle logic; live-Maya verification owed — no mayapy in this session). -
2026-07-25 — Reference Manager: opened Maya rows, flattened the context menu to Maya's, fixed the invisible Open icon, and expanded the Include Types row. Live report (blendertk panel). (1) Open on a foreign
.ma/.mb/.fbxrow now bakes the scene to a.blendand opens a throwaway copy as a new, unsaved file (_open_foreign_as_new→bake_maya_scene+ a scratch copy so the cached bake the link icon reuses is never edited) instead of the old "can't open" guard message. Because Open is now a real action on foreign rows, the Open icon is rendered in the neutral#555555(matching the reference column) rather than the near-invisible greyed-outunavailablestate — the reported "open icons are invisible in the neutral state" bug. (2) The row context menu was flattened to a 1:1 mirror of the mayatk panel — Open / Rename / Delete / Reference-Unreference / Unlink-and-Import / Open File Location — dropping the Blender-only extras (Append / Reload / Relocate / Remove / per-reference Display submenu; reference toggle + remove stay on the link icon, display stays on the display icon). The separate Import (convert) item is gone: Unlink and Import now covers both cases — make a linked reference's data local, or convert + import a foreign scene as local data. New consolidated handlerstoggle_reference_selected/unlink_import_selected; removedreference_selected/reload_selected/relocate_selected/make_local_selected/remove_selected/set_display/_import_foreign_selectedand the now-unused_DISPLAY_MODESconstant. (3) The header Include Types row now usesjustify="expand"(equal-width slots filling the menu width) instead ofjustify="between". (4) The foreign-origin(Maya)/(FBX)tag is no longer appended to table names — the extension already distinguishes them. Mirrored on the mayatk panel (foreign-row Open bakes + opens as a new scene, Import-convert fold, Include-Types expand,(Blender)tag removed);tentacle/docs/parity_map.pyre-ledgered andcompare_panel_surface.py --panel reference_manageris green (0 untriaged). -
2026-07-25 — Reference Manager: fixed duplicated context/header menus, a raw "File format is not supported" banner opening a foreign scene, and row actions going dead after a UI reload. Live report (blendertk panel). (1) The row context menu listed every action twice:
tbl000_initbuilds it underif not widget.is_initialized, but a nested reentrant call lands mid-build —_refresh_table_content→_workspace_dir()lazily registerscmb000, whosecmb000_init→_populate_workspaces()→_refresh()callstable.init_slot()again before the framework stampsis_initialized, so the menu was appended twice (traced and confirmed: 28 = 2×14 entries on a plain load). (2) The header menu's controls (Naming / Filter / Include Types) also duplicated across repeatheader_initcalls — a real, reproducible path (the offscreen test harness's own documented "drive*_initexplicitly" pattern re-invokes it). Bothtbl000_initandheader_initare now split the waychannels'tbl000_init/_wire_table_signalsalready establishes for this exact bug class: the one-time widget/menu construction stays guarded byis_initialized, while everything that binds toself(the row action-column handlers,itemDoubleClicked/itemChanged/itemSelectionChanged/customContextMenuRequested/the item-delegate'scloseEditor, everyregister_menu_actionhandler, andheader'srefresh_requested) now re-wires unconditionally on every call (idempotent — Qt signals disconnect-then-connect, the dict-keyed action-column/menu-action registries just overwrite). A blunt "build once, never again" guard on the whole method — my first attempt — would have left every row icon click and context-menu action bound to a dead priorselfafter any reload, the same class of bugchannels' own regression test exists to prevent; splitting build-vs-wire avoids that while still killing the duplicate-widget bug. (3) Row-menu Open on a foreign.ma/.mb/.fbxrow handed the path towm.open_mainfile; theRuntimeErrorwas caught but Blender still reported the error banner —_open_pathnow short-circuits a foreign path with a guard message (matching the Open icon's "unavailable" state) before touching bpy. (4) The header Include Types row now spans the full menu width (Menu.add_row(justify="between")). Regression tests intest_blender_ui_handler.py(+6, incl. deliberate repeat-header_init/tbl000_initidempotency + reload-rewire checks). Mirrored to mayatk (self.controlleris rebuilt fresh per slots instance, so the reload-rewire fix applies there too). -
2026-07-25 — RizomUV bridge phases 1/2/4 (1:1 mirror of the mayatk change). Lua files (
pack.lua,unwrap_hard.lua,unwrap_organic.lua, newunwrap_hybrid.lua, newtemplates/pack_block.lua) copied byte-identical from mayatk;parameters.pygains the sameexpand_includesinclude directive, inline@min/@max_rizom_linegating,PACK_SPACING/PACK_MARGIN/FIT_CONESparams, and corrected Pre-scale / Layout Scale enum labels (registry asserted identical to mayatk under the workspace venv — 26/26 entries, gating verified).process_with_rizomuv(skip_instances=True)collapses linked duplicates (objects sharing one mesh datablock — the Blender twin of shared-shape instances) to one representative per datablock before export. Live-Blender round-trip verification of the new presets/params is owed (the pure-logic gating + registry mirror are venv-verified). See mayatkdocs/rizom_bridge_upgrade_plan.md. -
2026-07-25 — RizomUV bridge mirror: target-UDIM + fractional-tile packing, pack-into-empty-space preset, preset-level version gating (1:1 mirror of the mayatk change).
scripts/pack.lua+ newscripts/pack_into_existing.luacopied byte-identical from mayatk (post-packZomDeformplacement tail; gap-fill viaZomPack WorkingSet="Visible&Selected"with scale/layout locks); registry gainsTARGET_UDIM+UV_AREA;Parameters.preset_min_versionparses the-- @min_rizomheader and both the panel combo andprocess_with_rizomuvenforce it (pack_into_existing needs RizomUV ≥ 2022.2).process_with_rizomuv(select_objects=)renders exported copy names into the selection token, andRizomUVBridge.expand_by_materials(bpy material-slot walk overbpy.data.objects) mirrors mayatk's material-defines-the-map expansion; the panel'sb000routes the preset through it. Registry mirror asserted against mayatk under the workspace venv (23/23 entries match). Live-Blender pass + the tentacle Pack option-box port (Tile Coverage / Pack Into Empty Space) are owed — ledgered intentacle/docs/parity_map.py; plan: mayatkdocs/rizom_bridge_upgrade_plan.md. -
2026-07-25 — Duplicate Grid: per-axis Spacing X/Y/Z with optional lock, dimension min 1, and reset on every field (1:1 mirror of the mayatk change). The uniform Spacing field becomes per-axis Spacing X/Y/Z, each under its Dimensions field, and each spacing field gets an option-box lock toggle (lock two or more → they move together by an equal delta). Dimension counts clamp to a minimum of 1 in the UI; the engine still accepts negative counts.
DuplicateGrid.duplicate_grid'sspacingnow takes a float (uniform) or an(sx, sy, sz)tuple via_normalize_spacing. The twin.uiis identical to mayatk's (parity sweep clean: 0 deltas).test_duplicate.py(+2: per-axis lattice, scalar↔uniform-tuple) — 23/23 PASS headless (Blender 5.1,--factory-startup). Rides the shared uitk reset-default +link_spinboxes+ option-box-wrap fixes (see uitk CHANGELOG). -
2026-07-25 —
restore_transformsgainschannels+traverse(mirror of the mtk un-freeze upgrade).channelsrestricts the restore to a subset of{"translate", "rotate", "scale"}; only the consumed channels' bakes are deleted, so unrestored bake history stays available for later calls (previouslydelete_attrs=Truedropped all three keys regardless).traverse=Truealso restores every descendant (children_recursive), parents first (depth-sorted), matchingmtk.restore_transforms(traverse=True). Maya's pivot-preservation half of the upgrade has no Blender analogue (the origin is the transform; world position was already preserved). Self-critique then caught a pre-existing hierarchy bug (probed headless: child world drifted +2 in x on a parented restore, 1.95 vertex drift under a rotated parent): freeze'stransform_applyfolds the applied basis into each child'smatrix_parent_inverseto keep children in place, and restore put the parent's basis back while that compensation remained — a double-apply. Restore now inverts it per direct child (pinv ← new_basis⁻¹ @ old_basis @ pinv), returning children to their exact pre-freeze relationship. Also:replace_with_instances'freeze_transformspre-clean now freezes location only (was location+scale defaults) — the true mirror of mtk (rotation/scale stay live per instance; baking source scale would rescale every target's adopted datablock). Pinned by new 7e/7f/7g checks intest_xform_utils.py(channel subset, traverse, rotated-parent world/vertex holds) — suite PASS headless (Blender 5.1,--factory-startup);test_duplicate21/21. -
2026-07-25 — Substance Painter reimport works end-to-end (1:1 mirror of the mayatk fix). Same four-part fix as mayatk (see its CHANGELOG for the full mechanics): (1) new vendored Painter-side
substance_rpcplugin (substance_rpc/plugin_src/substance_rpc,{op, kwargs}HTTP wire on 8090) withmesh.reload/project.info/js.evaluate/system.*ops, plusInstaller(Documents plugins folder,SUBSTANCE_PAINTER_PLUGINS_PATHhonored) auto-installed on send viaensure_rpc_plugin;PainterRpcClientrewritten onpythontk.RpcClient(JSON-RPC 2.0 envelope client removed). (2) Targeting:_preflightnarrows a defaulttarget="auto"to the template'sTARGET_INSTANCE(reimport can never silently launch a fresh Painter),_resolve_connectionprobes the default RPC port after a registry miss (finds a Painter across Blender restarts / user-launched), andSubstanceConnection.open()always wires the RPC client. (3) Export-path recording: every FBX export records its path as a scene custom property (substance_bridge_last_fbx, the Blender-idiomatic twin of mayatk'sfileInfo; saved with the .blend), and reimport (REUSE_RECORDED_EXPORT) overwrites the recorded path instead of re-deriving it. (4)NO_CONNECTION_HINT+RPC_OPStemplate fields (templates re-vendored verbatim from mayatk); an unreachable Painter degrades to the FBX overwrite + logged manual reload steps (delivered: False) instead of a dead-end error. Verified under the workspace venv (template parse, target narrowing, RPC_OPS rendering, substance_rpc imports); the transport/plugin/client suites live in mayatk (test_substance_rpc_plugin.py12/0 — identical vendored code). First-run caveat (a newly-installed Painter plugin may sit disabled until ticked once in Painter's Python menu — Python ▸ Reload Plugins Folder picks it up without a relaunch) is documented in a newsubstance_rpc/README.mdand consistently in the install log, RPC-timeout error, reimportNO_CONNECTION_HINT, and panel Help — mirror of mayatk's. Live-Painter reload verification owed alongside mayatk's. -
2026-07-25 — Mirror re-centers the result's origin when the operation calls for it (parity with mtk).
EditUtils.mirror/cut_along_axisgainedcenter_pivot=True: merge / symmetrize combine into one object, so its origin re-centers on the combined bounding box (origin_set BOUNDS= Maya'scenterPivots); in separate mode only the new_mirrorhalf is centered — the source object's origin is left untouched. Plain cuts (no mirror) are unaffected.center_pivot=Falseopts out.test_mirror_cut24/0 headless (--background). -
2026-07-20 — Reference Manager: a foreign row is now a real reference (bake → link), not an import-only row;
.fbxjoins the listable types. The cross-DCC follow-up tracked by the 2026-07-19 entry below. (1) Reference parity: the green import-only state is gone. A foreign (.ma/.mb/.fbx) row's link icon bakes the scene to a cached.blend— headless Maya → FBX → headless Blender →.blend, with an.fbxsource skipping the Maya hop (and its license checkout) entirely — and links that; clicking again removes the same library, and display overrides work. Only Open staysunavailable(a.macan't be opened as a.blend). A source row resolves to its library through a.source.jsonsidecar written beside every bake, so the round trip survives a session that did not perform the bake. "Import (convert)…" stays in the row menu as the local-copy path and now handles.fbxdirectly. (2).fbxis live:FOREIGN_EXTENSIONSgained.fbx(the inert placeholder from the previous entry), and rows are tagged(FBX)vs(Maya)so the origin — and whether a Maya install is needed — is obvious. (3) New converter API:MayaSceneImport.bake_scene()/btk.bake_maya_scene(),bake_source()for the inverse lookup, atemplates/_bake_scene.pyheadless-Blender template (empty factory scene → FBX import →save_as_mainfile) that replays the conversion's texture manifest through the existing_apply_texture_manifestengine rather than a second copy of it (the parent'ssys.pathis passed into the child, so blendertk is importable there), andfind_scenes(extensions=…). Conversion + bake cache independently on source and template identity, via the new sharedptk.CachedArtifact. (4) Icon states + tooltips now match mayatk word-for-word.test_blender_ui_handler.pyupdated (foreign rows assert native toggle states; new bake→link dispatch check); parity sweep 0 untriaged. -
2026-07-19 (later) — Reference Manager: header is now a strict 1:1 mirror of mayatk, footer relabeled, empty-state disambiguated, + a real folder-structure filter bug fixed. Supersedes the footer/header parts of the entry below. Follow-up to a live report — "the Blender panel shows nothing even with files present, and no icon buttons." Root cause was NOT a code bug: reproduced in a fresh headless Blender (Qt offscreen), the table + link/open/display icon columns + foreign listing all render (4 rows, non-null icons). The panel was correctly applying the user's active header preset
MF_reference, whoseFilter by Suffix = "_module"+Filter by Folder Structure = "{scenes}/modules/{name}"hide every non-matching file — no rows ⇒ no icons. Changes made: (1) Header mirrored exactly to mayatk — the orphan top-level Recursive toggle and the Blender-only Operations (New/Mark Workspace, Reload All) left the header; Recursive Search + New/Mark As Workspace now live on the Root Directory ▸ option box (matching Maya'schk000/b001/b006placement; Recursive defaults on like Maya), Operations is just Make Local All + Un-Reference All (Maya's Unlink-and-Import-All / Un-Reference-All; Convert-to-Assembly has no analogue), and Naming aligns to Maya's objectNames + labels (Save To Workspace,txt_subfolder_structurewith the{scenes}default). (2) Footer button relabeled "Un-Reference All" and built with the samefooter.add_widget(QPushButton(...))as Maya (wasadd_action_button("Remove All")). (3) Empty-state now distinguishes a filter-hidden list — "All N file(s) hidden by the active filter — check the header menu…" + footer "(N hidden by filter)" — from a genuinely empty folder (the misleading "No .blend files found" was what made a working-but-filtered panel read as broken). (4) Pre-existing bug fixed (the actual mechanism behind the report):_filter_by_folder_structurenever passedscenes=toreplace_placeholders, so a{scenes}/…pattern (now the header default) resolved to a literal and matched nothing — silently hiding every file; it now resolves the workspace'sscenerule (fallback"scenes") exactly likesave_scene_asand Maya's filter. Regression test added totest_reference_manager.py(45/45 under Blender 5.1). parity_map updated (chk000→chk_recursivereason; staletxt_subfolder_structurerename entry dropped — same objectName both sides now); header-mirror + option-box-relocation guards added totest_blender_ui_handler.py(211/211); parity sweep 0 untriaged. Cross-DCC reference-in (convert→link instead of import) is the tracked follow-up. -
2026-07-19 — Reference Manager: header regrouped 1:1 with mayatk, footer bulk-clear button restored, icon columns fixed, + cross-DCC Maya-scene import. Four items bringing the panel to mayatk parity. (1) Header reorg + tooltips: the naming fields (case / suffix / subfolder) and Save Scene now group under Naming:, the filter / hide / notes toggles under a distinct Filter / Display: separator; mayatk was reorganized the same way first and this mirrors it 1:1, tooltips harmonized. (2) Footer "Remove All" button added via
add_action_button(mirror of mayatk's footer "Un-Reference All" — Blender's per-reference removal isremove_library). (3) Icon columns were invisible —_refresh_table_contentoverrode the link/open/display action columns toResizeToContents, collapsing each icon-only column to ~0 px (and to nothing in the empty/placeholder state — the reported symptom "the table has no icon button columns" seen with no scenes loaded); removed the override so they keep the Fixed row-height-square sizingTableActions.add()/_reapply()applies, matching mayatk. (4) Cross-DCC import: a new Include Maya Scenes (.ma/.mb) header toggle (+ a row context "Import (convert)…") also lists the workspace's Maya scenes as import-only rows (import icon; open + displayunavailable; name tagged "(Maya)"); the import routes tobtk.import_maya_scene(the existing headless mayapy→FBX bridge the Scene menu already uses). Converter API extended withMayaSceneImport.find_scenes()(the discovery half of the import — reuses the sameSUPPORTED_EXTENSIONSthe importer accepts; delegates toptk.FileUtils.get_dir_contents). Regression tests added totest_blender_ui_handler.py(footer button, Fixed+visible icon columns, foreign listing + import dispatch); stale_LOG_LINK_HANDLERSimport in that suite fixed en route → 209/209. Registry regen deferred to a clean-tree pass. -
2026-07-19 — Channels panel: wheel-scrub / value-edit / MMB-scrub / context-menu were silently dead after a UI reload (parity fix — mirror of the mayatk panel). Live report: "the scroll wheel over a value does nothing." Root cause: the
tbl000QWidget can outlive theChannelsSlotsinstance (a UI reload / slots rebuild leavesis_initializedstamped on the persisted widget), yet the Blender port wired all its table signals —cellWheelScrolled,cellChanged,cellScrub{Started,Moved,Finished}, andcustomContextMenuRequested— inside the one-timeif not widget.is_initialized:block. On a rebuild that block is skipped, so those signals stayed bound to the dead instance's handlers and no-op'd (the exact bug mayatk had fixed and documented as "the root cause of 'edits don't set the attribute'" — the blendertk port omitted the fix). Fix: extracted_wire_table_signals(disconnect-then-connect, idempotent) and call it unconditionally on everytbl000_init(plus a best-effort first pass in__init__); the one-time block now holds only widget-state setup that persists across rebuilds (set_scrub_columns/set_wheel_scrub_columns/ action columns / context-menu build / scene subscription)._setup_value_inputreduced to column registration; the standalone_wire_context_menu_statefolded into_wire_table_signals. Regression added totest_blender_ui_handler.py— it drops all bindings then callstbl000_initon the already-initialized widget, so ONLY the unconditional re-wire (not__init__'s best-effort pass) can revive the wheel; proven red against the old wiring / green after. Every channels check passes. -
2026-07-19 — Marmoset bridge panel was completely broken (found via the channels regression run); wrong class instantiated.
marmoset_bridge_slots.pydidfrom ._marmoset_bridge import SEND_TO, _TEMPLATE_DIRbut_marmoset_bridge.py(unlike mayatk's) never re-exported those two constants from_marmoset_engine, so the slots module raisedImportErrorat discovery →MarmosetBridgeSlotsnever registered → the switchboard's file-name fallback resolved the panel to the engineMarmosetBridgeinstead, whose__init__rejectsswitchboard=(panel dead:TypeError: MarmosetBridge.__init__() got an unexpected keyword argument 'switchboard'). Fix: added theSEND_TO/_TEMPLATE_DIRre-export block to_marmoset_bridge.py, verbatim mirror of mayatk's.test_blender_ui_handler.pynow 209/209 (marmoset wiresMarmosetBridgeSlots; the earlierreference_managermisses were offscreen-QPA flakiness, not a regression). Private-module re-export only — no public-surface change. -
2026-07-19 — Migration note:
env_utils.script_outputmodule-level facades were removed. The encapsulation sweep (below) dropped the module-levelshow/hide/toggle/begin_capture/restorefunctions forScriptConsoleclassmethods — but external startup scripts call them too. Blender'sscripts/startup/tentacle_startup.pystill calledscript_output.begin_capture(), which now raisesAttributeError(swallowed by its try/except), so the session-wide capture never installed early and the console lost pre-console output (theimport tentaclegreeting banner). Fix: external callers must useScriptConsole.begin_capture()/ScriptConsole.restore()/ etc. (in-repo callers —tcl_blender, the editors slot — were already migrated). The sweep's "No public-surface change" held for in-repo consumers only. -
2026-07-19 — Full source-level encapsulation: no module-level functions. Completed the sweep so no module has root-level functions (except exec-script templates + the vendored marmoset plugin). Every remaining module-level helper was moved into a
_<Class>Internal(object)base the public class inherits (intra-module calls fully-qualified as_<Class>Internal._helper), and the stray public functions across the tool/engine/bridge/shots/plumbing modules (~55 modules incl.target_weld,_hierarchy_sync,_scene_import, the substance/marmoset bridges,_shots,bake_session,style_setter, and no-class config modules now wrapped inParameters/ToolbagLog/Installer/… classes) were relocated into their classes.env_utils.script_outputprocedural facades (show/hide/toggle/begin_capture/restore) becameScriptConsoleclassmethods (singleton instance methods renamed_-private) — no name collision, no module functions; consumers callScriptConsole.<action>(). Dead re-export imports removed (_anim_utilsno longer imports the unusedScaleKeys). No public-surface change. Full Blender suite 83/83. -
2026-07-19 — Encapsulation sweep: tool modules class-only in the init,
_*_utilsroots relocated into their class bodies. The co-located tool modules (env_utils.usd,edit_utils.duplicate_linear/_radial/_grid,edit_utils.curtain,edit_utils.target_weld,rig_utils.tube_rig, plus the earliercore_utils.diagnostics/env_utils.fbx_utils) are now class-only inDEFAULT_INCLUDE: the redundant flat function names were dropped so they no longer pollutebtk.*, matching mayatk's namespace layout. Consumer moves (all in-repo call sites migrated, incl. the tentacle Blender slots):btk.duplicate_linear→btk.DuplicateLinear.duplicate_linear(same for_radial/_grid);btk.create_curtain/btk.curtain_rail_from_selection→btk.CurtainUtils.<fn>;btk.target_weld→btk.TargetWeld.activate;btk.export_selection_usd/btk.import_usd→btk.UsdUtils.<fn>(redundant module-levelimport_usddeleted);register_strategy→TubeStrategy.register. Separately, everydef fn()+Class.fn = staticmethod(fn)(and the bare module-level publics) in the 14_*_utilsroots — 220 functions — was relocated into its*Utilsclass body as a real@staticmethod, and the 128 cross-modulefrom ..._X_utils import fnimporters rewritten toClass.fn(scope-aware, so local shadows are untouched). No public-surface change for the roots:btk.get_parentandbtk.NodeUtils.get_parentremain the same object under"*". Full Blender suite 81/81.