Skip to content

Releases: m3trik/blendertk

blendertk v0.5.48

Choose a tag to compare

@m3trik m3trik released this 02 Aug 19:16
  • 2026-08-02 — Color ID's Outliner channel now sets the object's outliner TEXT color, like Maya's (OutlinerTint), and Set Per Color becomes an opt-in on both DCCs. The previous day's mapping — link objects into a color-tagged ID_<HEX> collection — was correct about Blender's published API but diverged from mayatk: Maya's channel tints the object's text, and Blender exposes no equivalent (probed live on 5.1.2: no Object.color_tag/outliner_color/use_outliner_color, and SpaceOutliner's whole RNA surface is filters and display toggles — no tree, rows, or expansion state). So the color is painted. New display_utils/outliner_tint.py: the color is stored as a btk_outliner_color custom property (ordinary data — saves with the .blend, readable without any of the below), and a SpaceOutliner POST_PIXEL draw handler walks the outliner's internal TreeElement tree through the space's struct pointer, masks each tinted row's label with the theme background, and repaints it in the object's color at Blender's own layout position (text_x = te->xs + 2*UI_UNIT_X + 4*ufac, text_y = te->ys + 5*ufac, from outliner_draw.c). Offsets were established empirically by planting known object names and matching the resolved IDs (te->subtree +0x20, xs +0x30, ys +0x34, store_elem +0x38 → TreeStoreElem.id +0x08 → ID.name +0x28, tree head at SpaceOutliner +0xC0). TreeElement is runtime state with no API-stability guarantee, so the whole subsystem is built to fail closed: the offsets are re-validated against the live process before the overlay is ever allowed to draw (the walk must resolve real bpy.data names, else it stands down), every dereference passes an OS memory-protection query (_MemGuard, region-cached — a bad offset yields None, never a segfault in the user's session), and any failure disables the overlay and leaves the outliner exactly as Blender drew it. A disabled overlay costs nothing and loses no data: the colors live on the objects. Non-Windows platforms keep the stored colors and skip the painting (is_supported). Set Per Color (chk016, new on BOTH twins, off by default) takes over the grouping the Outliner channel used to do — ID_<HEX> objectSet in Maya (ColorId.add_to_color_set/get_color_set_color/remove_from_color_sets, stamped mtk_color_id), color-tagged collection in Blender — as a grouping aid orthogonal to the four color channels. Both are stamp-keyed, never name-keyed, so a user's own ID_* set/collection is never adopted, retagged, or swept, and a recolor moves rather than piles up (emptied containers are garbage-collected; verified in mayapy that deleting an objectSet leaves its members alive). All four channels plus the new one now match mayatk by objectName, label, and capability — compare_panel_surface --panel color_id 0 untriaged / 0 prop deltas. Three details the overlay gets right because getting them wrong is visible or unsafe: the mask uses the row background Blender actually drew — plain back, or the selection/active highlight, keyed on selected (an active-but-unselected row has no highlight, so keying on "is active" alone paints a box where Blender drew none); the font comes from ui_styles[0].widget.points, not a hardcoded 11, so a user's UI font size still lines up; and a stand-down from inside the draw callback defers the draw_handler_remove to a timer rather than mutating Blender's handler list mid-iteration. test_color_id.py +13 checks (stored color round-trip, select-by, tinted_objects, reset, None-safety, and the enable/disable idempotency + "colors are readable regardless of the overlay" fail-closed contract) → 64/64; new GUI harness test/outliner_tint_gui_check.py covers what --background cannot (draw handlers never fire headless): live-process calibration reaching status == "ok", the walk resolving every planted object at distinct row positions, selected/active/unselected rows all masked correctly, a deliberately corrupted offset standing the overlay down with a recorded reason instead of crashing (colors intact), and a screenshot → 16/16.

  • 2026-08-02 — Maya bridge send: hierarchy arrived as locators, real materials arrived gray, and the launched Maya inherited Blender's OCIO. All three fixed (live-verified end to end).

    • Parent Empties → plain Maya groups. Blender Empties travel as FBX nulls and Maya's importer gives EVERY null a locator shape, so a sent group tree arrived as locators all the way down. The import template now strips the locator shape from every imported transform whose Empty has children (a Blender Empty is a Maya group); a CHILDLESS Empty stays a locator — it marks a point, and a shapeless leaf transform would be invisible. Scoped to the import's new nodes (pre-existing user locators untouchable), skips multi-shape transforms. The import itself went deterministic while at it: FBXResetImport + pinned add mode + returnNewNodes (mirror of the pull engine's _import_fbx — sticky global import options and the factory merge mode could retarget animation onto same-named scene nodes).
    • Native material rebuild via the pull direction's sidecar contract. Blender's FBX exporter only carries images wired (almost) directly into Principled sockets — packed ORM/MSAO through SeparateColor, AO multiplies, node-group plumbing export as NOTHING, so production materials (exactly what the Maya→Blender direction builds) arrived gray. MayaBridge._produce now writes the same .manifest.json the pull has always used (in-process collector: whole-node-tree walk incl. groups, UDIM flatten-to-first-tile, file-less entries for packed-only images so Maya warns BY NAME, scene_materials for the rename-on-clash guard; kept in step by hand with the dependency-free copy in mayatk's pull template), and the import template replays it through the ONE existing applier, mayatk.BlenderSceneImport._apply_texture_manifest (GameShader → standardSurface, the established template→paired-engine contract from both _bake_scene.py templates) — guarded, so a Maya without mayatk keeps the FBX materials with a console line saying so. Skipped when INCLUDE_MATERIALS is off.
    • Launch env. The launched Maya inherited Blender's whole environment; an OCIO pointing at Blender's bundled v2.5 config fails Maya 2025's color-management init on every send ("This version of the OpenColorIO library (2.3.2) is not able to load that config version"). _SPEC.launch_envMayaBridge._launch_env strips OCIO via the new ptk.AppLauncher.handoff_env exactly when it points inside Blender's own install (root from bpy.app.binary_path; import-safe outside Blender); a studio config elsewhere inherits untouched.
    • Verified live: probe scene (grp > sub > cube + leaf Empty + a Mix-routed texture the FBX provably drops) exported through the real producer, imported in mayapy through the real rendered template — arrives as grp/sub plain groups, leaf_marker locator, and a rebuilt standardSurface with the texture wired. test_maya_bridge.py +10 → 25 (template wiring pins; manifest schema incl. shared-datablock merge, packed → file-less, clash guard; launch-env all three branches). scene_import (74) / unity_bridge (15) re-run green.
  • 2026-08-02 — Bridge hand-offs flattened the scene graph: the shared FBX export set was mesh-only, so every Empty was dropped and its children re-rooted. fbx_utils._EXPORT_DEFAULTS pinned object_types={"MESH"}, and Blender's FBX exporter does not just omit an excluded type — it re-parents that object's children to the nearest included ancestor, i.e. to the root. A grp > sub > mesh chain therefore arrived in Maya (and Unity) as parentless meshes: total, silent loss of the scene graph on every send. Every other consumer had already discovered this and overridden it locally (Substance and Marmoset pin {"MESH","EMPTY"}, the Scene Exporter ["EMPTY","ARMATURE","MESH"], tentacle's scene slot {"MESH","EMPTY","OTHER"}) — the bridges were the ones left on the broken default. EMPTY is now in the shared default (a Blender Empty is a Maya group; hierarchy is not an opt-in), and BlenderExportMixin._fbx_options — the producer behind the Maya and Unity bridges — widens it to {"MESH","EMPTY","ARMATURE","OTHER"}, covering the other two silent losses: skin deformation (the mirror of mayatk's FBXExportSkins -v true) and curve/text/metaball geometry. Cameras and lights stay out; the hand-off ships assets. Verified live on 5.1.2 in both directions — the Blender→Maya round trip now returns grp > sub > mesh intact, and the Maya→Blender pull (whose MEL flag battery was always correct) is unchanged. test_fbx_utils.py +3 (parenting round-trip through the defaults; the hand-off set; and a BlenderExportMixin._export_fbx round trip proving a grp > armature > skinned mesh chain survives the real producer). Substance and Marmoset are untouched — their _produce calls export_selection_fbx directly with their own options, so only the two DCC-to-DCC bridges read _fbx_options. bridges / maya_bridge / unity_bridge / scene_import / scene_exporter / emissive_groups / rizom suites re-run green.

  • 2026-08-02 — Workspace Editor review: a rule added in Nice Names view wrote the LABEL as the rule name, table edits died after a UI reload, and the root field acted on every keystroke. Companion to the mayatk half (see its CHANGELOG for the shared model work).

    • Nice-name rule keys. A row added via Add New File Rule has no stored key, so _key_at fell back to its cell text — and in Nice Names view the user is looking at labels, so they type one. "Render Data" went into workspace.mel verbatim as workspace -fr "Render Data" …, a rule name Maya does not recognize. New _key_for_label reverse-maps ptk.RULE_NICE_NAMES case-insensitively; an unknown label still passes through as a custom rule.
    • Signals re-wire on every _init. tbl000's `itemChang...
Read more

blendertk v0.5.45

Choose a tag to compare

@m3trik m3trik released this 01 Aug 23:09
  • 2026-08-01 — Exporter blind spots: duplicate-material merges are now VERIFIED, NLA/data-level animation reaches the bake range, and the selection funnel can no longer silently drop (or be killed by) unshippable objects. Three fixes, mirrors of mayatk's new two-phase duplicate design where one exists. (1) MatUtils.find_materials_with_duplicate_textures(materials, strict=False, verify=True) is now two-phase (mirror of mtk's): phase 1 fingerprints on (surface shader idname, {(surface socket, texture id)}) — TEX_IMAGE nodes are collected through nested node groups (previously invisible) and attributed to the surface-shader input they feed, so one shared detail/normal map no longer merges two different materials (confirmed false positive: reassign_duplicate_materials(delete=True), the default-on exporter task, DELETED one of them); phase 2 (verify=True, default) pairwise-verifies each candidate against its group's keeper — same shader type, equal unlinked shader inputs (1e-5), and per-slot identical color space, ShaderNodeMapping placement, and image CONTENT (size + partial hash via the new _texture_content_id, library-aware paths). reassign_duplicate_materials(..., verify=True) re-proves each group right before its destructive merge. (2) AnimUtils.get_animated_extent / has_nla_or_data_animation: _actions reads only o.animation_data.action, so NLA-strip-only or shape-key/data-level animation was invisible while the FBX write bakes the evaluated scene — set_bake_animation_range now covers active-action fcurves ∪ non-muted NLA strip extents (scene time) ∪ data/shape-key fcurve ranges, and check_untied_keyframes / check_floating_point_keys / check_framerate WARN once per run ("NLA/data-level animation present — only validates active object actions") instead of staying vacuously green (edit tasks deliberately NOT extended to strips: strip actions may be shared/library-linked with their own frame mapping). (3) Export funnel content loss: FbxUtils.export selected with unguarded select_set — hidden objects silently vanished from the FBX and a view-layer-excluded member RAISED and killed the export; unselectable members are now collected + WARNED (count + names; new strict=True raises instead), the selection restore tolerates a no-longer-selectable prior member, SceneExporter pre-filters the export set with an INFO log (the funnel warning is the backstop; check_hidden_geometry remains the failing gate), and export_data_node links a collection-hidden/excluded data_export carrier to the scene root for the write (deferred unlink after) so its metadata ships regardless. test_mat_anim_utils.py +12 checks (socket identity, tiling/content/colorspace/settings near-misses each NOT duplicates, node-group texture seen, true duplicates merged, reassign's own verify gate) → 160/160; test_smart_bake.py +11 (NLA-only strip extent reaches the scene range, muted strips skipped, shape-key extent, once-per-run check advisory) → 138/138; test_scene_exporter.py +10 (funnel drop warning + strict raise, excluded member no longer fatal, pre-filter INFO + FBX content, excluded-collection carrier ships + root link removed) → 58/58.

  • 2026-08-01 — Texture checks become storage-aware (library / packed / UDIM), and the glb-only sidecar no longer rolls forward on a failed conversion. Three fixes from the exporter tasks/checks audit. (1) _MatUtilsInternal._abspath is now library-aware: an image linked from a library .blend stores its // path relative to the LIBRARY file, so resolving without library=img.library pointed every linked texture at the wrong directory — false "missing" in check_valid_paths/get_image_records, wrong duplicate fingerprints and size probes for every _abspath consumer. (2) Packed + UDIM images in the three texture checks (mirrors mayatk's <UDIM>-collapse semantics): PACKED images are skipped by check_valid_paths, check_absolute_paths, and check_texture_file_size — the FBX embeds them from memory, so their (often stale, absolute) bookkeeping path never ships; TILED (UDIM) images — previously invisible to check_valid_paths (get_image_records is FILE-only, so a deleted tile set passed) — are validated by probing the first declared tile on disk (tiles[0].number, 1001 fallback), and size-gated at their largest existing tile via the new _MatUtilsInternal._udim_first_tile_path / _udim_tile_paths (previously os.path.getsize on the raw token path raised OSError into a silent continue, letting multi-GB tile sets through unmeasured). (3) _write_export writes the scene-data sidecar only after the deliverable is confirmed: in glb-only mode it was written before the FBX→GLB conversion, so a failed conversion produced no deliverable but still rolled the hierarchy-diff baseline forward (same ordering bug as mayatk's). test_scene_exporter.py +10 checks (packed passes both path checks, tile-less UDIM fails / first-tile UDIM passes validity, largest-tile size gate, failed-glb-leaves-no-sidecar + success-writes-it) → 48/48; test_texture_path_editor.py +3 (a linked image resolves against its library and reports as existing) → 16/16.

  • 2026-08-01 — Naming.rename collapses separator residue after strip/replace formatting (mayatk parity). Stripping a token from 'a__tok__tokB' left 'a____B'; now collapsed to 'a_B' via the new ptk.collapse_delimiter_runs — an explicit __ typed in the pattern is honored. Mirror of the mayatk fix root-caused from the VDATS mangled-shape-name incident (mayatk CHANGELOG 2026-08-01 has the full story; the uninstance scratch-token source is Maya-only, so only the rename half applies here).

  • 2026-08-01 — Scene Exporter: the smart_bake session is now actually RESTORED after export (first fix from the tasks/checks audit). The task stored result.session_id but nothing in production ever consumed it, so every export with Smart Bake on (default) permanently muted the user's constraints/drivers and left the baked Action in place — despite the task docstring's and tooltip's "restorable" contract. perform_export's outer finally now mirrors mayatk's: SmartBake.restore(session_id) on every exit path (successful write, failed check, raising task), restore-failure surfaced at WARNING with the session id (the session stays in the manifest for a manual retry), and exceptions never mask the export result. Also: a check-blocked export now warns that task edits (material cleanup, key snapping/tying, path rewrites) remain in the scene — checks run after tasks and there is no automatic rollback; and check_hidden_geometry's message + tooltip now state the actual Blender hazard: the selection-based funnel (use_selection=True) silently DROPS hidden meshes from the FBX — the old Maya-inherited message claimed they "will be exported", the exact inverse. test_smart_bake.py +10 exporter-level checks (_run_exporter_bake_restore_checks: success path and blocked-export path, both asserting the constraint unmutes and no session is left) → 127/127.

  • 2026-08-01 — BlenderUiHandler + BlenderNativeMenus join the package namespace (mayatk parity). mayatk exposes mtk.MayaUiHandler / mtk.MayaNativeMenus; blendertk exposed neither, so tcl_blender composed its handler set with mtk.MayaUiHandler's twin spelled as a deep module path — the two DCC hosts reading differently for no reason, in a package whose whole contract is mirroring mayatk's names. Now btk.BlenderUiHandler / btk.BlenderNativeMenus, class-only like their Maya counterparts.

  • 2026-08-01 — Scene Exporter now refreshes every metadata producer at export: FbxUtils.run_export_preparers + _KNOWN_PRODUCERS (mayatk parity), so a mesh deleted after the last bake can no longer ship a stale data_export manifest. From the DCC→Unity data_nodes architecture review. Blender has no before-FBX-export event, so authoring-time publishes were the only refresh — export_data_node claimed "the carrier is already current at export", which scene edits since the last bake/commit quietly falsified. The task now runs the new producer registry (shadow / emissive_groups / lightmap — LightmapBaker.refresh_export_metadata added as the public no-arg entry point, mirroring the other producers) via _refresh_scene_data_node before folding the carrier in; producers are isolated and clear-on-empty, and non-Scene-Exporter export paths still ship the authoring-time state (documented as the remaining divergence from mayatk's session hook). Also: DataNodes._set_string honors its documented return contract (None when an empty value had nothing to clear — it returned the carrier name), and docs/data_nodes.md's channel table gains the shadow_metadata / emissive_groups rows it was missing (plus de-staled Shots wording). test_scene_exporter.py's carrier round-trip now authors a real lightmap marker and publishes through the producer instead of hand-stamping producer-owned state (which the refresh correctly regenerates away — that's the feature). Suites: emissive_groups 60/60, lightmap_baker 38/38, node_utils 58/58, shadow_rig 63/63, smart_bake 117/117, scene_exporter 38/38.

