Undo: Support global (memfile) undo in Texture Paint

When in Texture Paint mode or in the image editor, certain actions do
not push undo steps (e.g. changing the active texture) that users would
expect. Additionally, some actions that *do* push undo steps (e.g.
changing face selection) have no effect when undone.

To fix the former, remove the code preventing property changes from
pushing undo steps while in Texture Paint mode.

To fix the latter, remove the poll function from `image_undo.cc`. This
prevents global undo steps from being tagged as image undo steps.

This works because the `poll` function is only needed when creating the
undo step to identify steps that do not have a `step_init` defined.

This is similar to, but notably different from !139871:

Image Undo (and Sculpt Undo) store deltas encoded as part of the undo
step and require explicit tagging of the begin and end state (see
`ED_image_undo_push_begin` and `ED_image_undo_push_end`). This allows
us to use memfile undo in the general case and image undo when
explicitly requested, avoiding the memory concerns of the previously
linked PR.

Addresses: #69346, #62520, #71895

Pull Request: https://projects.blender.org/blender/blender/pulls/151399
This commit is contained in:
Sean Kim 2026-01-02 16:35:13 +01:00 • committed by Sean Kim
parent f1aa11974a
commit 0ac5946a89
3 changed files with 25 additions and 26 deletions

View file

@ -615,6 +615,7 @@ def view3d_texture_paint_simple():
def view3d_texture_paint_complex():
import bpy
# More complex test than `view3d_texture_paint_simple`,
# including interleaved memfile steps,
# and a call to history to undo several steps at once.
@ -628,16 +629,33 @@ def view3d_texture_paint_complex():
yield from _call_by_name(e, "Add Texture Paint Slot")
yield e.ret() # Accept popup.
initial_data = tuple(bpy.data.images['Suzanne Base Color'].pixels)
yield from e.leftmouse.cursor_motion(_cursor_motion_data_x(window))
yield from e.leftmouse.cursor_motion(_cursor_motion_data_y(window))
after_strokes = tuple(bpy.data.images['Suzanne Base Color'].pixels)
t.assertTrue(any([orig != new for (orig, new) in zip(initial_data, after_strokes)]),
"At least one pixel should differ in color component")
yield from _call_by_name(e, "Add Texture Paint Slot")
yield e.ret() # Accept popup.
yield from _call_by_name(e, "Add Modifier")
yield e.a() # Array modifier
t.assertEqual(len(bpy.context.active_object.modifiers), 1, "One modifier should exist")
yield from e.leftmouse.cursor_motion(_cursor_motion_data_x(window))
yield from e.leftmouse.cursor_motion(_cursor_motion_data_y(window))
yield e.ctrl.z(6) # Undo: initial texture paint.
yield e.ctrl.z(6) # Undo: second slot added.
t.assertEqual(len(bpy.context.active_object.modifiers), 0, "No modifiers should exist")
after_undo = tuple(bpy.data.images['Suzanne Base Color'].pixels)
t.assertTrue(all([orig == new for (orig, new) in zip(initial_data, after_undo)]),
"All pixels should be the same as their original state")
yield e.ctrl.z(1) # Undo: initial texture paint.
t.assertEqual(window.view_layer.objects.active.mode, 'TEXTURE_PAINT')
yield e.ctrl.z() # Undo: object mode.
t.assertEqual(window.view_layer.objects.active.mode, 'OBJECT')