Fix #134292: Clone brush cannot access local blendfile images

With the brush assets project, brushes were moved from being local to
the working blendfile to being linked from asset libraries. This breaks
the Image Paint 'Clone' brush, as it has a brush property that links to
other Image datablocks.

To support this functionality, this commit adds the corresponding
properties into the `ImagePaintSettings` struct so that it is stored
locally with the images that will be used by the tool, inside the main
blendfile.

The source image property is shared with the 3D version of the 'Clone'
brush instead of adding a separate field to preserve old behavior.

Notably, this has the following limitations:
* If clone brush assets have been made and shared with external packs,
  they would not work out of the box with linked image assets.
* Despite these settings being stored on the scene, they are populated
  inside the tool window under "Brush Settings" which is potentially
  misleading. However, this is already the case for the 3D version of
  the brush, so further UI refinement will happen outside of this PR.
* Users will be unable to use separate images simultaneously for the
  Image editor and the 3D viewport, unlike in pre-4.3 versions. This
  can be adjusted in the future if it is a critical workflow.

Because the intended design and functionality of this tool is currently
questionable, this commit opts to make these changes instead of doing
further design to support both accessing data on the brush and on the
scene.

Pull Request: https://projects.blender.org/blender/blender/pulls/134474
This commit is contained in:
Sean Kim
2025-02-19 22:00:39 +01:00
committed by Sean Kim
parent af8da338a0
commit f1fca48a4f
16 changed files with 60 additions and 84 deletions

View File

@@ -1171,7 +1171,7 @@ def brush_shared_settings(layout, context, brush, popover=False):
layout.row().prop(brush, "direction", expand=True)
def brush_settings_advanced(layout, context, brush, popover=False):
def brush_settings_advanced(layout, context, settings, brush, popover=False):
"""Draw advanced brush settings for Sculpt, Texture/Vertex/Weight Paint modes."""
mode = UnifiedPaintPanel.get_brush_mode(context)
@@ -1317,8 +1317,8 @@ def brush_settings_advanced(layout, context, brush, popover=False):
elif brush.image_tool == 'CLONE':
if mode == 'PAINT_2D':
layout.prop(brush, "clone_image", text="Image")
layout.prop(brush, "clone_alpha", text="Alpha")
layout.prop(settings, "clone_image", text="Image")
layout.prop(settings, "clone_alpha", text="Alpha")
# Vertex Paint #
elif mode == 'PAINT_VERTEX':

View File

@@ -1255,7 +1255,7 @@ class IMAGE_PT_paint_settings_advanced(Panel, ImagePaintPanel):
settings = context.tool_settings.image_paint
brush = settings.brush
if brush:
brush_settings_advanced(layout.column(), context, brush, self.is_popover)
brush_settings_advanced(layout.column(), context, settings, brush, self.is_popover)
class IMAGE_PT_paint_color(Panel, ImagePaintPanel):

View File

@@ -408,7 +408,7 @@ class VIEW3D_PT_tools_brush_settings_advanced(Panel, View3DPaintBrushPanel):
settings = UnifiedPaintPanel.paint_settings(context)
brush = settings.brush
brush_settings_advanced(layout.column(), context, brush, self.is_popover)
brush_settings_advanced(layout.column(), context, settings, brush, self.is_popover)
class VIEW3D_PT_tools_brush_color(Panel, View3DPaintPanel):

View File