blendertk v0.5.42

Choose a tag to compare

@m3trik m3trik released this 31 Jul 00:32
  • 2026-07-30 — no Blender analogue of mayatk's intermediate-shape freeze bug (documented, verified). Maya's cmds.instance shares every shape including the deformer's intermediate (orig) shape, which blocks makeIdentity forever (see mayatk's CHANGELOG). Blender has no intermediate datablock — modifiers evaluate off the single obj.data — so preserved_instances / uninstance have nothing equivalent to fork, and the delete_history opt-in the Maya twin needed is deliberately absent (delete_history was already a documented no-op here). Suites re-verified unchanged: test_node_utils.py / test_xform_utils.py PASS headless.

  • 2026-07-30 — NodeUtils.preserved_instances + freeze_transforms(instance_strategy=...) (mayatk parity — see its CHANGELOG for the mechanism and the failure it fixes). Linked-data twin of the Maya context manager: a group is the objects sharing one datablock, the master's "fork" is data.copy(), and restore re-points every sibling at the master's post-op data with its world matrix compensated (B = post_world⁻¹ @ pre_world, sibling W → W @ B⁻¹) — writing matrix_world absorbs parents / parent-inverse / deltas in one assignment, so the Maya twin's pivot re-pinning and instance-edge surgery have no analogue here. The orphaned original datablock is removed and the fork takes over its name. Triage (skipped up front, reported, untouched): library-linked objects/data, and siblings whose transforms are driven (drivers, action fcurves, unmuted constraints — compact yes/no twin of transform_diag._driving_connections). freeze_transforms — where transform_apply refuses multi-user data outright, so "skip" was the only behavior — gains the same instance_strategy="skip"|"preserve"|"uninstance" (default unchanged). fix_non_orthogonal_axes deliberately does not take the option (divergence documented in its docstring): the Blender fix writes only transform channels, never shared geometry, so linked duplicates were already fixed in place with their instancing intact. test_node_utils.py +11 checks, test_xform_utils.py +7; both suites PASS headless.

  • 2026-07-30 — libpyside: Failed to disconnect … from signal console spam from the Reference Manager header and the Channels table (mayatk twins fixed identically). User-reported from a live session (reference_manager.py:246, refresh_requested()). Two defects, both in idempotent signal re-wiring — mandatory here because the QWidget outlives the slots instance across a reload. (1) Blanket signal.disconnect() on a signal with nothing attached: PySide reports that through warnings.warn, not an exception, so the surrounding except (RuntimeError, TypeError) never suppressed it and every first call printed the line — the header's refresh_requested, and channels' four cellScrub* / cellWheelScrolled bindings (which, unlike the cellChanged / customContextMenuRequested blankets beside them, have no owner but us). Both now drop only their own prior connection, tracked per (widget, key). (2) The tracked-disconnect primitive had the same hole: _rewire_signal dropped its stored QMetaObject.Connection through the signal-instance disconnect(), which expects a slot — handed a Connection it can't match (the reload case: PySide breaks the binding when the old slots instance is collected) it warns and returns rather than raising, so the except was dead code and the warning would simply have come back wearing a Connection repr. Verified against live PySide6, then switched to the static QObject.disconnect() — the API that actually takes a Connection — with a falsy (already-broken) Connection skipped outright. test_blender_ui_handler.py 226/226 (its single-connection regression unchanged) with a warning-free run, test_channels.py 56/56, test_reference_manager.py 64/64.

  • 2026-07-30 — Maya-scene import robustness: tolerant scene open, .mb driver probe, namespace-safe translated shaders, and collision-safe visibility replay. Four hardenings of the pull-direction conversion, each closing a real production edge. (1) Tolerant open (both conversion templates): cmds.file(open=...) raises RuntimeError over missing renderer plugins / unknown nodes even when the scene content loaded — routine for production scenes on a vanilla mayapy — and that killed the whole conversion; _open_scene now verifies via cmds.file(q, sceneName) that the scene actually loaded and tolerates the noise (printed), while a genuinely failed open still raises. (2) scene_has_complex_animation now answers for .mb too (was a blind False): node type names are stored as plain byte strings in the binary, so a chunked, overlap-safe token scan over the same _DRIVER_NODE_TYPES (+ the <node>_visibility curve name) gives binary scenes the same bake-vs-raw browser signal — heuristic by contract, since a false positive only costs an unneeded bake attempt (the Maya-side probe re-decides under "auto"). (3) Namespace-safe created nodes (both templates): a referenced scene's materials are namespaced (loadReferenceDepth="all"), and f"{mat}_fbxsafe" put the translated phong INSIDE that namespace — its exported name's colon then at the mercy of FBX name mangling, silently missing the Blender-side slot match; _ns_safe flattens colons so created phong/standardSurface/bump nodes live at root under names that round-trip verbatim. (4) Visibility collision handling: _collect_baked_visibility keyed the manifest by short name and silently last-writer-won on duplicates — and the Blender-side replay matches by short name, so ONE object's curve would land on BOTH; differing duplicates are now dropped loudly (identical ones merge), no keys beating wrong keys. Also: the four engine entry points expand ~ beside $VAR. test_scene_import.py +10 checks (incl. a behavioral run of the extracted collector against a stub cmds, and .mb scans with a chunk-boundary-straddling token), verified failing pre-fix via stashed-source run; suite PASS under both the venv and headless Blender 5.1.

  • 2026-07-30 — MatUtils.find_unassigned (mayatk parity — see its CHANGELOG for the Maya-side subtlety). Objects carrying no material: no material slots at all, or every slot empty. Blender has the real state Maya has to synthesize — nothing here joins a default shading group — so there's no include_default axis; objects that can't hold materials (empties, lights, cameras) are not reported, since they aren't unshaded geometry. Same objects=None pool convention and object-level scope as find_by_mat_id, whose complement it is. Drives tentacle's materials "No Material" select mode. test_mat_anim_utils.py +6.

  • 2026-07-29 — RizomUV temp payloads move onto ptk.TempArtifacts, and the no-save error diagnoses itself (mirrors mayatk — see its CHANGELOG for the diagnosis). This twin used the same two fixed names as the Maya bridge (rizomuv_exported.fbx / riz_uv_script.lua in the system temp dir), so the two panels were racing each other for one script file — and RizomUV re-reads the -cfi script after launch, so whichever run got overwritten exited 0 without ever reaching ZomSave. The round-trip now allocates both payloads from a "scoped" store (clean run deletes them, a failure keeps them so the script can be opened in RizomUV's Script Editor) and the one-way send from a "detached" one (its session may outlive us, so allocation age-sweeps instead of deleting) — replacing a hand-rolled time.time_ns() tag that TempArtifacts documents as not unique enough alone on Windows. _no_save_diagnosis replaces the old message's promise of a Lua traceback (RizomUV prints nothing in -cfi mode) with the script/FBX paths — both rendered OS-native, they used to mix separators — a comparison of the on-disk script against what was written, and a pointer at RizomUV's Script Editor. _release_temp_payloads re-allocates on the next run, since cleanup untracks what it removed and a cached path would leak. test_rizom_construction.py +10 → 34/34.

  • 2026-07-29 — m_frame step cycle + clip fitting (mayatk parity — see its CHANGELOG for the detail); CamUtils gains get_view_state / set_view_state / fit_camera_clipping. Same contract: m_frame(steps=2, adjust_clipping=True), first press frames, further presses within FRAME_STEP_TIMEOUT step in, the last returns the view exactly where it started, and a pause collapses the cycle to a plain there-and-back toggle (shared ptk.StepToggle). Blender-side differences: the ideal distance comes from view3d.view_selected itself (no per-element fit factors) with the step ramp applied as a dolly on view_distance, and the state snapshotted/restored is the RegionView3D (location / rotation / distance / perspective) plus the viewport's clip planes — SpaceView3D.clip_start/clip_end is what a free view actually clips against, not the camera data adjust_camera_clipping writes — so the fit widens whichever surface is doing the clipping (the lens while locked to a camera, else the viewport's planes) against that surface's own current values, and the snapshot carries the lens planes too or the widening would outlive the view it was made for. Smooth view is suppressed for the framing op, which otherwise animates the view over the next ~200 ms and would fight the step dolly. fit_camera_clipping measures depth from view_location/view_rotation/view_distance rather than rv3d.view_matrix, which is only refreshed at draw time and is still the previous view right after a framing op. test_macros.py +8, test_cam_utils.py +9.

  • 2026-07-29 — Shell Xform tri-state snap (off / grid / shell) + UvUtils.get_neighbor_shell_bounds (mayatk parity — see its CHANGELOG for the detail). Same 3-state ActionOption cycle and the same ptk.MathUtils.next_clear_offset geometry, so the two panels behave identically. The pool is every mesh whos...

