Skip to content

blendertk v0.5.21

Choose a tag to compare

@m3trik m3trik released this 19 Jul 03:27
  • 2026-07-18 — anim_utils.shots dead-surface removal (mirror of mayatk's shots improvement pass). Dropped the never-called ShotManifestController.browse_csv (browsing is wired exclusively through _browse_csv_option) and the never-passed prevent_overlap param + _push_overlapping_objects on ShotSequencer.move_object_in_shot — grep-confirmed zero call sites in both packages; removed symmetrically so the parity surfaces stay matched. test_shot_sequencer.py 65/65 and test_shot_manifest.py 24/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_grp empty (only when it holds nothing else — never a user's own parent), the contact empty — found via a new shadowContact ID-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=True also removes the PNG from disk; the shadow_metadata channel is republished (cleared when the last rig goes). Targets and the shared shadow_source empty survive; works on live and baked rigs. Pre-existing bug fixed red-first en route: find_shadow_planes double-listed a plane when the selection overlapped (group + its plane child — the group's children_recursive re-lists it; proven ['Box_shadow', 'Box_shadow']), which was a harmless no-op for bake_planes but crashed delete_rigs with a ReferenceError on the second, already-removed entry — now deduped via dict.fromkeys (mirror of mayatk) and hardened against stale refs, so a double delete() 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 — ScriptJobManager mirror sync (mayatk hardening pass): shared-widget cleanup bug fixed, suppressed() context manager, visible listener errors. (1) Bug (same as mayatk's): connect_cleanup keyed connected widgets by id(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) New suppressed(*tokens) context manager (mirror of mayatk) replaces the manual suppress/try/finally/resume dance (e.g. render_opacity_slots); skips None tokens, 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.handlers fire 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.Stretch section (re-applied after every table rebuild, since add() 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_current helper called from _on_root_changed + _write (the root selection is the Set-Project action, so the button was redundant). (3) The panel now opens persistentconfig_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 titled Separator("Current Workspace") followed by the bare workspace-name row (was a "Current Workspace: <name>" text prefix) — verified the ExpandableList accepts a Separator instance (mouse-transparent, non-interactive) offscreen; (6) the Blender dir-browser is now folders-only (dispatch guard os.path.existsos.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}.py updated 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 retired Default theme phantom. The pre-2026-07-05 backup/restore design left a Default.xml (a byte-identical copy of the shipped Maya theme) plus its Default_Backup.json sidecar in Blender's per-user presets/interface_theme dir; because list_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 .xml only when byte-identical to a shipped style, so a user's own theme legitimately named "Default" is untouched, and the .json sidecar unconditionally (Blender never writes .json into a preset dir). Self-healing on upgrade; a fresh user never had them. Covered by 4 new checks in test_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 QtDock modal-suspend guard is REVERTED and the Win+Shift+Arrow workaround is obsolete. The guard answered a misdiagnosis: the livelock was tentacle's processEvents pump dispatching Blender's own native messages from nested WM_TIMER contexts (py-spy-proven; the new tentacle/test/blender/console_frame_resize_check.py froze 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_INTERVAL and BlenderWindow.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_check 34/34; cross-DPI console_dpi_drag_check A0/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 (drops modal_move_size_active when 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_AFTERPARENT traffic was never the trigger.] Cross-DPI window-drag hard freeze root-caused (live report: "Blender freezes when resized"); QtDock now 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 answers WM_NULL (the OS never flags Not Responding; kill required) — reproduces with a bare QApplication and 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-entrant wglSwapBuffers/wndproc layers with a processEvents → DispatchMessageW that 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/InSendMessage gating); the programmatic path is proven clean by console_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 new BlenderWindow.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's WM_DPICHANGED_AFTERPARENT traffic out of the storm; Qt is pump-gated for the loop's duration anyway, so the hidden child costs nothing functional. console_dock_check.py 34/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): a dock_editor strip docked MID-SESSION has a border Blender's interactive edge hit-test cannot find (no resize cursor; drags and area_move ignored; rects tile fine) — a startup-docked strip resizes normally; the tag_redraw+redraw_timer heal 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.mel immediately (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 in test_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 canonical uitk.PresetManager.wire_combo selector (Refresh/Save icons + Rename/Open/Delete menu, inline naming — the established preset-combo pattern) in semantic mode over the SAME ptk.PresetStore the headless btk.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 to DEFAULT_FILE_RULES). Interop fix: workspace_template_rules now strips the _meta version 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 (_meta strip) → 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.mel merge-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 new ptk.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 surface workspace_template_rules / save_workspace_template / delete_workspace_template / list_workspace_templates, and create_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 standard DEFAULT_FILE_RULES, unchanged behavior. Both name-prompt slots now use the house sb.input_dialog (dropping the raw QInputDialogs). Tests: test_workspace.py +7 template checks under a sandboxed UITK_PRESETS_ROOT (active-template default, create_workspace builds 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 as workspace_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 the workspace.mel through the merge-preserving codec — hand-written lines survive, deleted rows are removed via the codec's new remove= 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 Editor row; 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's workspace -o) and governs only the ambient chain (get_env_info/no-arg accessors); pinned behavior itself is unchanged. test_workspace.py re-pins the semantics (pin wins ambient / explicit path bypasses — 27 disk checks + 2 bpy); test_blender_ui_handler.py rosters 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.mel read/write/create, built on the new pythontk.Workspace (publish pythontk first). Blender has no native project system; blendertk now has the ecosystem's: a workspace is either marked (a workspace.mel at 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 via set_current_workspace — Maya's workspace -o analogue → nearest marked root containing the saved .blend → the .blend's own folder), with workspace_root / scenes_dir / source_images_dir / workspace_scenes_dir accessors; get_env_info("workspace") now routes through it, so a .blend saved in a Maya project's scenes/ reports the project root. Rule-fed folders: the four independent <blenddir>/textures guesses (shader_templates + game_shader source_images_dir, the Texture Path Editor's footer resolver, the Lightmap Baker's output dir, normalize_texture_paths' copy/move default) all collapse into btk.source_images_dir()sourceImages rule for marked projects, existing sourceimages/textures folder, else the legacy textures default (Blender-alone behavior unchanged). save_scene_as' {scenes} placeholder now resolves the workspace's real scene rule (the workspace -q -fre lookup its docstring previously lamented not having). Discovery: find_workspaces is marker-aware — marked projects list even with no loose .blend at their root, and an unmarked candidate nested inside a marked project (its scenes/) belongs to that project instead of double-listing. Create/promote: create_workspace (Maya-standard rules + standard subfolders — the File ▸ Project Window ▸ New counterpart) and promote_workspace (writes a marker describing the layout the folder ALREADY has: scene rule . for flat blend folders, sourceImagestextures when 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 scans scenes/); Set To Current Workspace lands on the marked project root instead of the .blend's subfolder. New test/test_workspace.py — 28/28 under the Blender harness (26 disk-level checks also run under the .venv), incl. live get_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.workspace parses the marker natively) — the live round-trip contract is locked in mayatk/test/test_workspace_mel.py; Maya's Reference Manager listing .blend scenes (routed through import_blender_scene) is the deferred follow-up.

  • 2026-07-18 — auto_instance run diagnostics (mirror of mayatk). AutoInstancer.last_run_summary records 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-group details — populated in _process_groups (bookkeeping only). Static helpers AutoInstancer.default_summary() / AutoInstancer.format_summary(summary, output_count) (ASCII text) and a return_summary=False flag 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.py run-summary block: dense→instanced, micro→too-simple, cube-vs-sphere→no-match, default→bare list).

  • 2026-07-17 — BlenderUiHandler.__init__ registers dispatch_log_link into uitk's new log-link registry (uitk.bridge.register_log_link_handler), mirroring MayaUiHandler. 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_link already existed but uitk never reached it. Now uitk dispatches DCC-agnostically through the registry and Blender gets parity. A positive-registration assertion in test_blender_ui_handler.py locks the wiring (the try/except in __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 discrete cmds.roll); Blender's native nav is always-on via the mouse but not armable from a menu — so navigate_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 (multiplicative view_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 proven target_weld pattern — deferred one tick, then INVOKE_DEFAULT under a get_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 pure mathutils and headless-unit-tested in test_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_view exposure, and the background refusal). The interactive drag itself needs a windowed session: test/view_nav_gui_check.py (new, mirrors target_weld_gui_check) asserts the modal goes live on the window after navigate_view and that each mode mutates a live RegionView3D — run it with blender --factory-startup --python … (owed, not runnable headless). Public surface: navigate_view added to the root DEFAULT_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) and b012 (menu action) and threaded through HierarchySyncController.analyze_hierarchies → the engine's existing filter_cameras/filter_lights params. Blender's obj.type already reports 'CAMERA'/'LIGHT' exactly, so should_keep_node_by_type needed 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/LIGHT should_keep_node_by_type checks; analyze_hierarchies flags on → camera+light dropped from extras, off → both present) — the engine params' first coverage; 49/49 headless.