Files
blender-portable-repo/extensions/blender_org/brushstroke_tools/geomod.py
T
2026-07-14 14:44:58 -06:00

189 lines
7.2 KiB
Python

# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
"""
Compatibility wrappers for the GeoNodes modifier input API.
Blender 5.2 replaced dict-style custom-property access on GeoNodes modifiers
with proper RNA properties. All modifier input reads and writes in this add-on
go through these helpers so version branching stays in one place.
Before 5.2: modifier["identifier"] = value
5.2+: modifier.properties.inputs.identifier.value = value
"""
from typing import Any
import bpy
from bpy.types import NodesModifier, UILayout, NodeTreeInterfaceSocket
_ICON_FOR_SOCKET = {
bpy.types.NodeTreeInterfaceSocketObject: 'OBJECT_DATA',
bpy.types.NodeTreeInterfaceSocketMaterial: 'MATERIAL',
bpy.types.NodeTreeInterfaceSocketImage: 'IMAGE_DATA',
bpy.types.NodeTreeInterfaceSocketCollection: 'OUTLINER_COLLECTION',
}
_DATA_COLLECTION_FOR_SOCKET = {
bpy.types.NodeTreeInterfaceSocketMaterial: 'materials',
bpy.types.NodeTreeInterfaceSocketImage: 'images',
bpy.types.NodeTreeInterfaceSocketCollection: 'collections',
}
def get_value(mod: NodesModifier, identifier: str) -> Any:
if bpy.app.version >= (5, 2, 0):
return getattr(mod.properties.inputs, identifier).value
return mod[identifier]
def set_value(mod: NodesModifier, identifier: str, value: Any):
if bpy.app.version >= (5, 2, 0):
getattr(mod.properties.inputs, identifier).value = value
else:
prop_ui = mod.id_properties_ui(identifier).as_dict()
if 'items' in prop_ui.keys():
for item in prop_ui['items']:
if item[0] == value:
mod[identifier] = item[4]
return
print(f"Error: Item '{value}' not found in menu '{identifier}' of modifier '{mod.name}'")
else:
mod[identifier] = value
def is_input_value_settable(mod: NodesModifier, identifier: str) -> bool:
"""Return True if the modifier exposes a settable input with this identifier."""
if bpy.app.version >= (5, 2, 0):
inp = getattr(mod.properties.inputs, identifier, None)
return inp is not None and hasattr(inp, 'value')
return identifier in mod.keys()
def can_input_be_attribute(mod: NodesModifier, identifier: str) -> bool:
"""Return True if this input can be driven by an attribute field."""
if bpy.app.version >= (5, 2, 0):
inp = getattr(mod.properties.inputs, identifier, None)
if inp is None or not hasattr(inp, 'type'):
return False
return any(item.identifier == 'ATTRIBUTE'
for item in inp.bl_rna.properties['type'].enum_items)
return f'{identifier}_use_attribute' in mod.keys()
def get_input_is_attribute(mod: NodesModifier, identifier: str) -> bool:
"""Return True if this input is currently being driven by an attribute field."""
if bpy.app.version >= (5, 2, 0):
return getattr(mod.properties.inputs, identifier).type == 'ATTRIBUTE'
return mod[f'{identifier}_use_attribute']
def set_input_is_attribute(mod: NodesModifier, identifier: str, value: bool):
if bpy.app.version >= (5, 2, 0):
getattr(mod.properties.inputs, identifier).type = 'ATTRIBUTE' if value else 'VALUE'
else:
mod[f'{identifier}_use_attribute'] = value
def set_attribute_name(mod: NodesModifier, identifier: str, name: str):
if bpy.app.version >= (5, 2, 0):
getattr(mod.properties.inputs, identifier).attribute_name = name
else:
mod[f'{identifier}_attribute_name'] = name
def get_enum_value_to_compare(mod: NodesModifier, identifier: str) -> list[tuple[str, str | int]]:
"""Return list of (identifier_str, comparable_value) pairs for a Menu socket.
The comparable_value matches whatever get_value() returns for the same socket:
an identifier string on 5.2+ (where .value is a string), an integer on older versions."""
if bpy.app.version >= (5, 2, 0):
inp = getattr(mod.properties.inputs, identifier)
return [(item.identifier, item.identifier)
for item in inp.bl_rna.properties['value'].enum_items]
return [(item[0], item[4])
for item in mod.id_properties_ui(identifier).as_dict()['items']]
def copy_inputs(source_mod: NodesModifier, target_mod: NodesModifier):
"""Copy all GeoNodes input values (and attribute settings) between modifiers."""
if bpy.app.version >= (5, 2, 0):
props = source_mod.properties
if not props:
return
target_props = target_mod.properties
for socket_name in props.inputs.keys():
source_socket = getattr(props.inputs, socket_name, None)
target_socket = getattr(target_props.inputs, socket_name, None)
if source_socket and target_socket and hasattr(source_socket, "value"):
target_socket.value = source_socket.value
else:
for key, value in source_mod.items():
try:
target_mod[key] = value
except (TypeError, ValueError):
target_mod[key] = type(target_mod[key])(value)
def draw_socket(
layout: UILayout,
mod: NodesModifier,
v: NodeTreeInterfaceSocket,
label: str,
is_preset: bool,
):
"""Draw a GeoNodes input socket: value widget plus attribute toggle if supported."""
if can_input_be_attribute(mod, v.identifier) and not v.force_non_field:
if get_input_is_attribute(mod, v.identifier):
_draw_attribute_name(layout, mod, v.identifier, text=label)
else:
_draw_input_simple(layout, mod, v.identifier, text=label)
operator_id = ('brushstroke_tools.preset_toggle_attribute' if is_preset
else 'brushstroke_tools.brushstrokes_toggle_attribute')
toggle = layout.operator(operator_id, text='',
depress=get_input_is_attribute(mod, v.identifier),
icon='SPREADSHEET')
toggle.modifier_name = mod.name
toggle.input_name = v.identifier
else:
socket_type = type(v)
icon = _ICON_FOR_SOCKET.get(socket_type, 'NONE')
search_prop = _DATA_COLLECTION_FOR_SOCKET.get(socket_type)
_draw_input_simple(
layout, mod, v.identifier,
search_prop=search_prop,
text=label, icon=icon,
)
def _draw_attribute_name(
layout: UILayout,
mod: NodesModifier,
identifier: str,
**kwargs,
):
"""Draw a modifier input's attribute-name field."""
if bpy.app.version >= (5, 2, 0):
layout.prop(getattr(mod.properties.inputs, identifier), 'attribute_name', **kwargs)
else:
layout.prop(mod, f'["{identifier}_attribute_name"]', **kwargs)
def _draw_input_simple(
layout: UILayout,
mod: NodesModifier,
identifier: str,
search_prop: str | None = None,
**kwargs,
):
"""Draw a modifier input value widget.
Pass search_prop for prop_search on Blender < 4.2.
Later versions use pointers, so no need for prop_search.
"""
if bpy.app.version >= (5, 2, 0):
layout.prop(getattr(mod.properties.inputs, identifier), 'value', **kwargs)
elif search_prop:
layout.prop_search(mod, f'["{identifier}"]', bpy.data, search_prop, **kwargs)
else:
layout.prop(mod, f'["{identifier}"]', **kwargs)