Read more

blendertk v0.5.39

Choose a tag to compare

@m3trik m3trik released this 29 Jul 19:20
  • 2026-07-29 — Scene-data sidecar (mirrors mayatk's rename, plus the exporter-side write Blender never had). HierarchySidecarSceneDataSidecar (scene_data_sidecar.py, registered as btk.SceneDataSidecar; old module = import shim): the manifest is now format v2 — hierarchy diff baseline + data_export channel snapshot in one .{stem}.scene_data.json, with read_data() and the migrate_legacy() promotion of v1-named sidecars (mayatk's changelog has the full design rationale). New here: the SceneExporter actually writes the sidecar post-export. mayatk maintained its manifest via the hierarchy check's task-manager hook, but blendertk's exporter had no sidecar wiring at all — the check is still a disabled placeholder (TODO(blender-parity), tooltip updated to say precisely that). The write lives on the engine (_write_scene_data_sidecar, same reason as _create_glb: blendertk's TaskManager carries no export_path), is gated on the carrier having actually shipped (present in the export set — a hidden carrier excluded from a selected-mode export records nothing) or an existing manifest to maintain, and is best-effort — the record can never break the export it records. The engine's native _VERSION_SUFFIX_RE re-implementation (its comment claimed the sidecar class "lives in the unported hierarchy_sync subsystem" — stale since the sidecar port) is deleted in favor of SceneDataSidecar.VERSION_SUFFIX_RE. test_scene_data_sidecar.py (was test_hierarchy_sidecar.py) 43/43 headless, incl. v2-format / v1-compat / name-migration checks.

  • 2026-07-29 — Scene Exporter's missing-file check no longer fails an export over textures that never ship (mirrors mayatk). check_valid_paths walked every FILE image datablock in the .blend — its docstring even called the whole-file scope deliberate, "matching Maya's version". The Maya version turned out to be the bug (see mayatk's entry: an HDR environment map and orphaned duplicate-material file nodes flooded a real export log), so both sides move to the export scope. Image scope is now _get_export_images() — the datablocks feeding the materials assigned to self.objects — which drops the World/Environment-Texture HDR and the zero-user images a duplicate-material cleanup leaves behind. Linked libraries stay whole-file: they are the analogue of Maya's scene references, not of a texture. The scoping is applied by filtering MatUtils.get_image_records() against the export set rather than re-deriving "is a FILE image whose abspath exists" locally — that predicate belongs to get_image_records, and a second copy of it in the check would be free to drift. test_scene_exporter.py +2 (an unassigned broken image is ignored; a missing texture on an export material still fails).

  • 2026-07-29 — The "blendertk panels get a hide button" rule is now a default the UI Browser can override (mirrors mayatk). BlenderUiHandler.apply_styles hardcoded style["header_buttons"] = ("menu","collapse","hide") for anything tagged blendertk — a fixed tuple no user preference could outrank, and one of the three reasons the UI Browser's window-persistence setting had no effect (uitk's changelog has the diagnosis). Replaced by uitk's new default_persistence(ui) hook: blendertk-tagged UIs declare PERSISTENCE_STICKY, everything else falls through to the base PERSISTENCE_TRANSIENT — identical defaults, now at the bottom of a precedence chain the browser's global setting and per-window "Open as" override sit above. The gesture-scoped panels that ask for a pin button in header_init are unaffected (the handler leaves a self-configured header alone unless there's an explicit choice, and then swaps only pin/hide). test_blender_ui_handler.py +3 (default is sticky, an override outranks it, apply_styles is no longer overridden). Also fixed a stale assertion in that suite (pre-existing, unrelated): it pinned UnityBridgeSlots.MODE_LABELS to the single {copy: "Copy to Project"} entry from before the panel-native "Manage Unity Scripts" mode shipped — the mayatk twin carries the same two modes, so parity holds and the test, not the code, was wrong; it now asserts both modes plus the still-retired Unity Studio / existing-project ones. Suite back to 226/226.

  • 2026-07-29 — 25 panels declared fixed-vertical (mirrors mayatk): no more stretching into dead space above the footer. Same sweep and same classification as the mayatk entry — a panel is locked only when its trailing spacer is the sole vertical absorber. Those panels' spacers are now sizeType=Fixed with each group box declaring vsizetype="Fixed", making Qt's own QLayout.maximumSize() finite rather than leaning on uitk's container see-through; seven untuned Qt Designer 20x40 spacers are tuned to the 10px norm (a Fixed spacer's hint is its exact, unresizable height). Panels with real growable content stay resizable. Parity with mayatk is preserved panel-for-panel; verified through the real loader (finite native cap, ~10px footer gap, exact collapse/expand round-trips).

  • 2026-07-29 — Shell Xform gains Gather to Tile, and its snap now respects the tile border padding (parity with mayatk). UvUtils.gather_to_udim mirrors the Maya twin: stray islands return to the target UDIM by whole-tile offsets that keep their sub-tile position, clamped inside the shared border margin, with the target decided by majority vote when no UDIM is given. Islands are resolved in two passes (vote, then apply) because the object-mode bmesh is freed when the edit callback returns, so island references must not escape it. The vote pass runs only when no UDIM is given, and the vote itself is ptk.MathUtils.majority_tile. The move pad's grid snap lands islands one margin inside each grid line instead of on it. _island_bounds factored out of _island_bbox_center backs both. A partial selection moves the whole island — free here via _target_islands, and now asserted, because the Maya twin had to be fixed to match.