@@ -31,7 +31,7 @@ extern "C" {
/* Blender file format version. */
#define BLENDER_FILE_VERSION BLENDER_VERSION
#define BLENDER_FILE_SUBVERSION 28
#define BLENDER_FILE_SUBVERSION 29
/* Minimum Blender version that supports reading file written with the current
* version. Older Blender versions will test this and cancel loading the file, showing a warning to

View File

@@ -162,18 +162,6 @@ static void brush_make_local(Main *bmain, ID *id, const int flags)
bool force_local, force_copy;
BKE_lib_id_make_local_generic_action_define(bmain, id, flags, &force_local, &force_copy);
if (brush->clone.image) {
/* Special case: `ima` always local immediately.
* Clone image should only have one user anyway. */
/* FIXME: Recursive calls affecting other non-embedded IDs are really bad and should be avoided
* in IDType callbacks. Higher-level ID management code usually does not expect such things and
* does not deal properly with it. */
/* NOTE: assert below ensures that the comment above is valid, and that exception is
* acceptable for the time being. */
BKE_lib_id_make_local(bmain, &brush->clone.image->id, LIB_ID_MAKELOCAL_ASSET_DATA_CLEAR);
BLI_assert(!ID_IS_LINKED(brush->clone.image) && brush->clone.image->id.newid == nullptr);
}
if (force_local) {
BKE_lib_id_clear_library_data(bmain, &brush->id, flags);
BKE_lib_id_expand_local(bmain, &brush->id, flags);
@@ -200,7 +188,6 @@ static void brush_foreach_id(ID *id, LibraryForeachIDData *data)
Brush *brush = (Brush *)id;
BKE_LIB_FOREACHID_PROCESS_IDSUPER(data, brush->toggle_brush, IDWALK_CB_NOP);
BKE_LIB_FOREACHID_PROCESS_IDSUPER(data, brush->clone.image, IDWALK_CB_NOP);
BKE_LIB_FOREACHID_PROCESS_IDSUPER(data, brush->paint_curve, IDWALK_CB_USER);
if (brush->gpencil_settings) {
BKE_LIB_FOREACHID_PROCESS_IDSUPER(data, brush->gpencil_settings->material, IDWALK_CB_USER);
@@ -505,7 +492,6 @@ static void brush_defaults(Brush *brush)
FROM_DEFAULT(disconnected_distance_max);
FROM_DEFAULT(sculpt_plane);
FROM_DEFAULT(plane_offset);
FROM_DEFAULT(clone.alpha);
FROM_DEFAULT(normal_weight);
FROM_DEFAULT(fill_threshold);
FROM_DEFAULT(flag);

View File

@@ -1142,14 +1142,6 @@ void do_versions_after_linking_300(FileData * /*fd*/, Main *bmain)
imapaint->clone = nullptr;
}
}
LISTBASE_FOREACH (Brush *, brush, &bmain->brushes) {
if (brush->clone.image != nullptr &&
ELEM(brush->clone.image->type, IMA_TYPE_R_RESULT, IMA_TYPE_COMPOSITE))
{
brush->clone.image = nullptr;
}
}
}
if (!MAIN_VERSION_FILE_ATLEAST(bmain, 300, 28)) {

View File

@@ -5828,6 +5828,13 @@ void blo_do_versions_400(FileData *fd, Library * /*lib*/, Main *bmain)
}
}
if (!MAIN_VERSION_FILE_ATLEAST(bmain, 404, 29)) {
LISTBASE_FOREACH (Scene *, scene, &bmain->scenes) {
ToolSettings *ts = scene->toolsettings;
ts->imapaint.clone_alpha = 0.5f;
}
}
/* Always run this versioning; meshes are written with the legacy format which always needs to
* be converted to the new format on file load. Can be moved to a subversion check in a larger
* breaking release. */

View File

