Files
AnyImage/tools/nodes/arrangement/arrange.py
nan 9debab5c0c Add comprehensive tests for image processing nodes and refactor node imports
- Introduced new test files for image layer, image plane, normal scale, object normal rotation, shadeless, and temporary attributes.
- Implemented tests to validate the behavior of alpha curves, depth layers, and normal scales in various rendering engines.
- Refactored import paths for node groups to align with the new directory structure.
- Added checks for temporary attribute lifetimes and ensured proper cleanup without warnings.
- Enhanced CLI commands to require the Blender group and validate the presence of generated node assets before packaging.
- Updated packaging scripts to enforce the existence of necessary assets, preventing build failures.
2026-09-19 21:23:19 +08:00

131 lines
5.6 KiB
Python

"""Materialize the joint calculation and connection plan in node trees."""
from .branches import calculation_blocks
from .dimensions import estimate_node_dimensions, visible_socket
from .flow import origin_socket, socket_links
from .layout import plan_layout
from nodes.common.nodes import prepare_node_group, replace_link_source
def _remove_routes(node_group):
previous = [node for node in node_group.nodes if node.bl_idname == "NodeReroute"]
for node in [node for node in node_group.nodes if node not in previous]:
for socket in node.inputs:
links = socket_links(socket)
if not any(link.from_node in previous for link in links):
continue
if not socket.is_multi_input:
replace_link_source(node_group, links[0], origin_socket(links[0].from_socket))
continue
sources = [origin_socket(link.from_socket) for link in links]
for link in links:
node_group.links.remove(link)
seen = set()
for source in reversed(sources):
original = source
if source in seen:
# Blender coalesces identical direct links; retain repeated inputs.
duplicate = node_group.nodes.new("NodeReroute")
node_group.links.new(source, duplicate.inputs[0])
source = duplicate.outputs[0]
seen.add(original)
node_group.links.new(source, socket)
for node in previous:
node_group.nodes.remove(node)
def _localize_inputs(node_group, nodes, blocks, core):
"""Read interface values locally in each consuming calculation body."""
ids = {node: i for i, node in enumerate(nodes)}
block_of = {i: k for k, block in enumerate(blocks) for i in block["members"]}
for node in list(nodes):
if node.bl_idname != "NodeGroupInput":
continue
original = ("core", ids[node]) if ids[node] in core else ("block", block_of[ids[node]])
uses = list(node_group.links)
direct = [ids[edge.to_node] for edge in uses if edge.from_node == node and ids[edge.to_node] in core]
if direct:
original = ("core", direct[0])
local_reads = {original: node}
for edge in uses:
if edge.from_node != node:
continue
target = ids[edge.to_node]
owner = ("block", block_of[target]) if target in block_of else ("core", target)
if owner not in local_reads:
clone = node_group.nodes.new("NodeGroupInput")
ids[clone] = len(nodes)
nodes.append(clone)
if owner[0] == "block":
blocks[owner[1]]["members"].append(ids[clone])
block_of[ids[clone]] = owner[1]
else:
core.add(ids[clone])
local_reads[owner] = clone
source = next(
socket
for socket in local_reads[owner].outputs
if socket.identifier == edge.from_socket.identifier
)
if source != edge.from_socket:
replace_link_source(node_group, edge, source)
def arrange_nodes(node_group):
"""Arrange calculation bodies and shared channels before creating reroutes."""
for frame in [node for node in node_group.nodes if node.bl_idname == "NodeFrame"]:
for child in [node for node in node_group.nodes if node.parent == frame]:
position = tuple(child.location_absolute)
child.parent = None
child.location = position
node_group.nodes.remove(frame)
_remove_routes(node_group)
nodes = list(node_group.nodes)
if not nodes:
return
blocks, core = calculation_blocks(nodes, list(node_group.links))
_localize_inputs(node_group, nodes, blocks, core)
prepare_node_group(node_group)
for node in nodes:
node.select = False
if node.bl_idname in {"ShaderNodeMath", "ShaderNodeVectorMath", "FunctionNodeIntegerMath"}:
node.hide = all(socket.is_linked for socket in node.inputs if visible_socket(socket))
sizes = {i: estimate_node_dimensions(node) for i, node in enumerate(nodes)}
edges = list(node_group.links)
positions, routes = plan_layout(nodes, edges, sizes, blocks, core)
for i, (x, y) in positions.items():
nodes[i].location = (x, -y)
nodes[i].width = sizes[i].width
cache = {}
for index, path in routes.items():
edge = edges[index]
source = edge.from_socket
for x, y in path[1:-1]:
key = (source, round(x, 4), round(y, 4))
reroute = cache.get(key)
if reroute is None:
reroute = node_group.nodes.new("NodeReroute")
reroute.location = (x - 8, -y + 8)
node_group.links.new(source, reroute.inputs[0])
cache[key] = reroute
source = reroute.outputs[0]
if len(path) > 2:
replace_link_source(node_group, edge, source)
for block in blocks:
if len(block["members"]) < 5:
continue
left, top, right, bottom = block["box"]
frame = node_group.nodes.new("NodeFrame")
frame["arrangement_frame"] = True
frame.name = frame.label = block["title"]
frame.shrink = False
frame.location = (left - 28, -top + 48)
frame.width = right - left + 56
frame.height = bottom - top + 76
for i in block["members"]:
absolute = nodes[i].location.copy()
nodes[i].parent = frame
nodes[i].location = absolute - frame.location
prepare_node_group(node_group)