blendertk v0.5.36

Choose a tag to compare

@m3trik m3trik released this 29 Jul 12:10
  • 2026-07-29 — The Unity Bridge offers to install the optional unitytk instead of failing to import, and CoreUtils.ensure_packages generalises the Pillow provisioning. _unity_bridge.py imported unitytk at module scope, so a user without the optional package hit an ImportError when the module was merely loaded — before the panel existed to explain it. The import is now deferred behind UnityBridge._deliverer_cls(), and the panel's make_bridge calls ensure_optional_package("unitytk", feature="Unity Bridge") (see uitk's changelog) — prompt, install, proceed; decline and the panel simply reports no engine. list_delivery_modes became a classmethod so it resolves the deliverer the same lazy way. CoreUtils.ensure_packages(packages, add_to_path=True) is the general form of ensure_image_deps: same Blender provisioning policy (install into the per-version user-modules dir, already on sys.path, driven against Blender's bundled interpreter — sys.executable is blender.exe and cannot be pip'd) with no assumption about what is being installed. ensure_image_deps keeps its Pillow default and PIL-rebind and now delegates to it, so the shared body lives once; both are backed by _CoreUtilsInternal._ensure_packages. BlenderBridgeSlotsBase overrides _install_optional_package to route through it — necessary because uitk's default is pip install --user, which would succeed and still be unimportable, Blender's user-site not being on sys.path. Verified: test_unity_bridge.py 15/15, test_core_utils.py 13/13, and the package still imports with unitytk blocked (only the Unity panel is affected).

