Files
AnyImage/tools/nodes/common/nodes.py
nan 7f878a2f3f Optimize Cutout Geometry Nodes
- Refactor Depth Cutout to reuse the projected outline field, reducing redundant boundary calculations and improving performance.
- Derive the center field from shared low-frequency fields, eliminating an unnecessary blur operation.
- Implement triangle-only strip cleanup for Depth Cutout, as the input is confirmed to be triangulated.
- Short-circuit zero-thickness branches in both Cutout and Depth Cutout to avoid unnecessary geometry generation.
- Standardize internal attribute naming with a new `_o_` prefix and implement wildcard cleanup for temporary attributes.
- Update relevant tests to ensure functionality remains consistent and validate changes against existing behavior.
2026-09-16 09:11:25 +08:00

177 lines
6.5 KiB
Python

"""Build interfaces and basic nodes shared by asset definitions."""
from types import SimpleNamespace
def prepare_node_group(node_group):
"""Hide unused outputs and discard disconnected interface nodes."""
for node in list(node_group.nodes):
for socket in node.outputs:
socket.hide = not socket.is_linked
if node.bl_idname == "NodeGroupInput" and not any(socket.is_linked for socket in node.outputs):
node_group.nodes.remove(node)
def replace_link_source(node_group, link, source):
"""Replace a source while preserving multi-input socket order."""
multi = link.to_socket.is_multi_input
replacement = node_group.links.new(source, link.to_socket)
if multi:
replacement.swap_multi_input_sort_id(link)
node_group.links.remove(link)
def interface_socket(group, name, in_out, socket_type, parent=None):
socket = group.interface.new_socket(
name=name,
in_out=in_out,
socket_type=socket_type,
parent=parent,
)
if in_out == "INPUT" and hasattr(socket, "structure_type"):
socket.structure_type = "SINGLE"
return socket
def group_input(nodes, visible_names):
node = nodes.new("NodeGroupInput")
visible = set(visible_names)
for output in node.outputs:
output.hide = output.name not in visible
return node
def float_input(
group,
name,
default,
minimum=0.0,
maximum=None,
subtype=None,
parent=None,
):
socket = interface_socket(group, name, "INPUT", "NodeSocketFloat", parent)
socket.default_value = default
socket.min_value = minimum
if maximum is not None:
socket.max_value = maximum
if subtype is not None:
socket.subtype = subtype
return socket
def bool_input(group, name, default=False, parent=None):
socket = interface_socket(group, name, "INPUT", "NodeSocketBool", parent)
socket.default_value = default
return socket
def vector_input(group, name, default, subtype=None, parent=None):
socket = interface_socket(group, name, "INPUT", "NodeSocketVector", parent)
socket.default_value = default
if subtype is not None:
socket.subtype = subtype
return socket
def math_node(nodes, operation):
node = nodes.new("ShaderNodeMath")
node.operation = operation
return node
def menu_switch(group, control, names, kind):
selector = next((node for node in group.nodes if node.bl_idname == "GeometryNodeMenuSwitch" and node.inputs["Menu"].is_linked and node.inputs["Menu"].links[0].from_socket.name == control), None)
if selector is None:
selector = group.nodes.new("GeometryNodeMenuSwitch")
selector.data_type = "BOOLEAN"
for item, name in zip(selector.enum_items, names):
item.name = name
selector.inputs[names[0]].default_value = False
selector.inputs[names[1]].default_value = True
local = group_input(group.nodes, {control})
group.links.new(local.outputs[control], selector.inputs["Menu"])
node = group.nodes.new("GeometryNodeSwitch")
node.input_type = kind
group.links.new(selector.outputs[0], node.inputs["Switch"])
return SimpleNamespace(inputs={names[0]: node.inputs["False"], names[1]: node.inputs["True"]}, outputs=node.outputs)
def read_float_attribute(group, name):
node = group.nodes.new("GeometryNodeInputNamedAttribute")
node.data_type = "FLOAT"
node.inputs["Name"].default_value = name
return node.outputs["Attribute"]
def store_float_attribute(group, geometry, name, value, domain="POINT"):
node = group.nodes.new("GeometryNodeStoreNamedAttribute")
node.domain, node.data_type = domain, "FLOAT"
node.inputs["Name"].default_value = name
group.links.new(geometry, node.inputs["Geometry"])
if isinstance(value, (int, float)):
node.inputs["Value"].default_value = value
else:
group.links.new(value, node.inputs["Value"])
return node.outputs["Geometry"]
def evaluate_field(group, value, data_type, domain):
"""Evaluate a field on an explicit domain before downstream adaptation."""
node = group.nodes.new("GeometryNodeFieldOnDomain")
node.data_type, node.domain = data_type, domain
group.links.new(value, node.inputs["Value"])
return node.outputs["Value"]
def sample_field(group, geometry, value, data_type, domain="POINT", index=None):
"""Evaluate a field on an explicit source geometry and index domain."""
node = group.nodes.new("GeometryNodeSampleIndex")
node.data_type, node.domain = data_type, domain
node.clamp = False
if index is None:
index = group.nodes.new("GeometryNodeInputIndex").outputs["Index"]
group.links.new(geometry, node.inputs["Geometry"])
group.links.new(value, node.inputs["Value"])
group.links.new(index, node.inputs["Index"])
return node.outputs["Value"]
def store_boolean_attribute(group, geometry, name, value, domain="POINT"):
"""Store a topology marker needed after destructive mesh operations."""
node = group.nodes.new("GeometryNodeStoreNamedAttribute")
node.domain, node.data_type = domain, "BOOLEAN"
node.inputs["Name"].default_value = name
group.links.new(geometry, node.inputs["Geometry"])
group.links.new(value, node.inputs["Value"])
read = group.nodes.new("GeometryNodeInputNamedAttribute")
read.data_type = "BOOLEAN"
read.inputs["Name"].default_value = name
return node.outputs["Geometry"], read.outputs["Attribute"]
def remove_attributes(group, geometry, *names, enabled=None):
"""Remove only the explicitly owned temporary attributes."""
original = geometry
for name in names:
node = group.nodes.new("GeometryNodeRemoveAttribute")
node.inputs["Name"].default_value = name
group.links.new(geometry, node.inputs["Geometry"])
geometry = node.outputs["Geometry"]
if enabled is not None:
choose = group.nodes.new("GeometryNodeSwitch")
choose.input_type = "GEOMETRY"
group.links.new(enabled, choose.inputs["Switch"])
group.links.new(original, choose.inputs["False"])
group.links.new(geometry, choose.inputs["True"])
geometry = choose.outputs["Output"]
return geometry
def remove_attribute_pattern(group, geometry, pattern):
"""Remove every attribute matching an internal wildcard pattern."""
node = group.nodes.new("GeometryNodeRemoveAttribute")
node.inputs["Name"].default_value = pattern
node.pattern_mode = "WILDCARD"
group.links.new(geometry, node.inputs["Geometry"])
return node.outputs["Geometry"]