What I’m trying to do:
I’m prototyping a Geo Nodes setup to apply shear (skew) to Grease Pencil (GP) data using a shared node stack.
Each selected object is assigned a stack that has these nodes inside of it:
This conversion from GP to curves back to GP is required because Blender does not seem to allow you to set positions of handles with GP layer data.
The current setup works for transforming control points and overall hierarchy, but there’s a key limitation:
There are sometimes missing pieces and fills for thi example, the brown part of the collar is missing from the jacket.
When using GP with Bézier curves, the deformation only affects control points, not the handles. This causes visible artifacts (e.g. wavy lines), since the curve tangents are not being updated.
However, that approach introduces problems:
- Loss of fills on some layers
- Added complexity and performance cost from conversion
Question
How can I deform (shear/skew) Bézier handles of Grease Pencil data directly in Geo Nodes, without converting to curves and back? How can I robustly keep the GP data consistent while shearing it?
I’m including a blend file with the character and the shear effect in it.
EinsteinSheer.blend (3.8 MB)
If you open up the blend file and unhide the ‘BrownCollar’ layer you’ll see it kills the fill on the face layer. I’m trying to avoid this issue occurring.
Then if you disabled the modifier on the head object, you’ll see that it appears as it should.
I have a script that sets all this up if you want to test it. This limitation is the wall in the project.
Test script
It will take a selected grease pencil object, and put a skewing modifier on it with the help of geometry nodes, one must Copy and paste the below script into blender’s script editor, select all children of an object, and have the driving object be the actively selected one (last selected/warm orange), then hit script window play button.
import bpy
MAIN_GROUP_NAME = "GN_Shear_XZ_ParentSample_GP_Rot_HandleTypeFix"
HELPER_GROUP_NAME = "GN_ParentFloatSample_GP_HandleTypeFix"
HANDLE_GROUP_NAME = "GN_ShearHandleOffset_GP_HandleTypeFix"
MOD_NAME = "Shear XZ Parent Sample GP Rot"
ATTR_SHEAR_X = "shear_parent_x"
ATTR_SHEAR_Z = "shear_parent_z"
# ------------------------------------------------------------
# basic helpers
# ------------------------------------------------------------
def delete_existing_group(name):
ng = bpy.data.node_groups.get(name)
if ng:
bpy.data.node_groups.remove(ng, do_unlink=True)
def new_node(nodes, ntype, name, x, y):
node = nodes.new(ntype)
node.name = name
node.label = name
node.location = (x, y)
return node
def new_node_any(nodes, type_names, name, x, y):
last_err = None
for t in type_names:
try:
n = nodes.new(t)
n.name = name
n.label = name
n.location = (x, y)
return n
except Exception as ex:
last_err = ex
raise RuntimeError(f"Could not create node '{name}'. Tried {type_names}. Last error: {last_err}")
def link(links, out_node, out_socket, in_node, in_socket):
links.new(out_node.outputs[out_socket], in_node.inputs[in_socket])
def iface_socket(iface, name, in_out, socket_type, description=""):
return iface.new_socket(
name=name,
description=description,
in_out=in_out,
socket_type=socket_type,
)
def add_frame(nodes, label, x, y, w=600, h=400):
frame = nodes.new("NodeFrame")
frame.label = label
frame.location = (x, y)
frame.width = w
frame.height = h
return frame
def parent_frame(frame, *nodes_):
for n in nodes_:
n.parent = frame
def reroute(nodes, x, y, frame=None):
rr = nodes.new("NodeReroute")
rr.location = (x, y)
if frame:
rr.parent = frame
return rr
# ------------------------------------------------------------
# helper group for parent float sampling
# ------------------------------------------------------------
def ensure_helper_group(force_rebuild=True):
if force_rebuild:
delete_existing_group(HELPER_GROUP_NAME)
ng = bpy.data.node_groups.get(HELPER_GROUP_NAME)
if ng and ng.bl_idname == "GeometryNodeTree":
return ng
ng = bpy.data.node_groups.new(HELPER_GROUP_NAME, "GeometryNodeTree")
ng.is_modifier = False
nodes = ng.nodes
links = ng.links
nodes.clear()
iface = ng.interface
iface_socket(iface, "Geometry", 'INPUT', "NodeSocketGeometry", "Geometry source to sample from.")
iface_socket(iface, "Use Parent", 'INPUT', "NodeSocketBool", "When enabled, sample the named float attribute from the parent geometry. Otherwise output zero.")
s = iface_socket(iface, "Attr Name", 'INPUT', "NodeSocketString", "Name of the float attribute to sample.")
s.default_value = ""
iface_socket(iface, "Value", 'OUTPUT', "NodeSocketFloat", "Sampled float value.")
ng.interface_update(bpy.context)
gi = new_node(nodes, "NodeGroupInput", "Group Input", -800, 0)
go = new_node(nodes, "NodeGroupOutput", "Group Output", 140, 0)
named_attr = new_node(nodes, "GeometryNodeInputNamedAttribute", "Named Attr", -600, -80)
named_attr.data_type = 'FLOAT'
sample = new_node(nodes, "GeometryNodeSampleIndex", "Sample Index", -360, -80)
sample.data_type = 'FLOAT'
sample.domain = 'LAYER'
sample.clamp = True
sample.inputs["Index"].default_value = 0
zero = new_node(nodes, "ShaderNodeValue", "Zero", -360, -240)
zero.outputs[0].default_value = 0.0
use_switch = new_node(nodes, "GeometryNodeSwitch", "Use Parent", -120, -80)
use_switch.input_type = 'FLOAT'
link(links, gi, "Attr Name", named_attr, "Name")
link(links, gi, "Geometry", sample, "Geometry")
link(links, named_attr, "Attribute", sample, "Value")
link(links, gi, "Use Parent", use_switch, "Switch")
link(links, zero, "Value", use_switch, "False")
link(links, sample, "Value", use_switch, "True")
link(links, use_switch, "Output", go, "Value")
return ng
# ------------------------------------------------------------
# helper group for relative handle offset
# outputs:
# Offset = extra handle deformation
# Affect = non-zero flag so zero-shear does not touch handles
# ------------------------------------------------------------
def ensure_handle_group(force_rebuild=True):
if force_rebuild:
delete_existing_group(HANDLE_GROUP_NAME)
ng = bpy.data.node_groups.get(HANDLE_GROUP_NAME)
if ng and ng.bl_idname == "GeometryNodeTree":
return ng
ng = bpy.data.node_groups.new(HANDLE_GROUP_NAME, "GeometryNodeTree")
ng.is_modifier = False
nodes = ng.nodes
links = ng.links
nodes.clear()
iface = ng.interface
iface_socket(iface, "Relative Handle", 'INPUT', "NodeSocketVector", "Handle position relative to control point.")
iface_socket(iface, "Use Parent", 'INPUT', "NodeSocketBool", "Use parent-aligned space.")
iface_socket(iface, "Inverse Parent Rotation", 'INPUT', "NodeSocketVector", "Negative parent Euler rotation.")
iface_socket(iface, "Parent Rotation", 'INPUT', "NodeSocketVector", "Parent Euler rotation.")
iface_socket(iface, "Shear X", 'INPUT', "NodeSocketFloat", "X shear.")
iface_socket(iface, "Shear Z", 'INPUT', "NodeSocketFloat", "Z shear.")
iface_socket(iface, "Offset", 'OUTPUT', "NodeSocketVector", "Additional handle offset.")
iface_socket(iface, "Affect", 'OUTPUT', "NodeSocketBool", "Whether this handle should be modified.")
ng.interface_update(bpy.context)
gi = new_node(nodes, "NodeGroupInput", "Group Input", -1320, 0)
go = new_node(nodes, "NodeGroupOutput", "Group Output", 760, 0)
to_parent = new_node(nodes, "ShaderNodeVectorRotate", "To Parent Space", -1040, 0)
to_parent.rotation_type = 'EULER_XYZ'
to_parent.invert = False
use_switch = new_node(nodes, "GeometryNodeSwitch", "Use Parent Space", -800, 0)
use_switch.input_type = 'VECTOR'
sep = new_node(nodes, "ShaderNodeSeparateXYZ", "Separate", -560, 0)
mul_x = new_node(nodes, "ShaderNodeMath", "Z * Shear X", -320, 120)
mul_x.operation = 'MULTIPLY'
mul_z = new_node(nodes, "ShaderNodeMath", "X * Shear Z", -320, -80)
mul_z.operation = 'MULTIPLY'
raw_offset = new_node(nodes, "ShaderNodeCombineXYZ", "Raw Offset", -60, 20)
raw_offset.inputs["Y"].default_value = 0.0
back = new_node(nodes, "ShaderNodeVectorRotate", "Back To Local", 180, 20)
back.rotation_type = 'EULER_XYZ'
back.invert = False
off_switch = new_node(nodes, "GeometryNodeSwitch", "Use Rotated Offset", 420, 20)
off_switch.input_type = 'VECTOR'
length = new_node(nodes, "ShaderNodeVectorMath", "Offset Length", 420, -180)
length.operation = 'LENGTH'
gt = new_node(nodes, "ShaderNodeMath", "Affect Compare", 620, -180)
gt.operation = 'GREATER_THAN'
gt.inputs[1].default_value = 1.0e-8
link(links, gi, "Relative Handle", to_parent, "Vector")
link(links, gi, "Inverse Parent Rotation", to_parent, "Rotation")
link(links, gi, "Use Parent", use_switch, "Switch")
link(links, gi, "Relative Handle", use_switch, "False")
link(links, to_parent, "Vector", use_switch, "True")
link(links, use_switch, "Output", sep, "Vector")
link(links, sep, "Z", mul_x, 0)
link(links, gi, "Shear X", mul_x, 1)
link(links, sep, "X", mul_z, 0)
link(links, gi, "Shear Z", mul_z, 1)
link(links, mul_x, "Value", raw_offset, "X")
link(links, mul_z, "Value", raw_offset, "Z")
link(links, raw_offset, "Vector", back, "Vector")
link(links, gi, "Parent Rotation", back, "Rotation")
link(links, gi, "Use Parent", off_switch, "Switch")
link(links, raw_offset, "Vector", off_switch, "False")
link(links, back, "Vector", off_switch, "True")
link(links, off_switch, "Output", length, 0)
link(links, length, "Value", gt, 0)
link(links, off_switch, "Output", go, "Offset")
link(links, gt, "Value", go, "Affect")
return ng
# ------------------------------------------------------------
# main group
# ------------------------------------------------------------
def ensure_main_group(force_rebuild=True):
if force_rebuild:
delete_existing_group(MAIN_GROUP_NAME)
helper = ensure_helper_group(force_rebuild=force_rebuild)
handle_helper = ensure_handle_group(force_rebuild=force_rebuild)
ng = bpy.data.node_groups.get(MAIN_GROUP_NAME)
if ng and ng.bl_idname == "GeometryNodeTree":
return ng
ng = bpy.data.node_groups.new(MAIN_GROUP_NAME, "GeometryNodeTree")
ng.is_modifier = True
nodes = ng.nodes
links = ng.links
nodes.clear()
iface = ng.interface
iface_socket(iface, "Geometry", 'INPUT', "NodeSocketGeometry", "Geometry to deform.")
s = iface_socket(iface, "Shear X", 'INPUT', "NodeSocketFloat", "X shear amount.")
s.default_value = 0.0
s = iface_socket(iface, "Shear Z", 'INPUT', "NodeSocketFloat", "Z shear amount.")
s.default_value = 0.0
s = iface_socket(
iface,
"Use Center Parent",
'INPUT',
"NodeSocketBool",
"Use another object's origin and orientation as the shared shear reference."
)
s.default_value = False
s = iface_socket(
iface,
"Center Parent",
'INPUT',
"NodeSocketObject",
"Controller object used for pivot, orientation, and inherited shear attributes."
)
s.default_value = None
iface_socket(iface, "Geometry", 'OUTPUT', "NodeSocketGeometry", "Deformed geometry.")
ng.interface_update(bpy.context)
gi = new_node(nodes, "NodeGroupInput", "Group Input", -3700, 0)
go = new_node(nodes, "NodeGroupOutput", "Group Output", 3080, 0)
fr_inputs = add_frame(nodes, "Inputs / Parent Ref", -3500, -260, 980, 560)
fr_parent = add_frame(nodes, "Parent Values", -3500, -1120, 980, 420)
fr_effective = add_frame(nodes, "Effective Values", -2400, -1120, 520, 340)
fr_space = add_frame(nodes, "Parent Space", -2400, -260, 860, 660)
fr_shear = add_frame(nodes, "Apply Shear", -1380, -220, 820, 560)
fr_handles = add_frame(nodes, "Handle Offset Fix", -260, -980, 2460, 1220)
fr_store = add_frame(nodes, "Export Attrs", 2280, -140, 1040, 380)
# --------------------------------------------------------
# point shear path
# --------------------------------------------------------
pos = new_node(nodes, "GeometryNodeInputPosition", "Position", -3300, 120)
rr_center_parent = reroute(nodes, -3460, -60, fr_inputs)
rr_use_parent = reroute(nodes, -3460, -160, fr_inputs)
obj_info = new_node(nodes, "GeometryNodeObjectInfo", "Center Parent Info", -3180, -120)
try:
obj_info.transform_space = 'RELATIVE'
except Exception:
pass
zero_vec = new_node(nodes, "ShaderNodeCombineXYZ", "Zero Pivot", -2910, -20)
zero_vec.inputs["X"].default_value = 0.0
zero_vec.inputs["Y"].default_value = 0.0
zero_vec.inputs["Z"].default_value = 0.0
pivot_switch = new_node(nodes, "GeometryNodeSwitch", "Use Parent Pivot", -2640, -40)
pivot_switch.input_type = 'VECTOR'
rel_pos = new_node(nodes, "ShaderNodeVectorMath", "Relative Position", -2370, 40)
rel_pos.operation = 'SUBTRACT'
parent_frame(fr_inputs, pos, rr_center_parent, rr_use_parent, obj_info, zero_vec, pivot_switch, rel_pos)
rr_parent_geo = reroute(nodes, -3370, -760, fr_parent)
rr_parent_use = reroute(nodes, -3370, -660, fr_parent)
sample_parent_x = new_node(nodes, "GeometryNodeGroup", "Parent Sample X", -3140, -760)
sample_parent_x.node_tree = helper
sample_parent_x.inputs["Attr Name"].default_value = ATTR_SHEAR_X
sample_parent_z = new_node(nodes, "GeometryNodeGroup", "Parent Sample Z", -3140, -980)
sample_parent_z.node_tree = helper
sample_parent_z.inputs["Attr Name"].default_value = ATTR_SHEAR_Z
parent_frame(fr_parent, rr_parent_geo, rr_parent_use, sample_parent_x, sample_parent_z)
shear_x_switch = new_node(nodes, "GeometryNodeSwitch", "Shear X Source", -2180, -760)
shear_x_switch.input_type = 'FLOAT'
shear_z_switch = new_node(nodes, "GeometryNodeSwitch", "Shear Z Source", -2180, -980)
shear_z_switch.input_type = 'FLOAT'
parent_frame(fr_effective, shear_x_switch, shear_z_switch)
rr_parent_rot = reroute(nodes, -2320, -120, fr_space)
inv_rot = new_node(nodes, "ShaderNodeVectorMath", "Inverse Parent Rotation", -2140, -40)
inv_rot.operation = 'MULTIPLY'
inv_rot.inputs[1].default_value = (-1.0, -1.0, -1.0)
to_parent_space = new_node(nodes, "ShaderNodeVectorRotate", "To Parent Space", -1860, 0)
to_parent_space.rotation_type = 'EULER_XYZ'
to_parent_space.invert = False
rel_switch = new_node(nodes, "GeometryNodeSwitch", "Use Aligned Relative Pos", -1580, 20)
rel_switch.input_type = 'VECTOR'
sep_shear_space = new_node(nodes, "ShaderNodeSeparateXYZ", "Separate Shear Space Pos", -1300, 20)
parent_frame(fr_space, rr_parent_rot, inv_rot, to_parent_space, rel_switch, sep_shear_space)
mul_x = new_node(nodes, "ShaderNodeMath", "Z * Shear X", -1080, 140)
mul_x.operation = 'MULTIPLY'
mul_z = new_node(nodes, "ShaderNodeMath", "X * Shear Z", -1080, -40)
mul_z.operation = 'MULTIPLY'
raw_offset = new_node(nodes, "ShaderNodeCombineXYZ", "Raw Offset XZ", -800, 40)
raw_offset.inputs["Y"].default_value = 0.0
back_to_local = new_node(nodes, "ShaderNodeVectorRotate", "Back To Local", -520, 40)
back_to_local.rotation_type = 'EULER_XYZ'
back_to_local.invert = False
offset_switch = new_node(nodes, "GeometryNodeSwitch", "Use Rotated Offset", -240, 40)
offset_switch.input_type = 'VECTOR'
set_pos = new_node(nodes, "GeometryNodeSetPosition", "Set Position", 40, 40)
parent_frame(fr_shear, mul_x, mul_z, raw_offset, back_to_local, offset_switch, set_pos)
# --------------------------------------------------------
# handle offset path
# --------------------------------------------------------
gp_to_curves = new_node(nodes, "GeometryNodeGreasePencilToCurves", "Grease Pencil to Curves", 320, 40)
realize = new_node(nodes, "GeometryNodeRealizeInstances", "Realize Instances", 580, 40)
handle_read = new_node_any(
nodes,
[
"GeometryNodeInputCurveHandlePositions",
"GeometryNodeCurveHandlePositions",
"GeometryNodeInputCurveHandlePosition",
"GeometryNodeCurveHandlePosition",
],
"Curve Handle Positions",
860,
-180
)
try:
handle_read.inputs["Relative"].default_value = True
except Exception:
pass
left_handle_group = new_node(nodes, "GeometryNodeGroup", "Left Handle Offset", 1220, -420)
left_handle_group.node_tree = handle_helper
right_handle_group = new_node(nodes, "GeometryNodeGroup", "Right Handle Offset", 1220, -20)
right_handle_group.node_tree = handle_helper
affect_or = new_node(nodes, "FunctionNodeBooleanMath", "Handle Affect OR", 1500, -560)
affect_or.operation = 'OR'
set_handle_type = new_node_any(
nodes,
["GeometryNodeCurveSetHandles"],
"Set Handle Type Free",
1500,
-260
)
try:
set_handle_type.mode = 'BOTH'
except Exception:
pass
try:
set_handle_type.handle_type = 'FREE'
except Exception:
pass
set_left = new_node_any(
nodes,
["GeometryNodeSetCurveHandlePositions", "GeometryNodeSetHandlePositions"],
"Set Left Handle",
1780,
-320
)
try:
set_left.mode = 'LEFT'
except Exception:
pass
set_right = new_node_any(
nodes,
["GeometryNodeSetCurveHandlePositions", "GeometryNodeSetHandlePositions"],
"Set Right Handle",
2040,
-320
)
try:
set_right.mode = 'RIGHT'
except Exception:
pass
curves_to_gp = new_node_any(
nodes,
["GeometryNodeCurvesToGreasePencil"],
"Curves to Grease Pencil",
2300,
-320
)
try:
curves_to_gp.inputs["Instances as Layers"].default_value = False
except Exception:
pass
parent_frame(
fr_handles,
gp_to_curves, realize, handle_read,
left_handle_group, right_handle_group,
affect_or, set_handle_type,
set_left, set_right, curves_to_gp
)
# --------------------------------------------------------
# export attrs
# --------------------------------------------------------
rr_use_parent_store = reroute(nodes, 2260, -60, fr_store)
store_x = new_node(nodes, "GeometryNodeStoreNamedAttribute", "Store Shear X Attr", 2640, 120)
store_x.data_type = 'FLOAT'
store_x.domain = 'LAYER'
store_x.inputs["Name"].default_value = ATTR_SHEAR_X
store_z = new_node(nodes, "GeometryNodeStoreNamedAttribute", "Store Shear Z Attr", 2940, 120)
store_z.data_type = 'FLOAT'
store_z.domain = 'LAYER'
store_z.inputs["Name"].default_value = ATTR_SHEAR_Z
export_switch = new_node(nodes, "GeometryNodeSwitch", "Export Or Bypass", 3240, 40)
export_switch.input_type = 'GEOMETRY'
parent_frame(fr_store, rr_use_parent_store, store_x, store_z, export_switch)
# --------------------------------------------------------
# wiring
# --------------------------------------------------------
link(links, gi, "Center Parent", rr_center_parent, 0)
link(links, gi, "Use Center Parent", rr_use_parent, 0)
link(links, rr_center_parent, 0, obj_info, "Object")
link(links, rr_use_parent, 0, pivot_switch, "Switch")
link(links, zero_vec, "Vector", pivot_switch, "False")
link(links, obj_info, "Location", pivot_switch, "True")
link(links, pos, "Position", rel_pos, 0)
link(links, pivot_switch, "Output", rel_pos, 1)
link(links, obj_info, "Geometry", rr_parent_geo, 0)
link(links, rr_use_parent, 0, rr_parent_use, 0)
link(links, rr_parent_geo, 0, sample_parent_x, "Geometry")
link(links, rr_parent_use, 0, sample_parent_x, "Use Parent")
link(links, rr_parent_geo, 0, sample_parent_z, "Geometry")
link(links, rr_parent_use, 0, sample_parent_z, "Use Parent")
link(links, rr_use_parent, 0, shear_x_switch, "Switch")
link(links, gi, "Shear X", shear_x_switch, "False")
link(links, sample_parent_x, "Value", shear_x_switch, "True")
link(links, rr_use_parent, 0, shear_z_switch, "Switch")
link(links, gi, "Shear Z", shear_z_switch, "False")
link(links, sample_parent_z, "Value", shear_z_switch, "True")
link(links, obj_info, "Rotation", rr_parent_rot, 0)
link(links, rr_parent_rot, 0, inv_rot, 0)
link(links, rr_parent_rot, 0, back_to_local, "Rotation")
link(links, rel_pos, "Vector", to_parent_space, "Vector")
link(links, inv_rot, "Vector", to_parent_space, "Rotation")
link(links, rr_use_parent, 0, rel_switch, "Switch")
link(links, rel_pos, "Vector", rel_switch, "False")
link(links, to_parent_space, "Vector", rel_switch, "True")
link(links, rel_switch, "Output", sep_shear_space, "Vector")
link(links, sep_shear_space, "Z", mul_x, 0)
link(links, shear_x_switch, "Output", mul_x, 1)
link(links, sep_shear_space, "X", mul_z, 0)
link(links, shear_z_switch, "Output", mul_z, 1)
link(links, mul_x, "Value", raw_offset, "X")
link(links, mul_z, "Value", raw_offset, "Z")
link(links, raw_offset, "Vector", back_to_local, "Vector")
link(links, rr_use_parent, 0, offset_switch, "Switch")
link(links, raw_offset, "Vector", offset_switch, "False")
link(links, back_to_local, "Vector", offset_switch, "True")
link(links, gi, "Geometry", set_pos, "Geometry")
set_pos.inputs["Selection"].default_value = True
link(links, offset_switch, "Output", set_pos, "Offset")
# GP -> curves
link(links, set_pos, "Geometry", gp_to_curves, "Grease Pencil")
link(links, gp_to_curves, "Curves", realize, "Geometry")
# Common inputs to both handle helpers
for gnode in (left_handle_group, right_handle_group):
link(links, rr_use_parent, 0, gnode, "Use Parent")
link(links, inv_rot, "Vector", gnode, "Inverse Parent Rotation")
link(links, rr_parent_rot, 0, gnode, "Parent Rotation")
link(links, shear_x_switch, "Output", gnode, "Shear X")
link(links, shear_z_switch, "Output", gnode, "Shear Z")
link(links, handle_read, "Left", left_handle_group, "Relative Handle")
link(links, handle_read, "Right", right_handle_group, "Relative Handle")
# Combine affect masks
link(links, left_handle_group, "Affect", affect_or, 0)
link(links, right_handle_group, "Affect", affect_or, 1)
# Force affected handles to FREE first
link(links, realize, "Geometry", set_handle_type, "Curve")
link(links, affect_or, "Boolean", set_handle_type, "Selection")
# Write handle offsets
link(links, set_handle_type, "Curve", set_left, "Curve")
link(links, left_handle_group, "Affect", set_left, "Selection")
link(links, left_handle_group, "Offset", set_left, "Offset")
link(links, set_left, "Curve", set_right, "Curve")
link(links, right_handle_group, "Affect", set_right, "Selection")
link(links, right_handle_group, "Offset", set_right, "Offset")
link(links, set_right, "Curve", curves_to_gp, "Curves")
# Export attrs on final GP result
link(links, rr_use_parent, 0, rr_use_parent_store, 0)
link(links, curves_to_gp, "Grease Pencil", store_x, "Geometry")
store_x.inputs["Selection"].default_value = True
link(links, shear_x_switch, "Output", store_x, "Value")
link(links, store_x, "Geometry", store_z, "Geometry")
store_z.inputs["Selection"].default_value = True
link(links, shear_z_switch, "Output", store_z, "Value")
link(links, rr_use_parent_store, 0, export_switch, "Switch")
link(links, store_z, "Geometry", export_switch, "False")
link(links, curves_to_gp, "Grease Pencil", export_switch, "True")
link(links, export_switch, "Output", go, "Geometry")
return ng
# ------------------------------------------------------------
# modifier utilities
# ------------------------------------------------------------
def interface_input_identifiers(node_group):
ids = {}
for item in node_group.interface.items_tree:
if hasattr(item, "in_out") and hasattr(item, "identifier"):
if item.in_out == 'INPUT':
ids[item.name] = item.identifier
return ids
def ensure_modifier(obj, node_group):
for m in obj.modifiers:
if m.type == 'NODES' and m.node_group == node_group:
m.name = MOD_NAME
return m
mod = obj.modifiers.new(MOD_NAME, 'NODES')
mod.node_group = node_group
return mod
def set_mod_input(mod, id_map, name, value):
ident = id_map.get(name)
if ident is None:
raise KeyError(f"Input '{name}' not found. Available: {list(id_map.keys())}")
mod[ident] = value
def apply_to_selection():
ctx = bpy.context
selected = list(ctx.selected_objects)
active = ctx.active_object
if not selected:
raise RuntimeError("No objects selected.")
if active is None:
raise RuntimeError("No active object. Select the controller last so it becomes active.")
supported = {'GREASEPENCIL'}
if active.type not in supported:
raise RuntimeError(f"Active object {active.name} is unsupported type: {active.type}")
ng = ensure_main_group(force_rebuild=True)
id_map = interface_input_identifiers(ng)
print("INPUT IDS:", id_map)
required = ["Shear X", "Shear Z", "Use Center Parent", "Center Parent"]
for name in required:
if name not in id_map:
raise RuntimeError(f"Missing group input: {name}")
updated = []
skipped = []
for obj in selected:
if obj.type not in supported:
skipped.append(f"{obj.name} ({obj.type})")
continue
try:
mod = ensure_modifier(obj, ng)
if obj == active:
set_mod_input(mod, id_map, "Use Center Parent", False)
set_mod_input(mod, id_map, "Center Parent", None)
updated.append(f"{obj.name} [controller]")
else:
set_mod_input(mod, id_map, "Use Center Parent", True)
set_mod_input(mod, id_map, "Center Parent", active)
updated.append(f"{obj.name} [child -> {active.name}]")
except Exception as ex:
skipped.append(f"{obj.name} ({obj.type}) | {ex}")
print("\n=== Applied ===")
print("Controller:", active.name)
print("Updated:", updated if updated else "None")
print("Skipped:", skipped if skipped else "None")
apply_to_selection()
For more information, I’m journaling all of this here:
-S


