-
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-taggedID_<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: noObject.color_tag/outliner_color/use_outliner_color, andSpaceOutliner's whole RNA surface is filters and display toggles — no tree, rows, or expansion state). So the color is painted. Newdisplay_utils/outliner_tint.py: the color is stored as abtk_outliner_colorcustom property (ordinary data — saves with the .blend, readable without any of the below), and aSpaceOutlinerPOST_PIXELdraw handler walks the outliner's internalTreeElementtree 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, fromoutliner_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 atSpaceOutliner+0xC0).TreeElementis 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 realbpy.datanames, else it stands down), every dereference passes an OS memory-protection query (_MemGuard, region-cached — a bad offset yieldsNone, 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, stampedmtk_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 ownID_*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_id0 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 fromui_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 thedraw_handler_removeto 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 harnesstest/outliner_tint_gui_check.pycovers what--backgroundcannot (draw handlers never fire headless): live-process calibration reachingstatus == "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
importtemplate 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+ pinnedaddmode +returnNewNodes(mirror of the pull engine's_import_fbx— sticky global import options and the factorymergemode 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._producenow writes the same.manifest.jsonthe 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_materialsfor 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.pytemplates) — guarded, so a Maya without mayatk keeps the FBX materials with a console line saying so. Skipped whenINCLUDE_MATERIALSis off. - Launch env. The launched Maya inherited Blender's whole environment; an
OCIOpointing 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_env→MayaBridge._launch_envstrips OCIO via the newptk.AppLauncher.handoff_envexactly when it points inside Blender's own install (root frombpy.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 asgrp/subplain groups,leaf_markerlocator, and a rebuiltstandardSurfacewith 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.
- 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
-
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_DEFAULTSpinnedobject_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. Agrp > sub > meshchain 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.EMPTYis now in the shared default (a Blender Empty is a Maya group; hierarchy is not an opt-in), andBlenderExportMixin._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'sFBXExportSkins -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 returnsgrp > sub > meshintact, 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 aBlenderExportMixin._export_fbxround trip proving agrp > armature > skinned meshchain survives the real producer). Substance and Marmoset are untouched — their_producecallsexport_selection_fbxdirectly 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_atfell back to its cell text — and in Nice Names view the user is looking at labels, so they type one. "Render Data" went intoworkspace.melverbatim asworkspace -fr "Render Data" …, a rule name Maya does not recognize. New_key_for_labelreverse-mapsptk.RULE_NICE_NAMEScase-insensitively; an unknown label still passes through as a custom rule. - Signals re-wire on every
_init.tbl000'sitemChangedwas wired inside the one-timeif not is_initializedblock with norefresh_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 deadselfand silently no-op'd. Bothtbl000andtxt000now setrefresh_on_showand 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.
textChangedfired 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 plaindefer_with_timerwould not have fixed it (QTimer.singleShotstacks 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_foldereven 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_templateandworkspace_template_rulesare 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_workspaceshed their hand-rolled halves. The layout-describing heuristics and the path→workspace resolver tail were duplicated verbatim in mayatk; both now delegate to the newptk.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.
- Nice-name rule keys. A row added via Add New File Rule has no stored key, so
-
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_axesrecorded nothing.parent_clear(CLEAR_KEEP_TRANSFORM)/visual_transform_apply()rewrite the local transform without going throughfreeze_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_pivotalready dropped it, butEditUtils.separate_objects(center_pivots=True)and tentacle's Bake Pivot calledbpy.ops.object.origin_setdirectly and skipped the drop. The invalidation is now_XformUtilsInternal._invalidate_location_bakeand both call sites route throughXformUtils.center_pivot, which gains a"cursor"mode (Maya's Bake Pivot) so the slot has a sanctioned path.center_pivotis 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 publicXformUtils.store_transforms(the stamp side, which blendertk lacked entirely) andget_stored_transforms(the read side). - New
"original"pivot mode onget_operation_axis_matrix: rebuilds the pre-freeze frame asmatrix_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 toGLOBAL), written from the pre-freeze frame and made active. Non-destructive; no-ops headless, wherecreate_orientationhas no screen context. Surfaced as "Restore Authored Axes" in the Un-Freeze option box. - Auto-instancer
center_transform_on_geometry/canonicalize_transformstamp underCANONICAL_BAKE_PREFIX, so the PCA re-basing no longer destroys the authored frame unrecoverably. freeze_transformsnow RETURNS the objects it actually baked (mirroringrestore_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_keysmoved 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_bridgeclean). NewSCRIPTSparameter (uitk'scheck_listkind) fills itself at panel init fromunitytk.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_scriptskeeps only the panel-side preconditions; the work and its wording come fromTemplateDeployer.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 isptk.MapFactory.prepare_maps/ptk.MapRegistry, which live in DCC-agnostic pythontk, and the siblingMatUpdaterin this very package already ran it. A newGameShaderengine class (mat_utils/game_shader.py, registered asbtk.GameShader↔mtk.GameShader) now mirrorscreate_networkname-for-name: resolve a workflow profile (Unity URP/HDRP, Unreal, glTF, Godot …), runprepare_maps(format conversion, resize/optimize, packed-map generation), resolve one source per input, and delegate the graph build toMatUtils.create_pbr_material. Pillow is provisioned on demand bybtk.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.pngand wires it, which the panel could not do before.
Correctness fix.create_pbr_materialdecided packed-vs-loose with a local chain (ORMpopsMSAOpopsMetallic_Smoothness) that was a blendertk-only invention. It now calls the SHAREDptk.MapFactory.filter_redundant_mapsthrough a newMatUtils.resolve_pbr_plan— the same SSoT mayatk's_resolve_map_conflictsuses — 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:ORMandMSAOare 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 newgraphaction onUiUtils.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 packedMRAOsupersede loose Metallic/Roughness/AO — butcreate_pbr_materialhad 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)ORMandMSAOcan now coexist (neither supersedes the other) and each multiplies AO into Base Color, and_multiply_into_basechains 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_materialnow records the map types it actually wired intoplan["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 taggedNormal_OpenGLmap 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 ambiguousNormalcase (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) andcmb004(Shader Type) were REMOVED from the.ui, not left disabled. AiBridge attaches a parallelaiStandardSurfacepreview 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 ontoND_open_pbr_surface_surfaceshader, MaterialX 1.39.4 ships bundled, and Blender's own USD export emitsND_open_pbr_surface_surfaceshaderfor a Principled material, with every conversion node this tool builds translating cleanly (MixRGB→ND_mix_color3,Invert→ND_subtract_color3,SeparateColor→ND_extract_color3,NormalMap→ND_normalmap_float). So OpenPBR is reachable, and what the Maya combo really selects is the downstream target — which on this panel iscmb002. 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 ledgerednaintentacle/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 exposesteardown/scene_bundles/find_bundles+ abtn_removeslot. Three behavior fixes: (1) every segment was positioned byCopy Locationwith 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 withChild Of(scale channels off — Maya'sparentConstraintdrives 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 whatuse_offsetadds; teardown restores it. (2) The scale driver wasdist / initial, which kept shrinking toward zero past full collapse — Maya clamps there via a constant pre-infinity — so it is nows0 * 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_ofgained**propspassthrough 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 viafind_bundles. The hand-rolled oriented-bbox projection also went away in favour ofXformUtils.get_bounding_box(world_space=True): it was a divergence (mayatk measures a world AABB viaexactWorldBoundingBox), 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 gainedbtn_remove+ theAutocollapse 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_relativetested 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'ssourceImagesdir (EnvUtils.source_images_dir()). In the standard layout (<root>/scenes/shot.blendbeside<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_relativereturned 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.pngis 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.commonpathcompares case-sensitively, so a differently-cased blend dir left Windows paths absolute — the new_MatUtilsInternal._is_withinnormcase-folds both sides and answers False cross-drive instead of raising (normalize_texture_paths' own inside-project test used the rawcommonpathtoo), and Normalize's copy/move neverreload()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_nodesdistinguishes "nothing selected" from "selection with no valid nodes"); andset_texture_directoryskips 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 usesptk.FileUtils.open_explorer— cross-platform, already a dependency — with a missing-directory message instead of a Windows-only bareos.startfile.test_texture_path_editor.py+12 checks (the pureto_project_relativetruth table incl. case-folding and out-of-project, an end-to-end marked-workspace<root>/scenes+<root>/sourceimagesnormalize, 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_editor0 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 sideRigUtils.add_boneuniquifies a colliding name (ra_anchor_end→ra_anchor_end.001, per its own docstring) andRigUtils.child_ofappends 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. NewTubeRig._clear_end_anchorremoves 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'sremoveInfluence, no weight restoration is needed:apply_falloff_weightsscales 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-chainbone_index(previously any non-zero index was labeledend, so anchoring a middle bone would collide with the real end's assembly), andhead_radiusis 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 secondcreate()on the same target REUSED the first rig's material by name, andcreate_material'snodes.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_boundsreads the EVALUATED depsgraph: plane size / contact / light-bearing came from the rawbound_boxwhile the silhouette gather was already evaluated, so a modifier (array/mirror/subsurf) widened the shadow picture but not the plane under it — Maya'sexactWorldBoundingBoxis post-deformation. (4) Linked-library guard:bake_planes/delete_rigsnow skip (with a warning) planes withlibrary/override_libraryset 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-EXISTINGdata_exportcarrier kept the mutatedshadow_metadataprop (the Maya slots snapshot it viacontract.record_modification) and the previewed silhouette PNG stayed on disk (Maya:contract.add_file).Previewgains an opt-inrestore_funchook — 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_PRODUCERSexport-time refresh), and the resolution combo's8096typo is now8192in 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 anobj.coloror 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_typeOBJECT/VERTEX/MATERIAL — MATERIAL last in precedence since it is already the default), called fromb001after 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_typetakes['THEME','OBJECT','RANDOM']and'OBJECT'draws the wires inobj.color(probed live on 5.1.2). Blender keeps ONE per-object tint where Maya keeps two, sochk012/chk013share theobj.colorwrite and select where it is drawn; defaults now match Maya's exactly (both checked) and thenaledger row is gone. (3)ColorId.has_object_color— Blender ships no "use object color" flag (Maya'suseOutlinerColor), so the untouched default white is the only "unset" signal; without itget_object_colornever returnedNone, which madeb003Get 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 insideCoreUtils.undo_chunk(a rawbpy.datawrite 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 newCoreUtils.tag_redraw(area_type=None)(all area types when omitted;get_areasgained the sameNone= 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; thetarget_weld/cam_utilscontext.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.Objectexposes no per-object outliner color (color_tag/outliner_color/use_outliner_colorall 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 viaColorId.set_outliner_color: objects are linked into a color-taggedID_<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_colorsreads the live theme, factory fallback) for display while the exact color is stamped on the collection (btk_color_idcustom prop) soget_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 theID_name: reset spares a user's ownID_*collection, and the find-or-create lookup is stamp-keyed too — a user collection holding the exactID_<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_id0 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_unsavedguarded open / close / reference-toggle onbpy.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 singleed.undo_pushunder a VIEW_3D override flips the flag on aread_homefile(use_empty=True)scene; not reproducible headless or from a timer, where undo pushes have no context). The decision moves into the engine asEnvUtils.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 newEnvUtils.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_worktruth 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.