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-test cannot find (no resize cursor; drags andarea_moveignored; rects tile fine) — a startup-docked strip resizes normally; the tag_redraw+redraw_timerheal works only Qt-free, so the fix direction (Qt-side resize grip re-dock, or Blender-level) is pending. Registry/parity regen deferred to a clean-tree pass. -
2026-07-18 — Workspace Editor redesigned minimal: one Project Root field, real-time rule writes (no Accept), per-row table action icons, and the canonical uitk preset combo for templates. UX rework of the Project Window mirror below, to a 9-point user critique: the Current Project / Location split, New, Accept, Set As Current, Open Folder buttons and the Create Folders checkbox are all gone from the panel body — what remains is one path field + the rules table. (1) Real-time Accept: every committed cell edit, row add/remove/reset, template load, and Reset/Clear writes
workspace.melimmediately (merge-preserving; removals diffed against the last written baseline); a fresh path seeds the active template without writing, and the first rule edit is what creates the project (marker + rule folders) — pinned end-to-end intest_blender_ui_handler.py. (2) Per-row action columns (TableWidget.actions, the style/color-editor idiom): reset-to-template-default (undo) and remove (trash) icons on every row — Remove Selected leaves the header menu; row indices resolve at click time so they can't go stale. (3) Table headers fixed to Maya's vocabulary: RULE: / LOCATION: (was LOCATION:/FOLDER: — the rule column was misnamed). (4) One Project Root lineedit with option-box icon actions: browse + open-in-file-browser (the old Location field and Open Folder button folded in). (5) Set As Current moved to the header menu; Reset/Clear Settings stay there (now write-through, tooltips say so) and cover the old New button. (6) Templates ride the canonicaluitk.PresetManager.wire_comboselector (Refresh/Save icons + Rename/Open/Delete menu, inline naming — the established preset-combo pattern) in semantic mode over the SAMEptk.PresetStorethe headlessbtk.workspace_template_*API reads: picking a template loads its rules AND makes it the active default for new workspaces; the bespoke Save Template…/Delete Template buttons and the pseudo "Standard (built-in)" combo entry are gone (no active template still falls back toDEFAULT_FILE_RULES). Interop fix:workspace_template_rulesnow strips the_metaversion block PresetManager-saved templates carry (would have surfaced as a bogus rule). Tests:test_blender_ui_handler.py+9 functional checks driving the whole real-time loop offscreen (fresh-path seeding, first-edit creation, row reset/remove, Clear/Reset write-through, combo-template→headless-API round-trip) → 198/198;test_workspace.py+2 (_metastrip) → 37/37 under real Blender 5.1. -
2026-07-18 (later) — Workspace Editor reworked into a true mirror of Maya's Project Window, + named workspace TEMPLATES that define how every new workspace is built. Same-day rework of the panel below, to the user's screenshots of Maya's own window: Current Project / Location split (browsing to an existing workspace root auto-fills both — Location is always the project's parent, like Maya's), New (fresh definition seeded from the active template; nothing written until Accept), Accept (writes
workspace.melmerge-preservingly + optional Create Folders — the moment the workspace is built from its definition, exactly like Maya's Accept), Nice Names ↔ File Rules view (Maya's Edit ▸ View toggle — rows show Maya's own Project Window labels via the newptk.RULE_NICE_NAMES, in its display order; raw view makes the rule column editable, and the raw key rides each row's UserRole so the views round-trip unsaved edits), Add New File Rule / Remove Selected (the Custom Data Locations verbs) and Reset / Clear Settings (the Edit-menu pair). Templates (ptk.PresetStore("workspace_templates")-backed, per the user's uitk-preset ask): Save Template… stores the current rule set under a name and makes it ACTIVE; new module surfaceworkspace_template_rules/save_workspace_template/delete_workspace_template/list_workspace_templates, andcreate_workspace(rules=None)now seeds from the active template — so a saved template genuinely defines how each subsequent workspace is built, everywhere (this panel's New, the Reference Manager's New Workspace, any API caller); no template saved → the standardDEFAULT_FILE_RULES, unchanged behavior. Both name-prompt slots now use the housesb.input_dialog(dropping the rawQInputDialogs). Tests:test_workspace.py+7 template checks under a sandboxedUITK_PRESETS_ROOT(active-template default,create_workspacebuilds from it, named lookup, delete falls back) — 34 disk checks + 2 bpy; discovery 188/188. -
2026-07-18 — Workspace Editor panel (
env_utils/workspace_editor.py+.ui, discovered asworkspace_editor) + pin-resolution refinement. The blendertk counterpart of Maya's File ▸ Project Window: pick (or type) a root, edit the file-rule table (semantic name → folder; add/remove rows and a standard-template reset in the header menu), Save Workspace writes theworkspace.melthrough the merge-preserving codec — hand-written lines survive, deleted rows are removed via the codec's newremove=support, and "Create Folders" also makes each rule folder — and Set As Current pins it for the session. Launched from tentacle's Workspace tab (Workspace Editorrow; Maya's twin row opens the native Project Window — see the tentacle CHANGELOG). Resolver refinement:current_workspace(path)with an explicit path now resolves THAT path and never answers with the unrelated session pin — the pin is global state (Maya'sworkspace -o) and governs only the ambient chain (get_env_info/no-arg accessors); pinned behavior itself is unchanged.test_workspace.pyre-pins the semantics (pin wins ambient / explicit path bypasses — 27 disk checks + 2 bpy);test_blender_ui_handler.pyrosters the new panel (188/188, bpy-free slot instantiation included). Full harness green (84 suites). -
2026-07-18 — Maya-shareable project workspaces: a package-wide current-workspace resolver +
workspace.melread/write/create, built on the newpythontk.Workspace(publish pythontk first). Blender has no native project system; blendertk now has the ecosystem's: a workspace is either marked (aworkspace.melat its root — a shared Maya/Blender project whose file rules say where scenes/textures live) or unmarked (a plain folder of .blend files — zero ceremony, exactly the old heuristic). Resolver:current_workspace()(session pin viaset_current_workspace— Maya'sworkspace -oanalogue → nearest marked root containing the saved .blend → the .blend's own folder), withworkspace_root/scenes_dir/source_images_dir/workspace_scenes_diraccessors;get_env_info("workspace")now routes through it, so a .blend saved in a Maya project'sscenes/reports the project root. Rule-fed folders: the four independent<blenddir>/texturesguesses (shader_templates + game_shadersource_images_dir, the Texture Path Editor's footer resolver, the Lightmap Baker's output dir,normalize_texture_paths' copy/move default) all collapse intobtk.source_images_dir()—sourceImagesrule for marked projects, existingsourceimages/texturesfolder, else the legacytexturesdefault (Blender-alone behavior unchanged).save_scene_as'{scenes}placeholder now resolves the workspace's realscenerule (theworkspace -q -frelookup its docstring previously lamented not having). Discovery:find_workspacesis marker-aware — marked projects list even with no loose .blend at their root, and an unmarked candidate nested inside a marked project (itsscenes/) belongs to that project instead of double-listing. Create/promote:create_workspace(Maya-standard rules + standard subfolders — the File ▸ Project Window ▸ New counterpart) andpromote_workspace(writes a marker describing the layout the folder ALREADY has:scenerule.for flat blend folders,sourceImages→textureswhen that's the real texture home; never clobbers existing rules — the merge-preserving codec keeps hand-written lines verbatim). Reference Manager: header gains New Workspace… / Mark As Workspace (parity: review-extras — Maya covers this natively with the Project Window); a marked workspace's scene-rule folder is included in the non-recursive table scan (mirror of mayatk, which always scansscenes/); Set To Current Workspace lands on the marked project root instead of the .blend's subfolder. Newtest/test_workspace.py— 28/28 under the Blender harness (26 disk-level checks also run under the .venv), incl. liveget_env_info→ project root and{scenes}rule resolution;test_reference_manager/test_core_utils/test_texture_path_editor/test_lightmap_baker/ smoke all green. The Maya side needs no production change (cmds.workspaceparses the marker natively) — the live round-trip contract is locked inmayatk/test/test_workspace_mel.py; Maya's Reference Manager listing.blendscenes (routed throughimport_blender_scene) is the deferred follow-up. -
2026-07-18 —
auto_instancerun diagnostics (mirror of mayatk).AutoInstancer.last_run_summaryrecords why identical meshes did / didn't become instances —matched_groups,instanced_groups/instances_created,simple_groups(too simple → combined, not instanced),kept_separate_groups(flagged individual / non-static),micro_threshold, per-groupdetails— populated in_process_groups(bookkeeping only). Static helpersAutoInstancer.default_summary()/AutoInstancer.format_summary(summary, output_count)(ASCII text) and areturn_summary=Falseflag on the wrapper (True→(created, summary); default → bare list, backward compatible). Drives the Blender Duplicate slot's "why nothing was instanced" message box + console print. Verified headless (test_auto_instancer.pyrun-summary block: dense→instanced, micro→too-simple, cube-vs-sphere→no-match, default→bare list). -
2026-07-17 —
BlenderUiHandler.__init__registersdispatch_log_linkinto uitk's new log-link registry (uitk.bridge.register_log_link_handler), mirroringMayaUiHandler. This fixes a real latent bug: uitk previously hard-imported mayatk for log-panel node (select/reveal) actions, so in a Blender session those links were dead —blendertk.dispatch_log_linkalready existed but uitk never reached it. Now uitk dispatches DCC-agnostically through the registry and Blender gets parity. A positive-registration assertion intest_blender_ui_handler.pylocks the wiring (thetry/exceptin__init__would otherwise swallow a drifted import path silently). -
2026-07-17 —
cam_utils.navigate_view— interactive Maya-style viewport navigation (Tumble / Track / Dolly + Roll), rolled our own so it works exactly like Maya's drag tools. Maya arms a drag nav tool (setToolTo tumbleContext/trackContext/dollyContext; Roll is a discretecmds.roll); Blender's native nav is always-on via the mouse but not armable from a menu — sonavigate_view(mode)registers and invokes a modal operator (BTK_OT_view_nav) that LMB-drags the RegionView3D exactly like Maya:mode="ORBIT"orbits (turntable — azimuth about world-Z, elevation about the view's right axis),"TRACK"pans the pivot along the view right/up (scaled by distance),"DOLLY"moves the eye in/out (multiplicativeview_distance, floored so it never crosses the pivot),"ROLL"rotates about the view axis (a drag form of Maya's discrete roll, for consistency with the other three). RMB/Enter finishes, Esc restores the view snapshotted at invoke; Alt+drag passes through so Industry-Compatible / emulated-3-button native nav still works (the modal-tool-recipe gotcha). Launched via the proventarget_weldpattern — deferred one tick, thenINVOKE_DEFAULTunder aget_view3d_context()override (the Qt event-pump has no active viewport) — and refuses deterministically in--background(where a modal invoke CANCELs silently). The per-mode mouse-delta→view math (_orbit_rotation/_roll_rotation/_dolly_distance/_track_location) is puremathutilsand headless-unit-tested intest_cam_utils.py(8 checks pinning the exact quaternion/vector results — orbit axes, roll preserving forward, dolly halving + flooring, track direction — plus operator registration,btk.navigate_viewexposure, and the background refusal). The interactive drag itself needs a windowed session:test/view_nav_gui_check.py(new, mirrorstarget_weld_gui_check) asserts the modal goes live on the window afternavigate_viewand that each mode mutates a live RegionView3D — run it withblender --factory-startup --python …(owed, not runnable headless). Public surface:navigate_viewadded to the rootDEFAULT_INCLUDE(btk.navigate_view/btk.CamUtils.navigate_view); API registry regen owed. -
2026-07-17 — Hierarchy Sync: 'Filter Cameras' / 'Filter Lights' diff options — exclude all cameras / all lights from the comparison (mayatk parity). Two checkboxes in the Diff Options combo beside Filter Mesh Objects, read by both
tb001(Diff button) andb012(menu action) and threaded throughHierarchySyncController.analyze_hierarchies→ the engine's existingfilter_cameras/filter_lightsparams. Blender'sobj.typealready reports'CAMERA'/'LIGHT'exactly, soshould_keep_node_by_typeneeded no change (unlike mayatk, whose light shapes report concrete node types). Pure UI wiring; engine and core diff logic unchanged. Tests:test_hierarchy_sync.py+4 (CAMERA/LIGHTshould_keep_node_by_typechecks;analyze_hierarchiesflags on → camera+light dropped from extras, off → both present) — the engine params' first coverage; 49/49 headless.