Files
AnyImage/tools/nodes/arrangement/arrange.py
nan dc000e50b1 feat: Implement weight cutout boundary smoothing for image depth cutout
- Added a new proposal for boundary smoothing in the weight cutout process, differentiating between Split edges and Outline smoothing requirements.
- Introduced a new capability `cutout-boundary-smoothing` to control smoothing strength and default values.
- Updated specifications to define the behavior of boundary-specific smoothing strength and default settings.
- Created tasks to verify the implementation of boundary behavior and asset updates.
- Developed comprehensive tests for depth symmetry and normal scaling to ensure expected behavior and performance.
- Introduced new attributes for normal mapping to facilitate persistent face attributes sharing between depth symmetry and image materials.
- Built a new node group for O Image Depth Symmetry, integrating the new smoothing capabilities and ensuring compatibility with existing workflows.
2026-09-14 12:44:20 +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 ..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)