Skip to content

blendertk v0.5.48

Latest

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 itemChanged was wired inside the one-time if not is_initialized block with no refresh_on_show, the exact latent gap the channels panel was fixed for: the QWidget outlives the Slots instance, so after a UI reload every cell edit was bound to a dead self and silently no-op'd. Both tbl000 and txt000 now set refresh_on_show and route their connects through an idempotent _rewire (disconnect-then-connect), with only widget-state setup left in the one-time block.
    • Root field is debounced. textChanged fired per keystroke, and the handler reloads the rule table and pins the session's current workspace — so typing a path pinned every partial path that happened to exist along the way. A plain defer_with_timer would not have fixed it (QTimer.singleShot stacks rather than debounces), so the deferred call carries a generation stamp and all but the last no-op. Enter still commits immediately.
    • Create Project is now an explicit header-menu verb. Building a project only ever happened as a side effect of editing a rule — open_folder even said so ("edit a rule to create it"), which is a strange thing to tell someone who just asked for File ▸ Project Window ▸ New.
    • Templates moved to ptk.WorkspaceTemplates — the shared unnamespaced store — so a template saved here also builds Maya projects (mtk.create_workspace). btk.list/save/delete_workspace_template and workspace_template_rules are now thin mirrors of it; save_workspace_template(name) with no rules captures the current workspace's own rules, matching the mayatk twin.
    • promote_workspace / current_workspace shed their hand-rolled halves. The layout-describing heuristics and the path→workspace resolver tail were duplicated verbatim in mayatk; both now delegate to the new ptk.Workspace.promote / .for_path (see pythontk's CHANGELOG), so the two DCCs can no longer describe the same folder differently.
    • test_workspace.py +10 → 46; reference_manager (76) and blender_ui_handler (226) re-run green. Parity sweep exit 0.
  • 2026-08-02 — Freeze/unfreeze audit (mayatk parity): the axis fixer stamped no history, two origin moves left a STALE location bake, and the stored history is now readable + prefixable. Blender half of the same sweep; see mayatk's CHANGELOG for the shared design.

    • fix_non_orthogonal_axes recorded nothing. parent_clear(CLEAR_KEEP_TRANSFORM) / visual_transform_apply() rewrite the local transform without going through freeze_transforms, so an axis fix was irreversible — while the mayatk twin has always stamped. It stamps all three channels now (these ops write the WHOLE local transform, unlike Maya's rotate/scale-only freeze).
    • Stale location bakes. Blender's origin IS the translate reference, so ANY origin move invalidates a stored freeze-location bake — leaving one makes a later Un-Freeze double-apply the translation. center_pivot already dropped it, but EditUtils.separate_objects(center_pivots=True) and tentacle's Bake Pivot called bpy.ops.object.origin_set directly and skipped the drop. The invalidation is now _XformUtilsInternal._invalidate_location_bake and both call sites route through XformUtils.center_pivot, which gains a "cursor" mode (Maya's Bake Pivot) so the slot has a sanctioned path. center_pivot is documented as the only origin-move entry point.
    • Bake keys take a prefix (mirroring mayatk): _bake_keys(prefix) backs _store_bakes / restore_transforms / has_stored_transforms, so a tool that re-bases a local frame for its own bookkeeping can never compose onto the user's freeze history. New public XformUtils.store_transforms (the stamp side, which blendertk lacked entirely) and get_stored_transforms (the read side).
    • New "original" pivot mode on get_operation_axis_matrix: rebuilds the pre-freeze frame as matrix_world @ stored (column-vector convention), falling back to "object" when unstamped. A frozen object's local axes are the world's, so "object" alone could not express the frame the asset was authored in.
    • New restore_original_axes — Maya aims its manipulator pivot; the Blender parity is a custom transform orientation (the same slot World-Aligned Pivot flips to GLOBAL), written from the pre-freeze frame and made active. Non-destructive; no-ops headless, where create_orientation has no screen context. Surfaced as "Restore Authored Axes" in the Un-Freeze option box.
    • Auto-instancer center_transform_on_geometry / canonicalize_transform stamp under CANONICAL_BAKE_PREFIX, so the PCA re-basing no longer destroys the authored frame unrecoverably.
    • freeze_transforms now RETURNS the objects it actually baked (mirroring restore_transforms), empty when everything was skipped. Without it a caller cannot tell a fully-skipped multi-user batch from a successful one — the engine skips linked data with a console message where the raw operator used to raise, so tentacle's World-Aligned Pivot button would have looked like it did nothing. _bake_keys moved onto _XformUtilsInternal (no root-level functions).
    • test_xform_utils.py +13, test_diagnostics.py +2. Full suite 3219/3219; panel parity sweep clean.
  • 2026-08-02 — Unity Bridge: Manage Unity Scripts lists the C# scripts as a checkable set, and the Action applies to whatever is checked. Mirrors mayatk 1:1 (same parameter registry entry, same slot methods; compare_panel_surface --panel unity_bridge clean). New SCRIPTS parameter (uitk's check_list kind) fills itself at panel init from unitytk.TemplateDeployer.components() — one checked row per import channel with its own tooltip — so Install / Update deploys only the checked controllers (the shared core files ride along), Uninstall removes only those (dropping the package folder with the last one), and Status reports every component's state regardless of the checkboxes. _manage_unity_scripts keeps only the panel-side preconditions; the work and its wording come from TemplateDeployer.run_action, shared with the mayatk and extapps Unity panels. Verified building the panel outside Blender (Qt-only): rows, default checks and the manage-mode gate all match mayatk's.

  • 2026-08-02 — Game Shader: 1:1 parity with mayatk — the map pipeline is live, and the two dead controls are gone rather than faked. The Blender panel shipped four disabled widgets; three of those reasons were wrong or stale. Output Template (cmb002) and Ext (cmb003) are now ENABLED and wired. The ledger claimed Blender "wires existing textures and never writes output maps", but the pipeline behind both combos is ptk.MapFactory.prepare_maps / ptk.MapRegistry, which live in DCC-agnostic pythontk, and the sibling MatUpdater in this very package already ran it. A new GameShader engine class (mat_utils/game_shader.py, registered as btk.GameShadermtk.GameShader) now mirrors create_network name-for-name: resolve a workflow profile (Unity URP/HDRP, Unreal, glTF, Godot …), run prepare_maps (format conversion, resize/optimize, packed-map generation), resolve one source per input, and delegate the graph build to MatUtils.create_pbr_material. Pillow is provisioned on demand by btk.ensure_image_deps; without it the profile/format steps are skipped with a warning and the textures wire as-is — degraded, never a hard failure. Verified live on 5.1.2: an "Unreal Engine" profile over loose BaseColor/Normal/Roughness/Metallic/AO now writes a packed _ORM.png and wires it, which the panel could not do before.
    Correctness fix. create_pbr_material decided packed-vs-loose with a local chain (ORM pops MSAO pops Metallic_Smoothness) that was a blendertk-only invention. It now calls the SHARED ptk.MapFactory.filter_redundant_maps through a new MatUtils.resolve_pbr_plan — the same SSoT mayatk's _resolve_map_conflicts uses — so the two DCCs cannot drift, and the shared filter is lossless: a channel no surviving loose map covers is extracted to a real file instead of discarded (an MSAO beside loose Metallic/Roughness no longer loses its AO). Two behavior changes fall out, both toward Maya: ORM and MSAO are different channel layouts and neither supersedes the other (the old chain wrongly dropped MSAO), and reporting now distinguishes superseded / unrecognized / classified-but-unwirable maps. Reporting reached parity too: config banner, per-map table with conversion notes, batch summary, progress_callback → footer, and a clickable material link via a new graph action on UiUtils.dispatch_log_link (opens the Shader Editor — the analogue of Maya's Hypershade select).
    Four defects the shared-filter switch exposed, all fixed with tests. (1) The registry lets a packed MRAO supersede loose Metallic/Roughness/AO — but create_pbr_material had no MRAO consumer, so such a set would have dropped its loose maps and wired nothing in their place; MRAO is now wired (R→Metallic, G→Roughness, B→AO) like ORM. (2) ORM and MSAO can now coexist (neither supersedes the other) and each multiplies AO into Base Color, and _multiply_into_base chains onto the existing link — two surviving packed maps stacked two MULTIPLY nodes and darkened the albedo twice; AO is now applied once. (3) The per-map table inferred “connected” from the plan, so a map the wiring deliberately skips because another already drives its input (Height beside a Normal, Glossiness beside a real Roughness) was reported ✓ — precisely the silent drop the table exists to prevent. create_pbr_material now records the map types it actually wired into plan["wired"] (keyed off the image-load site, so there is no per-site bookkeeping to drift) and those rows report – <Owner> already drives that input. (4) An explicitly tagged Normal_OpenGL map was green-flipped whenever the panel's combo said DirectX, inverting an already-correct map; a filename tag now outranks the combo, which only decides the ambiguous Normal case (mayatk was already correct here — it prefers a map in the desired format rather than flipping unconditionally — so this was a Blender-only bug and the reference DCC is untouched).
    chk000 (AiBridge) and cmb004 (Shader Type) were REMOVED from the .ui, not left disabled. AiBridge attaches a parallel aiStandardSurface preview network; Blender ships no Arnold integration. Shader Type picks a Maya shader node — and probing Blender 5.1.2 settled the OpenPBR question: there is no OpenPBR node (0 of 101 shader node types), but Principled is the OpenPBR model — its inputs map onto ND_open_pbr_surface_surfaceshader, MaterialX 1.39.4 ships bundled, and Blender's own USD export emits ND_open_pbr_surface_surfaceshader for a Principled material, with every conversion node this tool builds translating cleanly (MixRGBND_mix_color3, InvertND_subtract_color3, SeparateColorND_extract_color3, NormalMapND_normalmap_float). So OpenPBR is reachable, and what the Maya combo really selects is the downstream target — which on this panel is cmb002. A Blender shader-type combo would be a second control for the same decision, with two items naming Maya shaders that are never created; a dead control that says "Stingray PBS" misleads a Maya user more than its absence does. Both are ledgered na in tentacle/docs/parity_map.py; the panel now has zero disabled widgets and the sweep reports 0 untriaged / 0 prop deltas / 0 item deltas.

  • 2026-08-02 — Telescope Rig: parity pass with mayatk — optional handles, two-segment struts, auto collapse, a real teardown, and a build that no longer teleports the segments. Ported alongside the mayatk change (see its CHANGELOG for the shared design). Blender-specific work: the build had no teardown at all (Maya has had one since the 2026-07-18 overhaul), so it now records a TelescopeRigBundle — constraint names, driver paths, created Empties, and the prior locks/location/scale it overwrote — returns it instead of the bare segment list, stamps it as JSON on the base handle (telescope_rig_data), and exposes teardown/scene_bundles/find_bundles + a btn_remove slot. Three behavior fixes: (1) every segment was positioned by Copy Location with no offset, so the build snapped each segment's origin onto the lerp line — harmless when the handles happened to sit on the segment pivots, actively destructive now that omitted handles are auto-placed at the strut's outer ends. End segments now ride their handle with Child Of (scale channels off — Maya's parentConstraint drives translate/rotate only) and interiors solve the offset that keeps the build pose exactly, o = (pose - frac*end)/(1 - frac) - base, written to the pre-constraint location because that is what use_offset adds; teardown restores it. (2) The scale driver was dist / initial, which kept shrinking toward zero past full collapse — Maya clamps there via a constant pre-infinity — so it is now s0 * max(dist, collapsed) / initial, which also picks up the build-pose scale the old expression assumed was 1.0. (3) Selection roles no longer come from "active + farthest": Empties are handles, everything else is a segment, segments order by distance from the active object (ties broken by descending length, since a nested strut's outer sleeve is always the base), and a lone handle is assigned to whichever end it sits nearer. RigUtils.copy_location/copy_rotation/damped_track/track_to/child_of gained **props passthrough for this. A fourth fix landed in the follow-up review: a second Build on already-rigged objects silently double-rigged them. Blender has no equivalent of Maya's "channel is already connected" pre-flight — constraints and drivers simply stack — so the rebuild wired a second set on top and overwrote the stamp, stranding the first bundle's nodes with no way to reach them. Both engines now refuse by name via find_bundles. The hand-rolled oriented-bbox projection also went away in favour of XformUtils.get_bounding_box(world_space=True): it was a divergence (mayatk measures a world AABB via exactWorldBoundingBox), so a rotated segment produced a different auto handle and collapse distance in each DCC — and the helper degrades to a zero-size box at the object's own position for an Empty, so the special-casing that needed is gone too. Panel gained btn_remove + the Auto collapse spinbox from the twin .ui, and _init_tooltips (previously Maya-only). Headless suite 77/77; full blendertk suite 3191/3191 across 86 suites.

  • 2026-08-02 — Texture Path Editor: Normalize Paths did nothing (or relocated files and left the path absolute) in any project layout where the .blend is not at the project root; found auditing the panel against its mayatk twin after a user report. MatUtils.to_project_relative tested containment against the .blend's own folder, but the panel's project folder — footer, Open Textures Folder, and Normalize's copy/move destination — is the workspace's sourceImages dir (EnvUtils.source_images_dir()). In the standard layout (<root>/scenes/shot.blend beside <root>/sourceimages/) those are different roots, so every texture the panel manages read as "external": Leave external untouched rewrote nothing at all, and Copy/Move into the project relocated the files into sourceimages but left the path absolute (to_project_relative returned the input unchanged, and the follow-up "relative" pass could not fix it either) — the exact inverse of the command's purpose. Containment is now tested against the workspace root (EnvUtils.workspace_root()), with the result still computed against the .blend as Blender's // requires: //../sourceimages/tex.png is valid and is precisely what mayatk's sourceimages-relative rewrite corresponds to. The .blend's own folder always stays in scope, so a workspace pinned elsewhere can never un-relativize a texture sitting beside the .blend, and an unmarked project (root == blenddir) behaves exactly as before. Same root cause fixed in all five repath paths, which all route through this helper (Normalize, Set Directory, Find & Copy, Resolve Missing, row Browse for File). Two more defects in the same guard: os.path.commonpath compares case-sensitively, so a differently-cased blend dir left Windows paths absolute — the new _MatUtilsInternal._is_within normcase-folds both sides and answers False cross-drive instead of raising (normalize_texture_paths' own inside-project test used the raw commonpath too), and Normalize's copy/move never reload()ed the relocated image the way the other three relocate ops do. Four behavior gaps closed alongside it, all mayatk parity: Resolve Missing now refuses an ambiguous multi-hit (the index kept the first path the walk reached, silently binding to an arbitrary one of two same-named textures — mayatk warns "'{match}' resolves to N files, skipped"; index values are path lists now and the whole strategy cascade moved into _resolve_replacement, which stops rather than falling through to a looser tier); Find & Copy picks the NEWEST duplicate rather than the shallowest (a recursive walk routinely turns up versioned / archived / cloud-conflict copies); a row selection that resolves to no live image no longer silently escalates to scene-wide scope (mayatk's _get_scope_nodes distinguishes "nothing selected" from "selection with no valid nodes"); and set_texture_directory skips a no-op repath instead of counting it as an update. Panel polish to match: all four directory dialogs and Browse for File open on the project's textures folder (Browse prefers the texture's own folder, as in Maya), and Open Textures Folder uses ptk.FileUtils.open_explorer — cross-platform, already a dependency — with a missing-directory message instead of a Windows-only bare os.startfile. test_texture_path_editor.py +12 checks (the pure to_project_relative truth table incl. case-folding and out-of-project, an end-to-end marked-workspace <root>/scenes + <root>/sourceimages normalize, copy-mode landing relative, the ambiguity refusal, newest-duplicate selection) → 28/28; full blendertk suite 3164/3164 across 86 suites; compare_panel_surface --panel texture_path_editor 0 untriaged.

  • 2026-08-02 — Tube Rig: re-anchoring a tube end now REPLACES its previous anchor instead of stacking (mirror of the mayatk fix landed the same day). Same defect class in both DCCs, found reviewing rig_utils/tube_rig.py. On the Blender side RigUtils.add_bone uniquifies a colliding name (ra_anchor_endra_anchor_end.001, per its own docstring) and RigUtils.child_of appends a constraint, so a second Add End Constraints on the same end left a dead deform bone still painted on the mesh (its falloff vertex group intact) and the end control carrying two CHILD_OFs — following both anchors at once. New TubeRig._clear_end_anchor removes the prior anchor bone, its vertex group, and the prefix-named CHILD_OF before the replacement is grafted; the CHILD_OF is now named off the bone (<bone>_child_of) so the sweep can find it, and the end control is resolved before the sweep (it comes from the Spline IK constraint + curve hooks, both independent of the anchor assembly). Unlike Maya's removeInfluence, no weight restoration is needed: apply_falloff_weights scales every other group on an affected vertex by the same (1 - w), so dropping the anchor group leaves the remainder in their original proportions and armature deform normalizes them back. Also hardened while there — the end label now handles a mid-chain bone_index (previously any non-zero index was labeled end, so anchoring a middle bone would collide with the real end's assembly), and head_radius is read before the sweep's EDIT-mode round trip rebuilds the bone collection. test/test_tube_rig.py +4 checks (bone replaced not uniquified, one anchor bone / one vertex group survive, the replacement tracks the LATEST anchor) → 47/47 headless.

  • 2026-08-02 — Shadow Rig lifecycle hardening (from a full both-DCC review of the rig): failed builds roll back, re-creating uniquifies instead of hijacking, the footprint measures the evaluated depsgraph, bake/delete skip linked planes, and a canceled preview repairs the carrier + PNG. Five Maya-parity gaps, all on the Blender side (the mayatk engine already had each behavior). (1) create() rollback: a build that failed after the source/contact/plane existed (e.g. a mesh-less target dying in the silhouette gather) left them orphaned — mayatk's node-diff rollback had no twin; now a pre-build name snapshot of objects/meshes/materials/images is diffed on failure, everything new is removed (objects first, so dependents drop to zero users) along with any half-written PNG, and the exception re-raises. (2) Name-collision semantics: the naming base is now uniquified against existing *_shadow_grps (Maya's exact rule) — previously a second create() on the same target REUSED the first rig's material by name, and create_material's nodes.clear() killed that rig's live opacity driver while the new silhouette overwrote its PNG. Preview refreshes are unaffected (rollback frees the names each pass, which is what the reuse-by-name existed for — that path still reuses). (3) _world_bounds reads the EVALUATED depsgraph: plane size / contact / light-bearing came from the raw bound_box while the silhouette gather was already evaluated, so a modifier (array/mirror/subsurf) widened the shadow picture but not the plane under it — Maya's exactWorldBoundingBox is post-deformation. (4) Linked-library guard: bake_planes / delete_rigs now skip (with a warning) planes with library/override_library set instead of erroring mid-batch on the read-only datablocks — mirror of mayatk's referenced-plane skip. (5) Preview cancel left out-of-snapshot residue: Preview's rollback removes created objects, but a PRE-EXISTING data_export carrier kept the mutated shadow_metadata prop (the Maya slots snapshot it via contract.record_modification) and the previewed silhouette PNG stayed on disk (Maya: contract.add_file). Preview gains an opt-in restore_func hook — called once after the rollback that DISCARDS a preview (un-check or failure), never on commit — and the shadow slots use it to republish the channel from the restored file state and remove the orphaned PNG (unless a surviving plane still references it). Also: the module/producer docstrings no longer claim "no before-FBX-export hook" (stale since the 2026-08-01 _KNOWN_PRODUCERS export-time refresh), and the resolution combo's 8096 typo is now 8192 in both DCCs' .ui (NPOT texture; mayatk CHANGELOG has that entry). test_shadow_rig.py +10 checks (rollback leak-diff, uniquify + first-rig-driver survival, evaluated Array-modifier footprint, linked plane found-but-skipped by bake and delete) → 73/73; test_preview.py +5 (restore_func fires exactly once on discard and on the failure-disable path, never on enable/refresh/commit/already-off) → 37/37; panel parity sweep 0 deltas; API registry regenerated (Preview.__init__ signature).

  • 2026-08-02 — Color ID applied colors that rendered nowhere; the Wireframe channel is now real and Maya's outliner tint is documented as impossible (user-reported). The port wrote every channel correctly (the engine suite was green) and yet nothing changed on screen, because writing a channel is only half the job in Blender: Solid shading defaults to color_type='MATERIAL', so an obj.color or vertex-color write renders nothing until the viewport is pointed at that source — Maya's outliner/wireframe tints need no such switch, so the straight port looked inert. Four fixes. (1) ColorId.show_channels(channels) points every VIEW_3D at the channel being applied (color_type OBJECT/VERTEX/MATERIAL — MATERIAL last in precedence since it is already the default), called from b001 after the write. The shading mode is forced only where the channel genuinely cannot draw without it: OBJECT/VERTEX need Solid, a wireframe-only pass accepts Solid or Wireframe, and a Material pass forces nothing at all — an ID material is exactly what Material-preview and Rendered already show, so yanking a look-dev viewport back to Solid would cost the user their view and gain nothing. (2) The Wireframe channel (chk012) is enabled and wired — the parity ledger's "Blender has no per-object wireframe color" was factually wrong: View3DShading.wireframe_color_type takes ['THEME','OBJECT','RANDOM'] and 'OBJECT' draws the wires in obj.color (probed live on 5.1.2). Blender keeps ONE per-object tint where Maya keeps two, so chk012/chk013 share the obj.color write and select where it is drawn; defaults now match Maya's exactly (both checked) and the na ledger row is gone. (3) ColorId.has_object_color — Blender ships no "use object color" flag (Maya's useOutlinerColor), so the untouched default white is the only "unset" signal; without it get_object_color never returned None, which made b003 Get Color's material/vertex fallback dead code (it handed back white forever) and let Select-By-Color on a white swatch sweep up every untouched object. (4) Undo + redraw: Set Color / Reset now run inside CoreUtils.undo_chunk (a raw bpy.data write pushes no undo step, so an ID pass — including Material, which replaces the object's material slots — was unrecoverable), and all three actions tag the viewports via the new CoreUtils.tag_redraw(area_type=None) (all area types when omitted; get_areas gained the same None = every-type mode), since a write from the Qt event-pump timer does not repaint on its own. StyleSetter._redraw_all — the same window-manager walk, hand-rolled — now delegates to it; the target_weld / cam_utils context.area.tag_redraw() calls deliberately do NOT, since inside a modal operator that context IS the area being drawn into and redrawing every window would be both wrong and wasteful (noted on the helper). The Outliner channel is now REAL, built on Blender's one outliner-color mechanism. Probed live: bpy.types.Object exposes no per-object outliner color (color_tag/outliner_color/use_outliner_color all absent) — Collection.color_tag (8 theme swatches tinting a collection row) is all the outliner offers. So chk013 keeps Maya's Outliner label and semantics via ColorId.set_outliner_color: objects are linked into a color-tagged ID_<HEX> collection (home memberships kept — multi-linking is a normal Blender idiom; a recolor moves between ID collections and empties are garbage-collected), with the tag quantized to the nearest of the 8 theme swatches (collection_tag_colors reads the live theme, factory fallback) for display while the exact color is stamped on the collection (btk_color_id custom prop) so get_objects_by_color(check_outliner=True) and Get Color round-trip losslessly — without the stamp, quantization drift would push a just-applied color past the select-by threshold. Every consumer trusts the stamp, never the ID_ name: reset spares a user's own ID_* collection, and the find-or-create lookup is stamp-keyed too — a user collection holding the exact ID_<HEX> name is not adopted/retagged/swept, the tool just creates alongside it (Blender .001-suffixes). Reset also re-homes an object whose ID collection became its only membership (scene-root link before the unlink) — a zero-collection object is orphaned data, silently gone from the view layer. The engine's obj.color-as-solid-fill channel keeps no checkbox (Wireframe covers the obj.color write; show_channels({"object": True}) stays available programmatically), and the panel's apply/reset/select redraws broadened to every editor so the outliner repaints. All four channels now match mayatk by objectName, label, and capability. test_color_id.py +38 checks (unset-vs-assigned object color, white select-by-color skipping unassigned objects, each channel's viewport switch incl. precedence and the mode-forcing rules, tag_redraw, and the full outliner-channel lifecycle: link/home-kept/nearest-tag/exact-round-trip/recolor-moves/reset-spares-unstamped/name-clash-not-adopted/orphan-re-homed/empty-batch-no-op) → 51/51; compare_panel_surface --panel color_id 0 untriaged / 0 prop deltas.

  • 2026-08-01 — The Reference Manager no longer asks to discard "unsaved changes" on a brand-new empty scene (user-reported). _confirm_discard_unsaved guarded open / close / reference-toggle on bpy.data.is_dirty, which Blender derives from the undo stack, not from authored content: one click in the viewport pushes an undo step and marks a never-saved, object-less scene dirty — so the panel prompted with literally nothing to lose (reproduced live in 5.1: a single ed.undo_push under a VIEW_3D override flips the flag on a read_homefile(use_empty=True) scene; not reproducible headless or from a timer, where undo pushes have no context). The decision moves into the engine as EnvUtils.scene_has_unsaved_changes — a file that exists on disk trusts the flag, a never-saved one counts only when it holds something, per the new EnvUtils.scene_has_content (any object beyond the default document's bare camera + light, any linked library, or any text datablock — an unsaved Text Editor script being the one authored thing that leaves no object behind; deliberately conservative, so a startup cube being modeled on still counts). Maya's twin needs no correction — cmds.file(q=True, modified=True) tracks real edits — so these are Blender-only helpers, not a parity gap. test_reference_manager.py +12 checks (the pure _is_unsaved_work truth table, the content probe over empty / camera+light / mesh / linked scenes, and the panel staying silent vs. prompting-and-honoring-"No") → 75/75; the fix re-verified in a live GUI Blender.