blendertk v0.5.31

Choose a tag to compare

@m3trik m3trik released this 29 Jul 04:24
  • 2026-07-28 — The Qt-only suites now actually run: run_tests.py re-runs a skipped suite under the workspace .venv, which immediately surfaced a broken test_blender_native_menus. Headless Blender ships no Qt binding, so seven suites (test_blender_ui_handler, test_blender_native_menus, test_rizom_construction, test_hierarchy_tree_utils, the shot panel/slots harnesses) self-reported ===RESULT: PASS=== (skipped) and the runner printed SKIP and moved on. They were documented as .venv targets, but nothing executed them automatically — 287 checks that only ever ran when someone remembered to run them by hand. run_suite now takes an optional python=, and run() retries any skipped suite under .venv (labelled [venv] in the output), accumulating totals after the retry so the venv result is the one counted; a suite that skips in both environments still reports SKIP. Blender remains the default — the session-safety rule governs the bpy suites, and these carry no bpy import. The gap was hiding a real failure: test_blender_native_menus patched blendertk.ui_utils.menu_harvest.refill_qmenu, a module-level target that stopped existing when the module migrated to the MenuHarvest class (production correctly calls menu_harvest.MenuHarvest.refill_qmenu). mock.patch resolves its target string at call time, so the stale name passed import and collection and only raised AttributeError when the suite was finally executed — the same moved-symbol class the ecosystem has been bitten by before. Repointed to mock.patch.object(menu_harvest.MenuHarvest, "refill_qmenu"), which binds the attribute at patch time and so cannot silently rot again.

  • 2026-07-28 — Smart Bake spacer parity (see mayatk's CHANGELOG for the detail). smart_bake.ui's trailing verticalSpacer flipped from the implicit Expanding to Fixed at height 0, matching the Maya SSoT, so the collapsed output group snaps flush to the footer instead of leaving a dead band. The underlying adjust_height_by double-count lives in uitk and is fixed there for both DCCs. Measured through the real BlenderUiHandler load (Qt-only, no bpy): collapsed gap 2 px in every state, window snapping to 254 with maximumHeight locked when collapsed and free when expanded — identical to the Maya panel. test/test_smart_bake.py 14/14.

  • 2026-07-28 — Shell Xform move-pad scope + snap (mayatk parity — see its CHANGELOG for the detail). gridMove and the <customwidgets> block are lifted verbatim from the Maya SSoT, so the two move pads cannot drift by hand-editing; parity_map needed no new entry (sweep clean). New UvUtils.get_uv_bounds(objects) -> (u_min, v_min, u_max, v_max) mirrors mayatk's, read off the active UV layer via _uv_read as a running min/max rather than collecting every loop UV — the move pad calls it per arrow press. The arrow slots now route through _move, which resolves the scope, reads the cached snap toggle, and calls ptk.MathUtils.step_offset — identical logic to the Maya twin. _snap_enabled deliberately does not import the Qt option-box internals: headless Blender has no Qt binding, and shell_xform_slot_check.py drives _move there (+9 checks: three scopes, opposite-arrow cancellation on both axes, Selection Bounds, snap on/off). test_uv_utils.py +3 (bounds order, bounds track a move, empty selection → None), and the slot check asserts the _MOVE_SCOPES shape now that the combo is built from it rather than from the .ui.

  • 2026-07-28 — Render Opacity's Fade Direction tooltip lost half a bullet to an unescaped <. "… ≥ 0.5 → fade out; if < 0.5 or no key → fade in." is handed to Qt, which auto-detects the surrounding fmt() output as rich text — so the bare < opened a tag and everything up to the next > (the bullet's own </li>) was swallowed: the Auto entry rendered as a truncated fragment. Now &lt;, with a comment saying why. Caught by a static pass that evaluates every fmt() call in the ecosystem and parses the result; all 224 literal tooltips across mayatk / blendertk / tentacle / extapps now come out well-formed (the pass also has to whitelist named entities like &nbsp; / &mdash;, which Qt renders but XML doesn't predefine).

  • 2026-07-28 — All 42 panels reach the tooltip DSL through self.sb.tooltip instead of importing uitk.widgets.mixins.tooltip_mixin by path (mirrors mayatk's sweep — see its CHANGELOG for the method and the verification). 41 modules here dropped the import, including the guarded try/except ImportError form in arnold_bridge and lightmap_baker and their if TooltipFormat is not None: branches. Mechanical + AST-verified; no behavior change, registries unaffected.

  • 2026-07-28 — Panel tooltip pass (mirrors mayatk's — see its CHANGELOG for the detail). Emissive Groups' 13 tooltips rebuilt through the new self.sb.tooltip Switchboard passthrough (uitk CHANGELOG), which also retires this module's try/except ImportError TooltipFormat guard and the if TooltipFormat is not None: branch around the header help — the panel is a uitk slots class, so the guard could never fire. Same content fixes: the Encoding combo states the 4-group cap as a combo-level note (it comes from the manifest's RGBA slot channels, so it binds both encodings — not a vertex-color property), Remove Group bolds the retired-slot contract, Make Weights Keyable splits the two DCCs' curve transports into a section, Remove Keyable Weights drops the shouty INCLUDING. .ui text: Mirror's chk007 Instance tooltip gains the disabled Bounding-Box-(center)-pivot restriction and names the bake path; Cut On Axis's four weighted-spacing fields (Spacing / Distribution / Bias / Curve) say what their values actually do, written from a measured ProgressionCurves run — which caught a first draft that had Bias backwards and mis-described Ease In-Out and Smooth Step. The smooth_step-ignores-Curve discrepancy in toggle_weight_ui is left standing and documented in mayatk's entry. Text and structure only — the files stay character-identical to their mayatk twins where the parity contract requires it (sweep: 0 deltas).

  • 2026-07-28 — UvUtils.auto_unwrap + a public transfer_uvs (mayatk parity). auto_unwrap(objects, method="hard"|"organic", …) mirrors mayatk's: each mesh exports to OBJ, is unwrapped by ptk.UvUnwrap (Ministry of Flat for hard surface, Boundary First Flattening for organic) and gets its UVs transferred back, inside one undo step with the prior mode/active/selection restored and every imported object plus orphaned mesh datablock removed. Per-object failures roll that mesh back via get_uv_coords/set_uv_coords and are reported rather than raised; the engine executable is resolved before the scene is touched, so a missing engine can't leave a half-run. Both engines preserve topology exactly, so the transfer is an exact per-loop copy and nothing is triangulated. Layout is per engine — Ministry of Flat's own island arrangement is kept and only scaled into the tile by the new data-level _fit_uvs_to_tile (no operator, so it runs headless), while BFF, which only flattens, gets the full _pack_shells pass. transfer_uvs is now public, closing a standing parity gap: the exact-copy / data_transfer spatial-fallback pair logic the RizomUV bridge carried privately is now one implementation on UvUtils, and the bridge delegates to it (keeping only its logging). Verified against both real engines in Blender 5.1 (topology preserved, UVs inside 0-1, no leftover objects); test_uv_utils.py green, rizom round-trip green.

  • 2026-07-28 (parity batch) — Thirteen Maya-parity gaps closed in one sweep: every ledgered pending item built (or decisively closed), plus three rolled subsystems Blender doesn't ship. New engines: SelectionOrder (ordered OBJECT selection — a depsgraph-diff click tracker + reorder_objects sort methods; Blender natively records component order only) backing the un-hidden Reorder Selection menu; CameraVisibility (per-camera exclusive/hidden isolate sets as camera id-props, derived from scene.camera with an exactly-reversible stash + optional msgbus auto-re-apply — Maya's SetExclusiveToCamera family) backing six new cameras-menu leaves; and the NurbsUtils CV-deformation family (bend_curve arc-length-exact circular bend, curl_curve, scale_curvature, straighten_curve, rebuild_curve uniform arc-length CV redistribution, extend_curve end-tangent extension — pure control-point math; the probed SIMPLE_DEFORM route stays refuted) backing six formerly-dead nurbs list leaves. Engine upgrades: EditUtils gained propagate_normals/conform_normals/extract_reversed_faces (Maya's five polyNormal modes now 1:1 in the normals menu), combine_objects/separate_objects gained uninstance=True (both linked-duplicate corruption hazards VERIFIED real headless — join mutates the sibling sharing the active's datablock; separate deletes sibling faces), and cut_along_axis gained spacing/distribution/weight_bias/weight_curve via the shared ptk.ProgressionCurves (linear default reproduces the old even-fill exactly). UvUtils gained scale_uvs (whole-map scale about a pivot) + transfer_uvs_to_similar (bbox-volume+vertex-count fan-out with TOPOLOGY data-transfer — the analogue of mtk's component-space transfer; linked duplicates of the source skipped). XformUtils.transfer_pivot gained mirror= (reflect the source origin across a world axis-plane before the ORIGIN_CURSOR snap). TelescopeRig gained aim_axis (signed x/y/z → Damped-Track enums + driven scale channel + off-axis locks, mirroring mtk's _resolve_axis) with the Aim Axis combo added to its .ui. Decisively closed, not built: Bake Inherited Visibility — Collection.hide_viewport/hide_render are NOT animatable (is_animatable=False; keyframe_insert and driver_add both refuse, verified live 5.1), so no animated inher...

Read more

blendertk v0.5.28

Choose a tag to compare

@m3trik m3trik released this 26 Jul 04:34
  • 2026-07-26 — Encapsulated _mat_utils.py's 15 loose module-level helpers onto a _MatUtilsInternal base (mirror of mayatk); closes a blendertk-vs-mayatk divergence. MatUtils and MatUpdater now inherit _MatUtilsInternal (the gold-standard _<Class>Internal pattern), so _is_gp_material / _abspath / _principled_node / _resolve_images / … are @staticmethods reached as _MatUtilsInternal._x(...) rather than loose module functions. The cross-module importers (mat_manifest, render_opacity, scene_exporter/task_manager) and the texture_path_editor test were updated to match; module constants (SHADER_TEMPLATES, _PBR_DIRECT, …) stay module-level. No behavior change — private helpers only, no public-surface change beyond the two class base annotations. Verified in fresh Blender 5.1: test_texture_path_editor (13/13), test_render_opacity (26/26), test_scene_exporter, test_mat_anim_utils — all PASS.

blendertk v0.5.26

Choose a tag to compare

@m3trik m3trik released this 26 Jul 03:23
  • 2026-07-25 — BREAKING: removed the flat wrappers btk.import_maya_scene, btk.bake_maya_scene, and btk.auto_instance — call the classes instead (mirrors mayatk). Migration: btk.import_maya_scene(p, **kw)btk.MayaSceneImport().import_scene(p, **kw); btk.bake_maya_scene(p, **kw)btk.MayaSceneImport().bake_scene(p, **kw); btk.auto_instance(objs, **cfg)btk.AutoInstancer.run_once(objs, **cfg) (new @classmethod, config via **config). Internal callers (reference_manager) + tests migrated — the ui_handler reference-routing mocks now patch the instance methods (MayaSceneImport.import_scene/bake_scene) rather than the deleted module functions. Verified in fresh Blender 5.1: test_auto_instancer, test_scene_import, and test_blender_ui_handler (222/222), 0 failures.

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-w...

Read more

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-t...

Read more