@@ -615,10 +615,11 @@ class MeshUVs : Overlay {
}
{
/* Brush Stencil Overlay. */
const Brush *brush = BKE_paint_brush_for_read(&tool_setting->imapaint.paint);
const ImagePaintSettings &image_paint_settings = tool_setting->imapaint;
const Brush *brush = BKE_paint_brush_for_read(&image_paint_settings.paint);
show_stencil_ = space_mode_is_paint && brush &&
(brush->image_brush_type == IMAGE_PAINT_BRUSH_TYPE_CLONE) &&
brush->clone.image;
image_paint_settings.clone;
}
{
/* UDIM Overlay. */
@@ -860,8 +861,8 @@ class MeshUVs : Overlay {
pass.state_set(DRW_STATE_WRITE_COLOR | DRW_STATE_DEPTH_ALWAYS |
DRW_STATE_BLEND_ALPHA_PREMUL);
const Brush *brush = BKE_paint_brush_for_read(&tool_setting->imapaint.paint);
::Image *stencil_image = brush->clone.image;
const ImagePaintSettings &image_paint_settings = tool_setting->imapaint;
::Image *stencil_image = image_paint_settings.clone;
TextureRef stencil_texture;
stencil_texture.wrap(BKE_image_get_gpu_texture(stencil_image, nullptr));
@@ -873,8 +874,8 @@ class MeshUVs : Overlay {
pass.bind_texture("imgTexture", stencil_texture);
pass.push_constant("imgPremultiplied", true);
pass.push_constant("imgAlphaBlend", true);
pass.push_constant("ucolor", float4(1.0f, 1.0f, 1.0f, brush->clone.alpha));
pass.push_constant("brush_offset", float2(brush->clone.offset));
pass.push_constant("ucolor", float4(1.0f, 1.0f, 1.0f, image_paint_settings.clone_alpha));
pass.push_constant("brush_offset", float2(image_paint_settings.clone_offset));
pass.push_constant("brush_scale", float2(stencil_texture.size().xy()) / size_image);
pass.draw(res.shapes.quad_solid.get());
}

View File

@@ -326,11 +326,14 @@ static bool image_paint_poll_ignore_tool(bContext *C)
static bool image_paint_2d_clone_poll(bContext *C)
{
const Scene *scene = CTX_data_scene(C);
const ToolSettings *settings = scene->toolsettings;
const ImagePaintSettings &image_paint_settings = settings->imapaint;
Brush *brush = image_paint_brush(C);
if (!CTX_wm_region_view3d(C) && ED_image_tools_paint_poll(C)) {
if (brush && (brush->image_brush_type == IMAGE_PAINT_BRUSH_TYPE_CLONE)) {
if (brush->clone.image) {
if (image_paint_settings.clone) {
return true;
}
}
@@ -514,12 +517,13 @@ struct GrabClone {
static void grab_clone_apply(bContext *C, wmOperator *op)
{
Brush *brush = image_paint_brush(C);
const Scene *scene = CTX_data_scene(C);
ToolSettings *settings = scene->toolsettings;
ImagePaintSettings &image_paint_settings = settings->imapaint;
float delta[2];
RNA_float_get_array(op->ptr, "delta", delta);
add_v2_v2(brush->clone.offset, delta);
BKE_brush_tag_unsaved_changes(brush);
add_v2_v2(image_paint_settings.clone_offset, delta);
ED_region_tag_redraw(CTX_wm_region(C));
}
@@ -532,11 +536,13 @@ static int grab_clone_exec(bContext *C, wmOperator *op)
static int grab_clone_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
Brush *brush = image_paint_brush(C);
const Scene *scene = CTX_data_scene(C);
const ToolSettings *settings = scene->toolsettings;
const ImagePaintSettings &image_paint_settings = settings->imapaint;
GrabClone *cmv;
cmv = MEM_cnew<GrabClone>("GrabClone");
copy_v2_v2(cmv->startoffset, brush->clone.offset);
copy_v2_v2(cmv->startoffset, image_paint_settings.clone_offset);
cmv->startx = event->xy[0];
cmv->starty = event->xy[1];
op->customdata = cmv;
@@ -548,7 +554,9 @@ static int grab_clone_invoke(bContext *C, wmOperator *op, const wmEvent *event)
static int grab_clone_modal(bContext *C, wmOperator *op, const wmEvent *event)
{
Brush *brush = image_paint_brush(C);
const Scene *scene = CTX_data_scene(C);
ToolSettings *settings = scene->toolsettings;
ImagePaintSettings &image_paint_settings = settings->imapaint;
ARegion *region = CTX_wm_region(C);
GrabClone *cmv = static_cast<GrabClone *>(op->customdata);
float startfx, startfy, fx, fy, delta[2];
@@ -570,8 +578,7 @@ static int grab_clone_modal(bContext *C, wmOperator *op, const wmEvent *event)
delta[1] = fy - startfy;
RNA_float_set_array(op->ptr, "delta", delta);
copy_v2_v2(brush->clone.offset, cmv->startoffset);
BKE_brush_tag_unsaved_changes(brush);
copy_v2_v2(image_paint_settings.clone_offset, cmv->startoffset);
grab_clone_apply(C, op);
break;

View File

@@ -1299,13 +1299,14 @@ static int paint_2d_op(void *state,
const float pos[2])
{
ImagePaintState *s = ((ImagePaintState *)state);
const ImagePaintSettings &image_paint_settings = s->scene->toolsettings->imapaint;
ImBuf *clonebuf = nullptr, *frombuf;
ImBuf *canvas = tile->canvas;
ImBuf *ibufb = tile->cache.ibuf;
ImagePaintRegion region[4];
short paint_tile = s->symmetry & (PAINT_TILE_X | PAINT_TILE_Y);
short blend = s->blend;
const float *offset = s->brush->clone.offset;
const float *offset = image_paint_settings.clone_offset;
float liftpos[2];
float mask_max = BKE_brush_alpha_get(s->scene, s->brush);
int bpos[2], blastpos[2], bliftpos[2];
@@ -1424,7 +1425,8 @@ static int paint_2d_canvas_set(ImagePaintState *s)
{
/* set clone canvas */
if (s->brush_type == IMAGE_PAINT_BRUSH_TYPE_CLONE) {
Image *ima = s->brush->clone.image;
const ImagePaintSettings &image_paint_settings = s->scene->toolsettings->imapaint;
Image *ima = image_paint_settings.clone;
ImBuf *ibuf = BKE_image_acquire_ibuf(ima, nullptr, nullptr);
if (!ima || !ibuf || !(ibuf->byte_buffer.data || ibuf->float_buffer.data)) {
@@ -1454,7 +1456,8 @@ static void paint_2d_canvas_free(ImagePaintState *s)
for (int i = 0; i < s->num_tiles; i++) {
BKE_image_release_ibuf(s->image, s->tiles[i].canvas, nullptr);
}
BKE_image_release_ibuf(s->brush->clone.image, s->clonecanvas, nullptr);
const ImagePaintSettings &image_paint_settings = s->scene->toolsettings->imapaint;
BKE_image_release_ibuf(image_paint_settings.clone, s->clonecanvas, nullptr);
if (s->blurkernel) {
paint_delete_blur_kernel(s->blurkernel);

View File

@@ -44,7 +44,6 @@
/* How far above or below the plane that is found by averaging the faces. */ \
.plane_offset = 0.0f, \
.plane_trim = 0.5f, \
.clone.alpha = 0.5f, \
.normal_weight = 0.0f, \
.fill_threshold = 0.2f, \
\

View File

@@ -19,16 +19,6 @@ struct Image;
struct MTex;
struct Material;
typedef struct BrushClone {
/** Image for clone tool. */
struct Image *image;
/** Offset of clone image from canvas. */
float offset[2];
/** Transparency for drawing of clone image. */
float alpha;
char _pad[4];
} BrushClone;
typedef struct BrushGpencilSettings {
/** Amount of smoothing to apply to newly created strokes. */
float draw_smoothfac;
@@ -175,7 +165,6 @@ typedef struct Brush {
ID id;
struct BrushClone clone;
/** Falloff curve. */
struct CurveMapping *curve;
struct MTex mtex;

View File

@@ -280,6 +280,7 @@
.paint.flags = PAINT_SHOW_BRUSH, \
.normal_angle = 80, \
.seam_bleed = 2, \
.clone_alpha = 0.5f, \
}
#define _DNA_DEFAULTS_ParticleBrushData \

View File

@@ -1041,6 +1041,11 @@ typedef struct ImagePaintSettings {
/** Display texture interpolation method. */
int interp;
char _pad[4];
/** Offset of clone image from canvas in Image editor. */
float clone_offset[2];
/** Transparency for drawing of clone image in Image editor. */
float clone_alpha;
char _pad2[4];
} ImagePaintSettings;
/** \} */

View File

@@ -802,12 +802,6 @@ static void rna_Brush_icon_update(Main * /*bmain*/, Scene * /*scene*/, PointerRN
WM_main_add_notifier(NC_BRUSH | NA_EDITED, br);
}
static bool rna_Brush_imagetype_poll(PointerRNA * /*ptr*/, PointerRNA value)
{
Image *image = (Image *)value.owner_id;
return image->type != IMA_TYPE_R_RESULT && image->type != IMA_TYPE_COMPOSITE;
}
static void rna_TextureSlot_brush_angle_update(bContext *C, PointerRNA *ptr)
{
Scene *scene = CTX_data_scene(C);
@@ -3906,26 +3900,6 @@ static void rna_def_brush(BlenderRNA *brna)
RNA_def_property_ui_text(prop, "Brush Icon Filepath", "File path to brush icon");
RNA_def_property_update(prop, 0, "rna_Brush_icon_update");
/* clone brush */
prop = RNA_def_property(srna, "clone_image", PROP_POINTER, PROP_NONE);
RNA_def_property_pointer_sdna(prop, nullptr, "clone.image");
RNA_def_property_flag(prop, PROP_EDITABLE);
RNA_def_property_ui_text(prop, "Clone Image", "Image for clone brushes");
RNA_def_property_update(prop, NC_SPACE | ND_SPACE_IMAGE, "rna_Brush_update");
RNA_def_property_pointer_funcs(prop, nullptr, nullptr, nullptr, "rna_Brush_imagetype_poll");
prop = RNA_def_property(srna, "clone_alpha", PROP_FLOAT, PROP_FACTOR);
RNA_def_property_float_sdna(prop, nullptr, "clone.alpha");
RNA_def_property_range(prop, 0.0f, 1.0f);
RNA_def_property_ui_text(prop, "Clone Alpha", "Opacity of clone image display");
RNA_def_property_update(prop, NC_SPACE | ND_SPACE_IMAGE, "rna_Brush_update");
prop = RNA_def_property(srna, "clone_offset", PROP_FLOAT, PROP_XYZ);
RNA_def_property_float_sdna(prop, nullptr, "clone.offset");
RNA_def_property_ui_text(prop, "Clone Offset", "");
RNA_def_property_ui_range(prop, -1.0f, 1.0f, 10.0f, 3);
RNA_def_property_update(prop, NC_SPACE | ND_SPACE_IMAGE, "rna_Brush_update");
prop = RNA_def_property(srna, "brush_capabilities", PROP_POINTER, PROP_NONE);
RNA_def_property_flag(prop, PROP_NEVER_NULL);
RNA_def_property_struct_type(prop, "BrushCapabilities");

View File

@@ -1210,6 +1210,18 @@ static void rna_def_image_paint(BlenderRNA *brna)
RNA_def_property_ui_text(
prop, "Missing Texture", "Image Painting does not have a texture to paint on");
RNA_def_property_clear_flag(prop, PROP_EDITABLE);
prop = RNA_def_property(srna, "clone_alpha", PROP_FLOAT, PROP_FACTOR);
RNA_def_property_float_sdna(prop, nullptr, "clone_alpha");
RNA_def_property_range(prop, 0.0f, 1.0f);
RNA_def_property_ui_text(prop, "Clone Alpha", "Opacity of clone image display");
RNA_def_property_update(prop, NC_SCENE | ND_TOOLSETTINGS, nullptr);
prop = RNA_def_property(srna, "clone_offset", PROP_FLOAT, PROP_XYZ);
RNA_def_property_float_sdna(prop, nullptr, "clone_offset");
RNA_def_property_ui_text(prop, "Clone Offset", "");
RNA_def_property_ui_range(prop, -1.0f, 1.0f, 10.0f, 3);
RNA_def_property_update(prop, NC_SCENE | ND_TOOLSETTINGS, nullptr);
}
static void rna_def_particle_edit(BlenderRNA *brna)