Skip to content

blendertk v0.5.24

Choose a tag to compare

@m3trik m3trik released this 26 Jul 02:13
  • 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 parameters module as params_module, but referenced_keys moved onto the Parameters class — so opening the panel or switching template raised AttributeError. Now return Parameters (re-exposing PARAMS as a class attribute). The RizomUV visibility proxy was still calling the deleted module-level referenced_keys/strip_unsupported; it now routes through Parameters and expands includes before stripping. Headless test_rizom_roundtrip now skips→PASS when the Blender interpreter ships no Qt binding (the shared uitk.bridge param 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 .ma whose 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 new MayaSceneImport.scene_has_complex_animation(path) — a cheap text scan of the ASCII .ma for driver node types (constraints / expressions / IK / motion paths / set-driven animCurveU*) plus keyed visibility (Maya's <node>_visibility curve), early-exiting on the first hit, so the panel decides without launching Maya. It mirrors the Maya-side _detect_complex_anim signals (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 — .mb still 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 .ma natively — no conversion, no such choice). Tests: test_scene_import.py +7 (the scan across constraint / SDK / keyed-vis / plain-key / static / .fbx / missing) and test_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 a smart_bake option — 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 via FBXExportBakeComplexAnimation, 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 no hide_* animation at all). So the fix has two parts: (1) a pre-pass that reuses mayatk's existing SmartBake engine (nondestructive override layer; bake_inherited_visibility samples 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.json sidecar — a visibility section beside the materials it already carries (one sidecar per FBX, not one per concern) — and are replayed Blender-side as hide_render/hide_viewport keyframes with CONSTANT interpolation, through a shared _apply_visibility_manifest used by both the direct-import path and the .blend bake template — shifted by the FBX importer's anim_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; True forces it, False is the prior behaviour. mayatk (+ pythontk) is injected on the child mayapy's PYTHONPATH by the driver only when active (_smart_bake_syspath, with an explicit mayatk_path constructor override); without mayatk the guarded import degrades to the plain bake. smart_bake is 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 yields hide_render+hide_viewport keys 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_script seam, 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, since widget.add/setColumnCount(0) resets the header's visual order), and setStretchLastSection(False) guards the now-rightmost Display column from stretching. Visual-only — every COL_* logical index is unchanged. Live Blender confirmation owed (geometry-only).

  • 2026-07-25 — Test fix: test_blender_ui_handler.py audio_clips tb001 check asserted the wrong wiring. It expected tb001.option_box.find_option(ActionOption), but tb001_init folds its Reveal In Sequencer / Sync Scene Range actions into the option-box menu as QPushButtons (btn_reveal_sequencer / btn_sync_range) — mirroring Maya, which also uses menu rows, not registered ActionOptions. find_option only sees add_option(...) registrations, so the check could never pass (stale since the tb001 menu refactor). Now checks the two menu buttons, matching the sibling cmb000 check. 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_structure shows 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 uitk TooltipFormat.placeholder_preview + pythontk StrUtils.resolve_placeholders (Qt imports deferred into the method, per the headless rule). The help text is folded into the bound tooltip because tooltip.bind replaces the static setToolTip. 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 Clips cmb000 folds its browse icon into the Clips menu as Add Clips…, and tb001 (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-folded tb001 (parity fix). Behaviour unchanged; parity ledgered in tentacle/docs/parity_map.py.

  • 2026-07-25 — Reference Manager: tbl000_init construction now precedes _wire_table_signals (mirror of the mayatk crash fix). The mayatk panel's real launch-crash root cause was _wire_table_signalsTableActions.add sizing a column before setColumnCount created it — a native access violation on Qt 6.5 (root-caused via faulthandler in a self-driven fresh GUI Maya; the newer .venv Qt tolerates it, which is why offscreen suites passed). This panel shared the hazard: it never calls setColumnCount at all (columns arrive later via TableWidget.add), so actions.add always ran against a 0-column table and only survived on Qt's tolerance. uitk's TableActions._apply_sizing now guards out-of-range columns at the source (sizing re-applies via _reapply when TableWidget.add builds the columns — see uitk CHANGELOG), and this panel's tbl000_init is 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_signals re-wire used a blanket signal.disconnect() (no args) on customContextMenuRequested and the item delegate's closeEditor — which strips the widget's OWN internal connections (the table wires customContextMenuRequested_show_context_menu in its __init__; Qt wires closeEditor for the edit lifecycle), breaking the right-click menu and leaving a dangling editor on the next clear(). Proven under the .venv (a precise QMetaObject.Connection disconnect 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) Connection and 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-wired itemDoubleClicked/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 new btk.new_scene() (Maya file -new), guarded by an unsaved-changes confirmation — instead of re-opening. Works for foreign rows too: currentness is filepath-authoritative (_is_current matches a native row's own file, or a foreign row's deterministic _foreign_scratch_path bake), 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_path ran import bpy unconditionally at the top (crashing the .venv degradation 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 / .venv graceful 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/.fbx row now bakes the scene to a .blend and opens a throwaway copy as a new, unsaved file (_open_foreign_as_newbake_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-out unavailable state — 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 handlers toggle_reference_selected / unlink_import_selected; removed reference_selected / reload_selected / relocate_selected / make_local_selected / remove_selected / set_display / _import_foreign_selected and the now-unused _DISPLAY_MODES constant. (3) The header Include Types row now uses justify="expand" (equal-width slots filling the menu width) instead of justify="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.py re-ledgered and compare_panel_surface.py --panel reference_manager is 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_init builds it under if not widget.is_initialized, but a nested reentrant call lands mid-build — _refresh_table_content_workspace_dir() lazily registers cmb000, whose cmb000_init_populate_workspaces()_refresh() calls table.init_slot() again before the framework stamps is_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 repeat header_init calls — a real, reproducible path (the offscreen test harness's own documented "drive *_init explicitly" pattern re-invokes it). Both tbl000_init and header_init are now split the way channels' tbl000_init/_wire_table_signals already establishes for this exact bug class: the one-time widget/menu construction stays guarded by is_initialized, while everything that binds to self (the row action-column handlers, itemDoubleClicked/itemChanged/itemSelectionChanged/customContextMenuRequested/the item-delegate's closeEditor, every register_menu_action handler, and header's refresh_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 prior self after any reload, the same class of bug channels' 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/.fbx row handed the path to wm.open_mainfile; the RuntimeError was caught but Blender still reported the error banner — _open_path now 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 in test_blender_ui_handler.py (+6, incl. deliberate repeat-header_init/tbl000_init idempotency + reload-rewire checks). Mirrored to mayatk (self.controller is 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, new unwrap_hybrid.lua, new templates/pack_block.lua) copied byte-identical from mayatk; parameters.py gains the same expand_includes include directive, inline @min/@max_rizom_line gating, PACK_SPACING / PACK_MARGIN / FIT_CONES params, 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 mayatk docs/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 + new scripts/pack_into_existing.lua copied byte-identical from mayatk (post-pack ZomDeform placement tail; gap-fill via ZomPack WorkingSet="Visible&Selected" with scale/layout locks); registry gains TARGET_UDIM + UV_AREA; Parameters.preset_min_version parses the -- @min_rizom header and both the panel combo and process_with_rizomuv enforce it (pack_into_existing needs RizomUV ≥ 2022.2). process_with_rizomuv(select_objects=) renders exported copy names into the selection token, and RizomUVBridge.expand_by_materials (bpy material-slot walk over bpy.data.objects) mirrors mayatk's material-defines-the-map expansion; the panel's b000 routes 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 in tentacle/docs/parity_map.py; plan: mayatk docs/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's spacing now takes a float (uniform) or an (sx, sy, sz) tuple via _normalize_spacing. The twin .ui is 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_transforms gains channels + traverse (mirror of the mtk un-freeze upgrade). channels restricts 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 (previously delete_attrs=True dropped all three keys regardless). traverse=True also restores every descendant (children_recursive), parents first (depth-sorted), matching mtk.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's transform_apply folds the applied basis into each child's matrix_parent_inverse to 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_transforms pre-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 in test_xform_utils.py (channel subset, traverse, rotated-parent world/vertex holds) — suite PASS headless (Blender 5.1, --factory-startup); test_duplicate 21/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_rpc plugin (substance_rpc/plugin_src/substance_rpc, {op, kwargs} HTTP wire on 8090) with mesh.reload / project.info / js.evaluate / system.* ops, plus Installer (Documents plugins folder, SUBSTANCE_PAINTER_PLUGINS_PATH honored) auto-installed on send via ensure_rpc_plugin; PainterRpcClient rewritten on pythontk.RpcClient (JSON-RPC 2.0 envelope client removed). (2) Targeting: _preflight narrows a default target="auto" to the template's TARGET_INSTANCE (reimport can never silently launch a fresh Painter), _resolve_connection probes the default RPC port after a registry miss (finds a Painter across Blender restarts / user-launched), and SubstanceConnection.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's fileInfo; saved with the .blend), and reimport (REUSE_RECORDED_EXPORT) overwrites the recorded path instead of re-deriving it. (4) NO_CONNECTION_HINT + RPC_OPS template 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.py 12/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 new substance_rpc/README.md and consistently in the install log, RPC-timeout error, reimport NO_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_axis gained center_pivot=True: merge / symmetrize combine into one object, so its origin re-centers on the combined bounding box (origin_set BOUNDS = Maya's centerPivots); in separate mode only the new _mirror half is centered — the source object's origin is left untouched. Plain cuts (no mirror) are unaffected. center_pivot=False opts out. test_mirror_cut 24/0 headless (--background).

  • 2026-07-20 — Reference Manager: a foreign row is now a real reference (bake → link), not an import-only row; .fbx joins 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 .fbx source skipping the Maya hop (and its license checkout) entirely — and links that; clicking again removes the same library, and display overrides work. Only Open stays unavailable (a .ma can't be opened as a .blend). A source row resolves to its library through a .source.json sidecar 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 .fbx directly. (2) .fbx is live: FOREIGN_EXTENSIONS gained .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, a templates/_bake_scene.py headless-Blender template (empty factory scene → FBX import → save_as_mainfile) that replays the conversion's texture manifest through the existing _apply_texture_manifest engine rather than a second copy of it (the parent's sys.path is passed into the child, so blendertk is importable there), and find_scenes(extensions=…). Conversion + bake cache independently on source and template identity, via the new shared ptk.CachedArtifact. (4) Icon states + tooltips now match mayatk word-for-word. test_blender_ui_handler.py updated (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, whose Filter 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's chk000/b001/b006 placement; 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_structure with the {scenes} default). (2) Footer button relabeled "Un-Reference All" and built with the same footer.add_widget(QPushButton(...)) as Maya (was add_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_structure never passed scenes= to replace_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's scene rule (fallback "scenes") exactly like save_scene_as and Maya's filter. Regression test added to test_reference_manager.py (45/45 under Blender 5.1). parity_map updated (chk000chk_recursive reason; stale txt_subfolder_structure rename entry dropped — same objectName both sides now); header-mirror + option-box-relocation guards added to test_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 is remove_library). (3) Icon columns were invisible_refresh_table_content overrode the link/open/display action columns to ResizeToContents, 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 sizing TableActions.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 + display unavailable; name tagged "(Maya)"); the import routes to btk.import_maya_scene (the existing headless mayapy→FBX bridge the Scene menu already uses). Converter API extended with MayaSceneImport.find_scenes() (the discovery half of the import — reuses the same SUPPORTED_EXTENSIONS the importer accepts; delegates to ptk.FileUtils.get_dir_contents). Regression tests added to test_blender_ui_handler.py (footer button, Fixed+visible icon columns, foreign listing + import dispatch); stale _LOG_LINK_HANDLERS import 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 tbl000 QWidget can outlive the ChannelsSlots instance (a UI reload / slots rebuild leaves is_initialized stamped on the persisted widget), yet the Blender port wired all its table signals — cellWheelScrolled, cellChanged, cellScrub{Started,Moved,Finished}, and customContextMenuRequested — inside the one-time if 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 every tbl000_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_input reduced to column registration; the standalone _wire_context_menu_state folded into _wire_table_signals. Regression added to test_blender_ui_handler.py — it drops all bindings then calls tbl000_init on 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.py did from ._marmoset_bridge import SEND_TO, _TEMPLATE_DIR but _marmoset_bridge.py (unlike mayatk's) never re-exported those two constants from _marmoset_engine, so the slots module raised ImportError at discovery → MarmosetBridgeSlots never registered → the switchboard's file-name fallback resolved the panel to the engine MarmosetBridge instead, whose __init__ rejects switchboard= (panel dead: TypeError: MarmosetBridge.__init__() got an unexpected keyword argument 'switchboard'). Fix: added the SEND_TO / _TEMPLATE_DIR re-export block to _marmoset_bridge.py, verbatim mirror of mayatk's. test_blender_ui_handler.py now 209/209 (marmoset wires MarmosetBridgeSlots; the earlier reference_manager misses were offscreen-QPA flakiness, not a regression). Private-module re-export only — no public-surface change.

  • 2026-07-19 — Migration note: env_utils.script_output module-level facades were removed. The encapsulation sweep (below) dropped the module-level show/hide/toggle/begin_capture/restore functions for ScriptConsole classmethods — but external startup scripts call them too. Blender's scripts/startup/tentacle_startup.py still called script_output.begin_capture(), which now raises AttributeError (swallowed by its try/except), so the session-wide capture never installed early and the console lost pre-console output (the import tentacle greeting banner). Fix: external callers must use ScriptConsole.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 in Parameters/ToolbagLog/Installer/… classes) were relocated into their classes. env_utils.script_output procedural facades (show/hide/toggle/begin_capture/restore) became ScriptConsole classmethods (singleton instance methods renamed _-private) — no name collision, no module functions; consumers call ScriptConsole.<action>(). Dead re-export imports removed (_anim_utils no longer imports the unused ScaleKeys). No public-surface change. Full Blender suite 83/83.

  • 2026-07-19 — Encapsulation sweep: tool modules class-only in the init, _*_utils roots 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 earlier core_utils.diagnostics / env_utils.fbx_utils) are now class-only in DEFAULT_INCLUDE: the redundant flat function names were dropped so they no longer pollute btk.*, matching mayatk's namespace layout. Consumer moves (all in-repo call sites migrated, incl. the tentacle Blender slots): btk.duplicate_linearbtk.DuplicateLinear.duplicate_linear (same for _radial/_grid); btk.create_curtain / btk.curtain_rail_from_selectionbtk.CurtainUtils.<fn>; btk.target_weldbtk.TargetWeld.activate; btk.export_selection_usd / btk.import_usdbtk.UsdUtils.<fn> (redundant module-level import_usd deleted); register_strategyTubeStrategy.register. Separately, every def fn() + Class.fn = staticmethod(fn) (and the bare module-level publics) in the 14 _*_utils roots — 220 functions — was relocated into its *Utils class body as a real @staticmethod, and the 128 cross-module from ..._X_utils import fn importers rewritten to Class.fn (scope-aware, so local shadows are untouched). No public-surface change for the roots: btk.get_parent and btk.NodeUtils.get_parent remain the same object under "*". Full Blender suite 81/81.