Files
blender-portable-repo/scripts/addons/brushstroke_tools/ops.py
T
2026-03-17 14:58:51 -06:00

1433 lines
54 KiB
Python

# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
import bpy
import random
from . import utils, settings
import mathutils
class BSBST_OT_new_brushstrokes(bpy.types.Operator):
""" Create new object according to method and type.
Link to correct collection.
Attach to selection context if applicable.
Assign selected modifier setup. Enter correct context for editing.
"""
bl_idname = "brushstroke_tools.new_brushstrokes"
bl_label = "New Brushstrokes"
bl_description = "Create new brushstrokes object of selected type with all the necessary setup in place"
bl_options = {"REGISTER", "UNDO"}
method: bpy.props.StringProperty(default='SURFACE_FILL')
@classmethod
def poll(cls, context):
surface_object = utils.get_active_context_surface_object(context)
return bool(surface_object)
def new_brushstrokes_object(self, context, name, surface_object):
settings = context.scene.BSBST_settings
settings.brushstroke_method = self.method
if settings.curve_mode == 'GP':
bpy.ops.object.grease_pencil_add(type='EMPTY')
context.object.name = name
context.object.data.name = name
brushstrokes_object = context.object
context.collection.objects.unlink(brushstrokes_object)
else:
if settings.curve_mode == 'CURVE':
brushstrokes_data = bpy.data.curves.new(name, type='CURVE')
brushstrokes_data.dimensions = '3D'
elif settings.curve_mode == 'CURVES':
brushstrokes_data = bpy.data.hair_curves.new(name)
brushstrokes_object = bpy.data.objects.new(name, brushstrokes_data)
# link to surface object's collections (fall back to active collection if all are linked data)
utils.link_to_collections_by_ref(brushstrokes_object, surface_object, unlink=False)
brushstrokes_object.visible_shadow = False
brushstrokes_object['BSBST_version'] = utils.addon_version
utils.set_deformable(brushstrokes_object, settings.deforming_surface)
utils.set_animated(brushstrokes_object, settings.animated)
return brushstrokes_object
def new_flow_object(self, context, name, surface_object):
settings = context.scene.BSBST_settings
if settings.curve_mode == 'GP':
bpy.ops.object.grease_pencil_add(type='EMPTY')
context.object.name = name
context.object.data.name = name
flow_object = context.object
context.collection.objects.unlink(flow_object)
else:
if settings.curve_mode == 'CURVE':
flow_data = bpy.data.curves.new(name, type='CURVE')
flow_data.dimensions = '3D'
elif settings.curve_mode == 'CURVES':
flow_data = bpy.data.hair_curves.new(name)
flow_object = bpy.data.objects.new(name, flow_data)
# link to surface object's collections (fall back to active collection if all are linked data)
utils.link_to_collections_by_ref(flow_object, surface_object, unlink=False)
visibility_options = [
'visible_camera',
'visible_diffuse',
'visible_glossy',
'visible_transmission',
'visible_volume_scatter',
'visible_shadow',
]
for vis in visibility_options:
setattr(flow_object, vis, False)
## add pre-processing modifier
mod = flow_object.modifiers.new('Pre-Processing', 'NODES')
mod.node_group = bpy.data.node_groups['.brushstroke_tools.pre_processing']
mod_info = flow_object.modifier_info.add()
mod_info.name = mod.name
utils.mark_socket_context_type(mod_info, 'Socket_2', 'SURFACE_OBJECT')
mod['Socket_2'] = surface_object
mod['Socket_3'] = False
utils.set_deformable(flow_object, settings.deforming_surface)
utils.set_animated(flow_object, settings.animated)
return flow_object
def main(self, context):
settings = context.scene.BSBST_settings
utils.ensure_resources()
if not context.mode == 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
surface_object = utils.get_active_context_surface_object(context)
flow_object = None
if not surface_object.data.uv_layers.active:
self.report({"ERROR"}, "Surface Object needs an available UV Map")
return {"CANCELLED"}
name = utils.bs_name(surface_object.name)
brushstrokes_object = self.new_brushstrokes_object(context, name, surface_object)
flow_is_required = settings.brushstroke_method == 'SURFACE_FILL'
if flow_is_required:
flow_object = None
if settings.reuse_flow:
if settings.context_brushstrokes:
bs = bpy.data.objects.get(settings.context_brushstrokes[settings.active_context_brushstrokes_index].name)
if 'BSBST_flow_object' in bs.keys():
flow_object = bs['BSBST_flow_object']
if not flow_object:
flow_object = self.new_flow_object(context, utils.flow_name(name), surface_object)
# attach surface object pointer
if surface_object:
utils.set_surface_object(brushstrokes_object, surface_object)
if flow_object:
brushstrokes_object['BSBST_flow_object'] = flow_object
brushstrokes_object['BSBST_active'] = True
brushstrokes_object['BSBST_method'] = settings.brushstroke_method
if surface_object:
surface_object.add_rest_position_attribute = True # TODO report if library data
brushstrokes_object.parent = surface_object
if flow_object:
flow_object.parent = surface_object
if not settings.preset_object:
bpy.ops.brushstroke_tools.init_preset()
# assign preset material
preset_material = getattr(settings.preset_object, '["BSBST_material"]', None)
if preset_material:
override = context.copy()
override['object'] = brushstrokes_object
with context.temp_override(**override):
bpy.ops.object.material_slot_add()
brushstrokes_object.material_slots[0].material = preset_material
settings.context_material = preset_material
brushstrokes_object['BSBST_material'] = settings.context_material
# transfer preset modifiers to new brushstrokes TODO: refactor to deduplicate
for mod in settings.preset_object.modifiers:
utils.transfer_modifier(mod.name, brushstrokes_object, settings.preset_object)
for v in mod.node_group.interface.items_tree.values():
if type(v) not in utils.linkable_sockets:
continue
if not settings.preset_object.modifier_info[mod.name].socket_info[v.identifier].link_context:
continue
# initialize linked context parameters
link_context_type = settings.preset_object.modifier_info[mod.name].socket_info[v.identifier].link_context_type
if link_context_type=='SURFACE_OBJECT':
brushstrokes_object.modifiers[mod.name][f'{v.identifier}'] = surface_object
elif link_context_type=='FLOW_OBJECT':
brushstrokes_object.modifiers[mod.name][f'{v.identifier}'] = flow_object
elif link_context_type=='MATERIAL':
brushstrokes_object.modifiers[mod.name][f'{v.identifier}'] = settings.context_material
elif link_context_type=='UVMAP':
if type(brushstrokes_object.modifiers[mod.name][f'{v.identifier}']) == str:
brushstrokes_object.modifiers[mod.name][f'{v.identifier}'] = surface_object.data.uv_layers.active.name
else:
brushstrokes_object.modifiers[mod.name][f'{v.identifier}_use_attribute'] = True
brushstrokes_object.modifiers[mod.name][f'{v.identifier}_attribute_name'] = surface_object.data.uv_layers.active.name
elif link_context_type=='RANDOM':
vmin = v.min_value
vmax = v.max_value
val = vmin + random.random() * (vmax - vmin)
brushstrokes_object.modifiers[mod.name][f'{v.identifier}_use_attribute'] = False
brushstrokes_object.modifiers[mod.name][f'{v.identifier}'] = type(brushstrokes_object.modifiers[mod.name][f'{v.identifier}'])(val)
# transfer modifier info data from preset to brush strokes
utils.deep_copy_mod_info(settings.preset_object, brushstrokes_object)
# estimate dimensions
if settings.estimate_dimensions:
bb_min = mathutils.Vector(surface_object.bound_box[0])
bb_max = mathutils.Vector(surface_object.bound_box[6])
bb_radius = mathutils.Vector([abs(co) for co in bb_max-bb_min]).length * 0.5
surf_est = 4 * 3.142 * bb_radius**2
mod = brushstrokes_object.modifiers['Brushstrokes']
if settings.brushstroke_method == 'SURFACE_FILL':
# set density
mod['Socket_7'] = utils.round_n((1000 / surf_est) ** 0.5, 2)
# set length
mod['Socket_11'] = utils.round_n(bb_radius * 0.5, 2)
# set width
mod['Socket_13'] = utils.round_n(bb_radius * 0.05, 2)
elif settings.brushstroke_method == 'SURFACE_DRAW':
# set width
mod['Socket_5'] = utils.round_n(bb_radius * 0.05, 2)
# refresh UI
for mod in brushstrokes_object.modifiers:
mod.node_group.interface_update(context)
if settings.assign_materials:
for mod in brushstrokes_object.modifiers:
for v in mod.node_group.interface.items_tree.values():
if type(v) != bpy.types.NodeTreeInterfaceSocketMaterial:
continue
mat = mod[v.identifier]
if not mat:
continue
if mat in [m_slot.material for m_slot in brushstrokes_object.material_slots]:
continue
override = context.copy()
override['object'] = brushstrokes_object
with context.temp_override(**override):
bpy.ops.object.material_slot_add()
brushstrokes_object.material_slots[-1].material = mat
# set deformable
set_brushstrokes_deformable(brushstrokes_object, settings.deforming_surface)
# set animated
set_brushstrokes_animated(brushstrokes_object, settings.animated)
for mod in brushstrokes_object.modifiers:
mod.show_group_selector = False
# update brushstroke context
utils.find_context_brushstrokes(context.scene, context.view_layer.depsgraph)
for i, name in enumerate([bs.name for bs in settings.context_brushstrokes]):
if name == brushstrokes_object.name:
settings.active_context_brushstrokes_index = i
break
utils.edit_active_brushstrokes(context)
return {"FINISHED"}
def execute(self, context):
settings = context.scene.BSBST_settings
settings.silent_switch = True
state = self.main(context)
settings.silent_switch = False
return state
class BSBST_OT_edit_brushstrokes(bpy.types.Operator):
"""
Enter the editing context for the active context brushstrokes.
"""
bl_idname = "brushstroke_tools.edit_brushstrokes"
bl_label = "Edit Brushstrokes"
bl_description = " Enter the editing context for the active context brushstrokes"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
settings = context.scene.BSBST_settings
return bool(settings.context_brushstrokes)
def execute(self, context):
settings = context.scene.BSBST_settings
settings.silent_switch = True
state = utils.edit_active_brushstrokes(context)
settings.silent_switch = False
return state
class BSBST_OT_delete_brushstrokes(bpy.types.Operator):
"""
Delete the active context brushstrokes
"""
bl_idname = "brushstroke_tools.delete_brushstrokes"
bl_label = "Delete Brushstrokes"
bl_description = "Delete the active context brushstrokes"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
settings = context.scene.BSBST_settings
return bool(settings.context_brushstrokes)
def execute(self, context):
settings = context.scene.BSBST_settings
edit_toggle = settings.edit_toggle
settings.edit_toggle = False
bs_ob = utils.get_active_context_brushstrokes_object(context.scene)
if not bs_ob:
settings.edit_toggle = edit_toggle
return {"CANCELLED"}
surface_object = utils.get_surface_object(bs_ob)
flow_object = utils.get_flow_object(bs_ob)
if context.active_object:
if context.mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
bpy.data.objects.remove(bs_ob)
settings.active_context_brushstrokes_index = max(0, settings.active_context_brushstrokes_index-1)
if surface_object:
context.view_layer.objects.active = surface_object
bs_ob = utils.get_active_context_brushstrokes_object(context.scene)
if bs_ob:
context.view_layer.objects.active = bs_ob
bs_ob.select_set(True)
if not flow_object:
settings.edit_toggle = edit_toggle
return {"FINISHED"}
# delete controller objects
if flow_object.users <= 1:
bpy.data.objects.remove(flow_object)
settings.edit_toggle = edit_toggle
return {'FINISHED'}
class BSBST_OT_duplicate_brushstrokes(bpy.types.Operator):
"""
Duplicate the active context brushstrokes
"""
bl_idname = "brushstroke_tools.duplicate_brushstrokes"
bl_label = "Duplicate Brushstrokes"
bl_description = "Duplicate the active context brushstrokes"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
settings = context.scene.BSBST_settings
return bool(settings.context_brushstrokes)
def execute(self, context):
settings = context.scene.BSBST_settings
bs_ob = utils.get_active_context_brushstrokes_object(context.scene)
if not bs_ob:
return {"CANCELLED"}
if not bs_ob.visible_get(view_layer=context.view_layer):
self.report({"WARNING"}, f"Skipped Brushstroke layer '{bs_ob.name}' because it is invisible in this context")
return {"CANCELLED"}
flow_object = utils.get_flow_object(bs_ob)
if context.mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
bpy.context.view_layer.objects.active = bs_ob
if context.mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
for ob in bpy.data.objects:
ob.select_set(False)
bs_ob.select_set(True)
if flow_object and not settings.reuse_flow:
flow_object.select_set(True)
bpy.ops.object.duplicate_move()
# reshuffle seed
for ob in context.selected_editable_objects:
for mod in ob.modifiers:
if not mod.type=='NODES':
continue
if not mod.node_group:
continue
for v in mod.node_group.interface.items_tree.values():
if type(v) not in utils.linkable_sockets:
continue
if not ob.modifier_info[mod.name].socket_info[v.identifier].link_context:
continue
# initialize linked context parameters
link_context_type = ob.modifier_info[mod.name].socket_info[v.identifier].link_context_type
if link_context_type=='RANDOM':
vmin = v.min_value
vmax = v.max_value
val = vmin + random.random() * (vmax - vmin)
mod[f'{v.identifier}'] = type(ob.modifiers[mod.name][f'{v.identifier}'])(val)
return {'FINISHED'}
class BSBST_OT_copy_brushstrokes(bpy.types.Operator):
"""
Copy the active context brushstrokes to the selected surface objects
"""
bl_idname = "brushstroke_tools.copy_brushstrokes"
bl_label = "Copy Brushstrokes to Selected"
bl_description = "Copy the active context brushstrokes to the selected surface objects"
bl_options = {"REGISTER", "UNDO"}
copy_all: bpy.props.BoolProperty(default=False)
@classmethod
def poll(cls, context):
settings = context.scene.BSBST_settings
return bool(settings.context_brushstrokes)
def execute(self, context):
settings = context.scene.BSBST_settings
active_surface_object = utils.get_surface_object(utils.get_active_context_brushstrokes_object(context.scene))
surface_objects = [ob for ob in context.selected_objects
if ob.type=='MESH'
and not utils.is_brushstrokes_object(ob)
and not ob==active_surface_object]
if not surface_objects:
return {"CANCELLED"}
if self.copy_all:
bs_objects = [bpy.data.objects.get(bs.name) for bs in settings.context_brushstrokes]
bs_objects = [bs for bs in bs_objects if bs]
else:
bs_objects = [utils.get_active_context_brushstrokes_object(context.scene)]
if not bs_objects:
return {"CANCELLED"}
if context.mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
for surface_object in surface_objects:
for ob in bpy.data.objects:
ob.select_set(False)
for bs_ob in bs_objects:
if not bs_ob.visible_get(view_layer=context.view_layer):
self.report({"WARNING"}, f"Skipped Brushstroke layer '{bs_ob.name}' because it is invisible in this context")
continue
bs_ob.select_set(True)
flow_object = utils.get_flow_object(bs_ob)
if not flow_object:
continue
flow_object.select_set(True)
all_obj = set(bpy.data.objects[:])
context.view_layer.depsgraph.update()
bpy.ops.object.duplicate_move()
new_bs = list(set(bpy.data.objects[:]) - all_obj)
del all_obj
# remap surface pointers and context linked data TODO: refactor to deduplicate
for ob in new_bs:
utils.link_to_collections_by_ref(ob, surface_object)
# if it's still using the default name initialize names again
if utils.split_id_name(ob.name)[0] == utils.bs_name(active_surface_object.name):
if utils.is_flow_object(ob):
ob.name = utils.flow_name(utils.bs_name(surface_object.name))
else:
ob.name = utils.bs_name(surface_object.name)
elif ob.name.startswith(active_surface_object.name):
ob.name = f"{surface_object.name}{ob.name[len(active_surface_object.name):]}"
ob.parent = surface_object
utils.set_surface_object(ob, surface_object)
for mod in ob.modifiers:
if not mod.type:
continue
if not mod.node_group:
continue
mod_info = ob.modifier_info.get(mod.name)
if not mod_info:
continue
for v in mod.node_group.interface.items_tree.values():
if type(v) not in utils.linkable_sockets:
continue
if not mod_info.socket_info[v.identifier].link_context:
continue
# re-initialize linked context parameters
link_context_type = ob.modifier_info[mod.name].socket_info[v.identifier].link_context_type
if link_context_type=='SURFACE_OBJECT':
ob.modifiers[mod.name][f'{v.identifier}'] = surface_object
elif link_context_type=='UVMAP':
if type(ob.modifiers[mod.name][f'{v.identifier}']) == str:
ob.modifiers[mod.name][f'{v.identifier}'] = surface_object.data.uv_layers.active.name
else:
ob.modifiers[mod.name][f'{v.identifier}_use_attribute'] = True
ob.modifiers[mod.name][f'{v.identifier}_attribute_name'] = surface_object.data.uv_layers.active.name
elif link_context_type=='RANDOM':
vmin = v.min_value
vmax = v.max_value
val = vmin + random.random() * (vmax - vmin)
ob.modifiers[mod.name][f'{v.identifier}_use_attribute'] = False
ob.modifiers[mod.name][f'{v.identifier}'] = type(ob.modifiers[mod.name][f'{v.identifier}'])(val)
# enable rest position
surface_object.add_rest_position_attribute = True
return {'FINISHED'}
class BSBST_OT_select_surface(bpy.types.Operator):
"""
Select the surface object for the active context brushstrokes.
"""
bl_idname = "brushstroke_tools.select_surface"
bl_label = "Select Surface"
bl_description = "Select the surface object for the active context brushstrokes"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
settings = context.scene.BSBST_settings
return bool(settings.context_brushstrokes)
def execute(self, context):
bs_ob = utils.get_active_context_brushstrokes_object(context.scene)
if not bs_ob:
return {"CANCELLED"}
surface_object = getattr(bs_ob, '["BSBST_surface_object"]', None)
if not surface_object:
return {"CANCELLED"}
bpy.context.view_layer.objects.active = surface_object
if not context.mode == 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
for ob in bpy.data.objects:
ob.select_set(False)
surface_object.select_set(True)
return {"FINISHED"}
class BSBST_OT_assign_surface(bpy.types.Operator):
"""
Assign a surface object for the active context brushstrokes.
"""
bl_idname = "brushstroke_tools.assign_surface"
bl_label = "Assign Surface"
bl_description = "Assign a surface object for the active context brushstrokes"
bl_options = {"REGISTER", "UNDO"}
surface_object: bpy.props.StringProperty()
@classmethod
def poll(cls, context):
settings = context.scene.BSBST_settings
return bool(settings.context_brushstrokes)
def execute(self, context):
bs_ob = utils.get_active_context_brushstrokes_object(context.scene)
if not bs_ob:
return {"CANCELLED"}
surface_object = bpy.data.objects.get(self.surface_object)
if not surface_object:
return {"CANCELLED"}
# TODO handle parenting with keep transform as default option
utils.set_surface_object(bs_ob, surface_object)
return {"FINISHED"}
def draw(self, context):
layout = self.layout
layout.prop_search(self, 'surface_object', bpy.data, 'objects')
def invoke(self, context, event):
bs_ob = utils.get_active_context_brushstrokes_object(context.scene)
surf_ob = utils.get_surface_object(bs_ob)
if surf_ob:
self.surface_object = surf_ob.name
return context.window_manager.invoke_props_dialog(self)
def set_brushstrokes_deformable(bs_ob, deformable):
flow_ob = utils.get_flow_object(bs_ob)
for mod in bs_ob.modifiers:
if not mod.type == 'NODES':
continue
if not mod.node_group:
continue
if mod.node_group.name == '.brushstroke_tools.pre_processing':
mod['Socket_3'] = deformable
elif mod.node_group.name == '.brushstroke_tools.surface_fill':
mod['Socket_27'] = deformable
elif mod.node_group.name == '.brushstroke_tools.surface_draw':
mod['Socket_15'] = deformable
mod.node_group.interface_update(bpy.context)
utils.set_deformable(bs_ob, deformable)
if not flow_ob:
return
utils.set_deformable(flow_ob, deformable)
def set_brushstrokes_animated(bs_ob, animated):
flow_ob = utils.get_flow_object(bs_ob)
if flow_ob:
ob = flow_ob
else:
ob = bs_ob
mod = ob.modifiers.get('Animation')
if animated:
if not mod:
mod = ob.modifiers.new('Animation', 'NODES')
mod.node_group = utils.ensure_node_group('.brushstroke_tools.animation')
mod_info = ob.modifier_info.get(mod.name)
if not mod_info:
mod_info = ob.modifier_info.add()
mod_info.name = mod.name
# ui visibility settings
mod_info.hide_ui = True
else:
mod['Socket_5'] = True
mod.node_group.interface_update(bpy.context)
with bpy.context.temp_override(object=ob):
bpy.ops.object.modifier_move_to_index(modifier=mod.name, index=0)
else:
if mod:
ob.modifiers.remove(mod)
utils.set_animated(bs_ob, animated)
if not flow_ob:
return
utils.set_animated(flow_ob, animated)
class BSBST_OT_copy_flow(bpy.types.Operator):
"""
"""
bl_idname = "brushstroke_tools.copy_flow"
bl_label = "Copy Flow from Existing"
bl_description = "Copy the flow object from another brushstroke layer."
bl_options = {"REGISTER", "UNDO"}
source_bs: bpy.props.StringProperty(name="Brushstroke Layers")
bs_list: bpy.props.CollectionProperty(type=settings.BSBST_context_brushstrokes)
@classmethod
def poll(cls, context):
settings = context.scene.BSBST_settings
return bool(settings.context_brushstrokes)
def execute(self, context):
bs_ob = utils.get_active_context_brushstrokes_object(context.scene)
flow_ob_old = utils.get_flow_object(bs_ob)
if not bs_ob:
return {"CANCELLED"}
# get source
source_ob = bpy.data.objects.get(self.source_bs)
flow_ob = utils.get_flow_object(source_ob)
if not source_ob or not flow_ob:
return {"CANCELLED"}
# set flow object
utils.set_flow_object(bs_ob, flow_ob)
# delete old flow if unused necessary
if not flow_ob_old:
return {"FINISHED"}
for bs in self.bs_list:
flow_ob = utils.get_flow_object(source_ob)
if not flow_ob:
continue
if flow_ob.name == flow_ob_old.name:
return {"FINISHED"}
bpy.data.objects.remove(flow_ob_old)
return {"FINISHED"}
def draw(self, context):
layout = self.layout
layout.prop_search(self, 'source_bs', self, 'bs_list', icon='OUTLINER_OB_FORCE_FIELD')
def invoke(self, context, event):
settings = context.scene.BSBST_settings
bs_ob = utils.get_active_context_brushstrokes_object(context.scene)
for i in range(len(self.bs_list)):
self.bs_list.remove(0)
for bs in settings.context_brushstrokes:
if not bs.method=='SURFACE_FILL':
continue
if bs.name==bs_ob.name:
continue
bs_new = self.bs_list.add()
bs_new.name = bs.name
bs_new.hide_viewport_base = bs.hide_viewport_base
if self.bs_list:
self.source_bs = self.bs_list[0].name
return context.window_manager.invoke_props_dialog(self)
class BSBST_OT_switch_deformable(bpy.types.Operator):
"""
"""
bl_idname = "brushstroke_tools.switch_deformable"
bl_label = "Switch Deformable"
bl_description = "Switch the deformable state of the brushstrokes"
bl_options = {"REGISTER", "UNDO"}
deformable: bpy.props.BoolProperty( default=True,
name="Deformable",)
switch_all: bpy.props.BoolProperty( default=False,
name="All Brushstrokes",
description="Switch all Brushstroke Layers of Current Surface Object.")
@classmethod
def poll(cls, context):
settings = context.scene.BSBST_settings
return bool(settings.context_brushstrokes)
def execute(self, context):
settings = context.scene.BSBST_settings
if self.deformable:
surf_ob = utils.get_active_context_surface_object(context)
if surf_ob:
for mod in surf_ob.modifiers:
if mod.type == 'MIRROR':
self.report({"WARNING"}, "Surface Objects with mirror modifier cannot properly support stable deformation. Apply the mirror modifier to proceed.")
if self.switch_all:
bs_objects = [bpy.data.objects.get(bs.name) for bs in settings.context_brushstrokes]
bs_objects = [bs for bs in bs_objects if bs]
else:
bs_objects = [utils.get_active_context_brushstrokes_object(context.scene)]
if not bs_objects:
return {"CANCELLED"}
for ob in bs_objects:
if self.deformable:
if utils.compare_versions(bpy.app.version, (5,0,0)) < 0: # TODO adjust for when GP supports node tool execution
if ob.type == 'GREASEPENCIL':
self.report({"WARNING"}, "Grease Pencil does not currently support drawing on deformable surface geometry.")
set_brushstrokes_deformable(ob, self.deformable)
context.view_layer.depsgraph.update()
return {"FINISHED"}
class BSBST_OT_switch_animated(bpy.types.Operator):
"""
"""
bl_idname = "brushstroke_tools.switch_animated"
bl_label = "Switch Animated"
bl_description = "Switch the atnimated state of the brushstrokes"
bl_options = {"REGISTER", "UNDO"}
animated: bpy.props.BoolProperty( default=True,
name="Animated",)
switch_all: bpy.props.BoolProperty( default=False,
name="All Brushstrokes",
description="Switch all Brushstroke Layers of Current Surface Object.")
@classmethod
def poll(cls, context):
settings = context.scene.BSBST_settings
return bool(settings.context_brushstrokes)
def execute(self, context):
settings = context.scene.BSBST_settings
if self.switch_all:
bs_objects = [bpy.data.objects.get(bs.name) for bs in settings.context_brushstrokes]
bs_objects = [bs for bs in bs_objects if bs]
else:
bs_objects = [utils.get_active_context_brushstrokes_object(context.scene)]
if not bs_objects:
return {"CANCELLED"}
for ob in bs_objects:
set_brushstrokes_animated(ob, self.animated)
context.view_layer.depsgraph.update()
return {"FINISHED"}
class BSBST_OT_init_preset(bpy.types.Operator):
"""
Initialize the preset to define a modifier stack applied to new brushstrokess.
"""
bl_idname = "brushstroke_tools.init_preset"
bl_label = "Initialize Preset"
bl_description = "Initialize the preset environment to setup a predefined modifier stack for new brushstrokess"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
settings = context.scene.BSBST_settings
return settings.preset_object is None
def init_fill(self, context):
settings = context.scene.BSBST_settings
preset_object = settings.preset_object
# add modifiers
## input
mod = preset_object.modifiers.new('Surface Input', 'NODES')
mod.node_group = bpy.data.node_groups['.brushstroke_tools.geometry_input']
mod_info = settings.preset_object.modifier_info.get(mod.name)
if not mod_info:
mod_info = settings.preset_object.modifier_info.add()
mod_info.name = mod.name
# context link settings
utils.mark_socket_context_type(mod_info, 'Socket_2', 'SURFACE_OBJECT')
# ui visibility settings
mod_info.hide_ui = True
mod_info.default_closed = True
## masking
mod = preset_object.modifiers.new('Masking', 'NODES')
mod.node_group = bpy.data.node_groups['.brushstroke_tools.mask_surface']
mod_info = settings.preset_object.modifier_info.get(mod.name)
if not mod_info:
mod_info = settings.preset_object.modifier_info.add()
mod_info.name = mod.name
mod_info.default_closed = True
# ui visibility settings
hide_sockets =[
'Socket_5',
]
for s in hide_sockets:
utils.mark_socket_hidden(mod_info, s)
## brushstrokes
mod = preset_object.modifiers.new('Brushstrokes', 'NODES')
mod.node_group = bpy.data.node_groups['.brushstroke_tools.surface_fill']
mod_info = settings.preset_object.modifier_info.get(mod.name)
if not mod_info:
mod_info = settings.preset_object.modifier_info.add()
mod_info.name = mod.name
# set fill custom defaults
mod['Socket_59'] = 1 # color method: curves
# context link settings
utils.mark_socket_context_type(mod_info, 'Socket_2', 'FLOW_OBJECT')
utils.mark_socket_context_type(mod_info, 'Socket_3', 'UVMAP')
utils.mark_socket_context_type(mod_info, 'Socket_9', 'RANDOM')
utils.mark_socket_context_type(mod_info, 'Socket_12', 'MATERIAL')
utils.mark_socket_context_type(mod_info, 'Socket_60', 'FLOW_OBJECT')
# ui visibility settings
hide_sockets =[
'Socket_2',
'Socket_3',
#'Socket_9', # seed
'Socket_12',
'Socket_15',
'Socket_27',
'Socket_37',
'Socket_40',
'Socket_62',
'Socket_67', # mesh loops
]
for s in hide_sockets:
utils.mark_socket_hidden(mod_info, s)
hide_panels = [
'Stroke Culling',
'Lighting Influence',
'Offset Override',
'Debug',
]
for p in hide_panels:
utils.mark_panel_hidden(mod_info, p)
def init_draw(self, context):
settings = context.scene.BSBST_settings
preset_object = settings.preset_object
# add modifiers
## add pre-processing modifier
mod = preset_object.modifiers.new('Pre-Processing', 'NODES')
mod.node_group = bpy.data.node_groups['.brushstroke_tools.pre_processing']
mod_info = settings.preset_object.modifier_info.get(mod.name)
if not mod_info:
mod_info = settings.preset_object.modifier_info.add()
mod_info.name = mod.name
utils.mark_socket_context_type(mod_info, 'Socket_2', 'SURFACE_OBJECT')
# ui visibility settings
mod_info.hide_ui = True
## brushstrokes
mod = preset_object.modifiers.new('Brushstrokes', 'NODES')
mod.node_group = bpy.data.node_groups['.brushstroke_tools.surface_draw']
mod_info = settings.preset_object.modifier_info.get(mod.name)
if not mod_info:
mod_info = settings.preset_object.modifier_info.add()
mod_info.name = mod.name
utils.mark_socket_context_type(mod_info, 'Socket_2', 'SURFACE_OBJECT')
utils.mark_socket_context_type(mod_info, 'Socket_4', 'MATERIAL')
utils.mark_socket_context_type(mod_info, 'Socket_6', 'RANDOM')
utils.mark_socket_context_type(mod_info, 'Socket_12', 'UVMAP')
# ui visibility settings
hide_sockets =[
'Socket_2',
'Socket_3',
'Socket_4',
#'Socket_6', # seed
'Socket_12',
'Socket_15',
'Socket_24',
'Socket_35', # mesh loops
]
for s in hide_sockets:
utils.mark_socket_hidden(mod_info, s)
hide_panels = [
'Debug',
]
for p in hide_panels:
utils.mark_panel_hidden(mod_info, p)
def execute(self, context):
settings = context.scene.BSBST_settings
utils.ensure_resources()
preset_name = f'BSBST-PRESET_{settings.brushstroke_method}'
preset_object = bpy.data.objects.new(preset_name, bpy.data.hair_curves.new(preset_name))
settings.preset_object = preset_object
if settings.brushstroke_method == "SURFACE_FILL":
self.init_fill(context)
elif settings.brushstroke_method == "SURFACE_DRAW":
self.init_draw(context)
# select preset material
mat = bpy.data.materials.get('Brush Material')
if not mat:
mat = utils.import_brushstroke_material()
settings.silent_switch = True
settings.context_material = mat
settings.silent_switch = False
preset_object['BSBST_material'] = settings.context_material
return {"FINISHED"}
class BSBST_OT_make_preset(bpy.types.Operator):
"""
Make the current brushstrokes style specification the active preset.
"""
bl_idname = "brushstroke_tools.make_preset"
bl_label = "Make Preset"
bl_description = "Make the current brushstrokes style specification the active preset"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return bool(utils.get_active_context_brushstrokes_object(context.scene))
def execute(self, context):
settings = context.scene.BSBST_settings
if not settings.preset_object:
preset_name = f'BSBST-PRESET_{settings.brushstroke_method}'
settings.preset_object = bpy.data.objects.new(preset_name, bpy.data.hair_curves.new(preset_name))
else:
for mod in settings.preset_object.modifiers[:]:
settings.preset_object.modifiers.remove(mod)
# transfer brushstrokes modifiers to preset
bs_ob = utils.get_active_context_brushstrokes_object(context.scene)
if not bs_ob:
return {"CANCELLED"}
for mod in bs_ob.modifiers:
utils.transfer_modifier(mod.name, settings.preset_object, bs_ob)
utils.refresh_preset(None)
for mod in settings.preset_object.modifiers:
# refresh UI
mod.node_group.interface_update(context)
# identify linked sockets
for v in mod.node_group.interface.items_tree.values():
if type(v) not in utils.linkable_sockets:
continue
if not settings.preset_object.modifier_info[mod.name].socket_info[v.identifier]:
continue
if type(v) == bpy.types.NodeTreeInterfaceSocketObject:
if bs_ob['BSBST_surface_object']==mod[v.identifier]:
mod[v.identifier] = None
settings.preset_object.modifier_info[mod.name].socket_info[v.identifier].link_context = True
elif type(v) == bpy.types.NodeTreeInterfaceSocketMaterial:
pass # TODO: figure out material preset linking
return {"FINISHED"}
class BSBST_OT_preset_add_mod(bpy.types.Operator):
"""
Add a modifier to the preset stack.
"""
bl_idname = "brushstroke_tools.preset_add_mod"
bl_label = "Add Preset Modifier"
bl_description = "Add a modifier to the preset"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
settings = context.scene.BSBST_settings
return settings.preset_object
def execute(self, context):
settings = context.scene.BSBST_settings
settings.preset_object.modifiers.new('Brushstrokes Style', type='NODES')
return {"FINISHED"}
class BSBST_OT_preset_remove_mod(bpy.types.Operator):
"""
Remove a modifier from the preset stack.
"""
bl_idname = "brushstroke_tools.preset_remove_mod"
bl_label = "Remove Preset Modifier"
bl_description = "Remove a modifier from the preset"
bl_options = {"REGISTER", "UNDO"}
modifier: bpy.props.StringProperty(default='')
@classmethod
def poll(cls, context):
settings = context.scene.BSBST_settings
return settings.preset_object
def execute(self, context):
settings = context.scene.BSBST_settings
with context.temp_override(object=settings.preset_object):
bpy.ops.object.modifier_remove(modifier=self.modifier)
return {"FINISHED"}
class BSBST_OT_preset_toggle_attribute(bpy.types.Operator):
"""
Toggle use_attribute property for a socket on a specific object's modifier.
(Workaround due to how these are actually stored as integer in Blender)
"""
bl_idname = "brushstroke_tools.preset_toggle_attribute"
bl_label = "Toggle Attribute"
bl_description = "Toggle using a named attribute for this input"
bl_options = {"REGISTER", "UNDO"}
modifier_name: bpy.props.StringProperty(default='GeometryNodes')
input_name: bpy.props.StringProperty(default='Socket_2')
@classmethod
def poll(cls, context):
settings = context.scene.BSBST_settings
return settings.preset_object
def execute(self, context):
settings = context.scene.BSBST_settings
override = context.copy()
override['object'] = settings.preset_object
with context.temp_override(**override):
bpy.ops.object.geometry_nodes_input_attribute_toggle(input_name=self.input_name,
modifier_name=self.modifier_name)
return {"FINISHED"}
class BSBST_OT_brushstrokes_toggle_attribute(bpy.types.Operator):
"""
Toggle use_attribute property for a socket on a specific object's modifier.
(Workaround due to how these are actually stored as integer in Blender)
"""
bl_idname = "brushstroke_tools.brushstrokes_toggle_attribute"
bl_label = "Toggle Attribute"
bl_description = "Toggle using a named attribute for this input"
bl_options = {"REGISTER", "UNDO"}
modifier_name: bpy.props.StringProperty(default='GeometryNodes')
input_name: bpy.props.StringProperty(default='Socket_2')
@classmethod
def poll(cls, context):
settings = context.scene.BSBST_settings
return settings.preset_object
def execute(self, context):
settings = context.scene.BSBST_settings
edit_toggle = settings.edit_toggle
settings.edit_toggle = False
bs_ob = utils.get_active_context_brushstrokes_object(context.scene)
if not bs_ob:
settings.edit_toggle = edit_toggle
return {"CANCELLED"}
override = context.copy()
override['object'] = bs_ob
with context.temp_override(**override):
bpy.ops.object.geometry_nodes_input_attribute_toggle(input_name=self.input_name,
modifier_name=self.modifier_name)
return {"FINISHED"}
class BSBST_OT_render_setup(bpy.types.Operator):
"""
Set up render settings.
"""
bl_idname = "brushstroke_tools.render_setup"
bl_label = "Render Setup"
bl_description = "Set up render settings"
bl_options = {"REGISTER", "UNDO"}
render_engine: bpy.props.EnumProperty(name='Render Engine',
items = [
('ALL', 'All', 'Set up for all available render engines', '', 0),
('CYCLES', 'Cycles', 'Set up for Cycles', '', 1),
('EEVEE', 'Eevee', 'Set up for Eevee', '', 2),
]
)
trans_pass_toggle: bpy.props.BoolProperty(default=True)
trans_pass: bpy.props.IntProperty(name='Transparency Passes', default=256, min=0, soft_max=1024)
prop_map = {
'CYCLES':['trans_pass',
],
'EEVEE':[
]
}
def draw(self, context):
layout = self.layout
layout.prop(self, 'render_engine', text='')
for k, v in self.prop_map.items():
if self.render_engine not in [k, 'ALL']:
continue
layout.label(text=k.capitalize())
for prop in v:
split = layout.split(factor=.1)
split.prop(self, f'{prop}_toggle', icon_only=True)
split.active = getattr(self, f'{prop}_toggle', False)
split.prop(self, prop)
def execute(self, context):
settings = context.scene.BSBST_settings
if self.render_engine in ['CYCLES', 'ALL']:
if self.trans_pass_toggle:
context.scene.cycles.transparent_max_bounces = self.trans_pass
return {"FINISHED"}
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
class BSBST_OT_view_all(bpy.types.Operator):
"""
Enable/disable all brushstrokes for the viewport.
"""
bl_idname = "brushstroke_tools.view_all"
bl_label = "Enable/Disable All"
bl_description = "Enable/disable all brushstrokes for the viewport"
bl_options = {"REGISTER", "UNDO"}
disable: bpy.props.BoolProperty(default=False)
def execute(self, context):
for ob in bpy.data.objects:
if not utils.is_brushstrokes_object(ob):
continue
if ob.hide_viewport == self.disable:
continue
else:
ob.hide_viewport = self.disable
return {"FINISHED"}
def brush_style_category_items(self, context):
addon_prefs = context.preferences.addons[__package__].preferences
items = [
('ALL', 'All', '', '', 0),
]
available_categories = set(bs.category for bs in addon_prefs.brush_styles)
for category_name in available_categories:
if not category_name:
continue
items.append((category_name.upper(), category_name, '', '', len(items)))
return items
def brush_style_type_items(self, context):
addon_prefs = context.preferences.addons[__package__].preferences
items = [
('ALL', 'All', '', '', 0),
]
available_types = set(bs.type for bs in addon_prefs.brush_styles)
for type_name in available_types:
if not type_name:
continue
items.append((type_name.upper(), type_name, '', '', len(items)))
return items
class BSBST_UL_brush_styles_filtered(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
resource_dir = utils.get_resource_directory()
if self.layout_type in {'DEFAULT', 'COMPACT'}:
split = layout.split(factor=0.5)
if not item.filepath:
split.label(text=item.name, icon='FILE_BLEND')
else:
split.label(text=item.name, icon='BRUSHES_ALL')
row = split.row()
row.label(text=item.category)
row = split.row()
row.label(text=item.type)
elif self.layout_type == 'GRID':
layout.label(text=item.name)
def draw_filter(self, context, layout):
return
class BSBST_OT_select_brush_style(bpy.types.Operator):
"""
Select Brush Style for context material.
"""
bl_idname = "brushstroke_tools.select_brush_style"
bl_label = "Select Brush Style"
bl_description = "Select Brush Style"
bl_options = {"REGISTER", "UNDO"}
name_filter: bpy.props.StringProperty(name='Name Filter', default='', update=utils.update_filtered_brush_styles)
brush_category: bpy.props.EnumProperty(name='Category', items=brush_style_category_items, update=utils.update_filtered_brush_styles)
brush_type: bpy.props.EnumProperty(name='Type', items=brush_style_type_items, update=utils.update_filtered_brush_styles)
brush_styles_filtered: bpy.props.CollectionProperty(type=utils.BSBST_brush_style)
brush_styles_filtered_active_index: bpy.props.IntProperty()
@classmethod
def poll(cls, context):
settings = context.scene.BSBST_settings
if not settings.context_material:
return False
if settings.context_material.library:
return False
return True
def draw(self, context):
settings = context.scene.BSBST_settings
layout = self.layout
row = layout.row()
split = row.split(factor=.45)
split.label(text='Name')
split = split.split(factor=.45)
split.label(text='Category')
split.label(text='Type')
row = layout.row(align=True)
split = row.split(factor=.45)
split.prop(self, 'name_filter', text='', icon='FILTER')
split = split.split(factor=.45)
split.prop(self, 'brush_category', text='')
split.prop(self, 'brush_type', text='')
layout.template_list("BSBST_UL_brush_styles_filtered", "", self, "brush_styles_filtered",
self, "brush_styles_filtered_active_index", rows=3, maxrows=5, sort_lock=True)
def execute(self, context):
settings = context.scene.BSBST_settings
if len(self.brush_styles_filtered) == 0:
return {"CANCELLED"}
settings.context_material.brush_style = self.brush_styles_filtered[self.brush_styles_filtered_active_index].name
return {"FINISHED"}
def invoke(self, context, event):
settings = context.scene.BSBST_settings
utils.refresh_brushstroke_styles()
utils.update_filtered_brush_styles(self, context)
for i, bs in enumerate(self.brush_styles_filtered):
if bs.name == settings.context_material.brush_style:
self.brush_styles_filtered_active_index = i
break
self.name_filter = ''
return context.window_manager.invoke_props_dialog(self, width=450)
class BSBST_OT_upgrade_resources(bpy.types.Operator):
""" Upgrade all local BST assets to available addon resources.
"""
bl_idname = "brushstroke_tools.upgrade_resources"
bl_label = "Upgrade Resources"
bl_description = "Upgrade local BST assets to available addon resources."
bl_options = {"REGISTER", "UNDO"}
upgrade_shape_modifiers: bpy.props.BoolProperty(name='Shape Modifiers', default=True)
upgrade_materials: bpy.props.BoolProperty(name='Materials', default=True)
upgrade_brush_styles: bpy.props.BoolProperty(name='Brush Styles', default=True)
modifier_id_count = 0
modifier_user_count = 0
material_id_count = 0
material_user_count = 0
brush_style_id_count = 0
brush_style_user_count = 0
def draw(self, context):
settings = context.scene.BSBST_settings
layout = self.layout
split = layout.split(factor=.6)
col = split.column()
col.prop(self, 'upgrade_shape_modifiers', icon='MODIFIER')
col.prop(self, 'upgrade_materials', icon='MATERIAL')
col.prop(self, 'upgrade_brush_styles', icon='BRUSHES_ALL')
col = split.column()
row = col.row()
row.active = self.upgrade_shape_modifiers
row.label(text=f'({self.modifier_id_count} IDs, {self.modifier_user_count} users)')
row = col.row()
row.active = self.upgrade_materials
row.label(text=f'({self.material_id_count} IDs, {self.material_user_count} users)')
row = col.row()
row.active = self.upgrade_brush_styles
row.label(text=f'({self.brush_style_id_count} IDs, {self.brush_style_user_count} users)')
def execute(self, context):
settings = context.scene.BSBST_settings
if self.upgrade_shape_modifiers and self.modifier_id_count:
utils.upgrade_geonodes_from_library()
if self.upgrade_materials and self.material_id_count:
utils.upgrade_materials_from_library()
if self.upgrade_brush_styles and self.brush_style_id_count:
utils.upgrade_brush_styles_from_library()
return {"FINISHED"}
def invoke(self, context, event):
settings = context.scene.BSBST_settings
mod_map = utils.find_local_geonodes_resources()
self.modifier_id_count = len(mod_map)
self.modifier_user_count = sum([len(v) for k,v in mod_map.items()])
mat_map = utils.find_local_material_resources()
self.material_id_count = len(mat_map)
self.material_user_count = sum([len(v) for k,v in mat_map.items()])
bs_map = utils.find_local_brush_style_resources()
self.brush_style_id_count = len(bs_map)
self.brush_style_user_count = sum([len(v) for k,v in bs_map.items()])
return context.window_manager.invoke_props_dialog(self)
class BSBST_OT_new_material(bpy.types.Operator):
"""
"""
bl_idname = "brushstroke_tools.new_material"
bl_label = "New Brushstrokes Material"
bl_description = "Create new material for the current brushstrokes"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return bool(utils.context_brushstrokes(context))
def execute(self, context):
settings = context.scene.BSBST_settings
mat = utils.import_brushstroke_material()
settings.context_material = mat
return {"FINISHED"}
classes = [
BSBST_OT_new_brushstrokes,
BSBST_OT_edit_brushstrokes,
BSBST_OT_delete_brushstrokes,
BSBST_OT_duplicate_brushstrokes,
BSBST_OT_copy_brushstrokes,
BSBST_OT_copy_flow,
BSBST_OT_switch_deformable,
BSBST_OT_switch_animated,
BSBST_OT_select_surface,
BSBST_OT_assign_surface,
BSBST_OT_init_preset,
BSBST_OT_make_preset,
BSBST_OT_preset_add_mod,
BSBST_OT_preset_remove_mod,
BSBST_OT_preset_toggle_attribute,
BSBST_OT_brushstrokes_toggle_attribute,
BSBST_OT_view_all,
BSBST_OT_render_setup,
BSBST_UL_brush_styles_filtered,
BSBST_OT_select_brush_style,
BSBST_OT_upgrade_resources,
BSBST_OT_new_material,
]
def register():
for c in classes:
bpy.utils.register_class(c)
def unregister():
for c in reversed(classes):
bpy.utils.unregister_class(c)