Files
AnyImage/tools/nodes/arrangement/fields.py
nan 5b2dc59f79 Implement arrangement framework for node groups
- Introduced `frames.py` to manage field complex calculations and their arrangement within node groups.
- Added functionality to create and remove field frames based on node arrangements.
- Developed `passages.py` to find unobstructed paths for connections around calculation frames.
- Implemented `references.py` to route shared results on peripheral tracks, ensuring efficient distribution of outputs.
- Created `sections.py` to reserve space for field calculations adjacent to geometry consumers.
- Added `sources.py` to position shared calculation sources effectively beside their distribution channels.
- Enhanced outline smoothing with `outline_smoothing.py` to adapt camera depth along original outlines.
2026-09-12 12:19:07 +08:00

222 lines
10 KiB
Python

"""Lay out expression dependencies with ordered operands and reserved wire passages."""
from collections import defaultdict
from dataclasses import dataclass
from graphlib import TopologicalSorter
import numpy as np
from scipy.optimize import linprog
from .flow import peer_links
@dataclass
class FieldLayout:
positions: dict
width: float
height: float
routes: list
def field_layout(nodes, parents, peers, sizes, *, gap, target_height):
"""Order dependency layers, reserve long-link passages, and solve socket heights."""
roots = {node: node for node in nodes}
def root(node):
while roots[node] != node:
node = roots[node]
return node
for sources in peers:
members = [node for node in sources if node in roots]
if sum(sizes[node].height for node in members) + gap * (len(members) - 1) > target_height:
continue
for node in members[1:]:
roots[root(node)] = root(members[0])
# Stack exclusive calculation chains without disturbing ordered operands.
operands = {node for sources in peers if len(sources) > 1 for node in sources}
chain_heights = {node: sizes[node].height for node in nodes}
simple = {"ShaderNodeMath", "ShaderNodeVectorMath", "ShaderNodeClamp"}
for node in nodes:
sources = [parent for parent in parents[node] if parent in roots]
if node.bl_idname not in simple or node in operands or len(sources) != 1:
continue
source = sources[0]
if source in operands or {link.to_node for socket in source.outputs for link in socket.links} != {node}:
continue
first, last = root(source), root(node)
height = chain_heights[first] + gap + chain_heights[last]
if first != last and height <= target_height:
roots[last] = first
chain_heights[first] = height
groups = defaultdict(list)
for node in nodes:
groups[root(node)].append(node)
for sources in peers:
members = [node for node in sources if node in roots]
if members and all(root(node) == root(members[0]) for node in members):
groups[root(members[0])].sort(key=lambda node: sources.index(node) if node in sources else len(sources))
blocks = list(groups.values())
block_of = {node: index for index, block in enumerate(blocks) for node in block}
children = {index: set() for index in range(len(blocks))}
for node in nodes:
for parent in parents[node]:
if parent in block_of and block_of[parent] != block_of[node]:
children[block_of[parent]].add(block_of[node])
ranks = {}
for block in TopologicalSorter(children).static_order():
ranks[block] = max((ranks[child] + 1 for child in children[block]), default=0)
depth = max(ranks.values())
ranks = {block: depth - rank for block, rank in ranks.items()}
heights = [sum(sizes[node].height for node in block) + gap * (len(block) - 1) for block in blocks]
offsets = {}
for index, block in enumerate(blocks):
cursor = -heights[index] / 2
for node in block:
offsets[node] = cursor
cursor += sizes[node].height + gap
layers = defaultdict(list)
for block, rank in ranks.items():
layers[rank].append(block)
operand_columns = {rank for node in nodes for link in peer_links(node)
if link.from_node in block_of
for rank in range(ranks[block_of[link.from_node]], ranks[block_of[node]] + 1)}
# Keep an operand block and its junction together when spreading tall layers.
columns, column = defaultdict(list), 0
for rank in sorted(layers):
used = 0
for block in layers[rank]:
if rank not in operand_columns and used and used + gap + heights[block] > target_height:
column += 1
used = 0
columns[column].append(block)
ranks[block] = column
used += heights[block] + (gap if used else 0)
column += 1
layers = columns
depth = column - 1
links = [link for node in nodes for socket in node.inputs for link in socket.links if link.from_node in block_of]
edges, passages, vertical = [], [], []
for link in links:
first, last = block_of[link.from_node], block_of[link.to_node]
if first == last:
vertical.append(link)
continue
start_offset = offsets[link.from_node] + sizes[link.from_node].sockets[link.from_socket]
end_offset = offsets[link.to_node] + sizes[link.to_node].sockets[link.to_socket]
previous, previous_offset, path = first, start_offset, []
for rank in range(ranks[first] + 1, ranks[last]):
point = len(heights)
heights.append(16)
ranks[point] = rank
layers[rank].append(point)
edges.append((previous, point, previous_offset, 0))
previous, previous_offset = point, 0
path.append(point)
edges.append((previous, last, previous_offset, end_offset))
if path:
passages.append((link, path))
incoming, outgoing = defaultdict(list), defaultdict(list)
for first, last, a, b in edges:
outgoing[first].append((last, a, b))
incoming[last].append((first, b, a))
centers = {}
def stack(layer):
height = sum(heights[item] for item in layer) + sum(separation(a, b) for a, b in zip(layer, layer[1:]))
cursor = -height / 2
for index, item in enumerate(layer):
centers[item] = cursor + heights[item] / 2
cursor += heights[item] + (separation(item, layer[index + 1]) if index + 1 < len(layer) else 0)
def separation(first, last):
return gap if first < len(blocks) and last < len(blocks) else gap / 2
for layer in layers.values():
stack(layer)
for _ in range(4):
for ranks_in_order, adjacent in ((range(depth + 1), incoming), (range(depth, -1, -1), outgoing)):
for rank in ranks_in_order:
layer = layers[rank]
def desired(item):
neighbors = adjacent[item]
return sum(centers[other] + their_offset - own_offset
for other, own_offset, their_offset in neighbors) / len(neighbors) if neighbors else centers[item]
layer.sort(key=lambda item: (desired(item), item))
stack(layer)
count = len(heights)
variable_count = count + 1 + len(edges)
inequalities, bounds, equalities, values = [], [], [], []
def constraint(terms, maximum):
row = np.zeros(variable_count)
for index, coefficient in terms:
row[index] += coefficient
inequalities.append(row)
bounds.append(maximum)
for index, height in enumerate(heights):
constraint([(index, -1)], -height / 2)
constraint([(index, 1), (count, -1)], -height / 2)
for layer in layers.values():
for first, last in zip(layer, layer[1:]):
constraint([(first, 1), (last, -1)], -(heights[first] + heights[last]) / 2 - separation(first, last))
for index, (first, last, a, b) in enumerate(edges):
slack = count + 1 + index
constraint([(first, 1), (last, -1), (slack, -1)], b - a)
constraint([(last, 1), (first, -1), (slack, -1)], a - b)
for node in nodes:
inputs = [link.from_socket for link in peer_links(node)]
if len(inputs) < 2 or any(socket.node not in block_of for socket in inputs):
continue
output = next(socket for socket in node.outputs if socket in sizes[node].sockets)
first, last = inputs[0], inputs[-1]
row = np.zeros(variable_count)
row[block_of[node]] += 2
row[block_of[first.node]] -= 1
row[block_of[last.node]] -= 1
equalities.append(row)
values.append(offsets[first.node] + sizes[first.node].sockets[first]
+ offsets[last.node] + sizes[last.node].sockets[last]
- 2 * (offsets[node] + sizes[node].sockets[output]))
objective = np.zeros(variable_count)
objective[count] = len(edges) + 1
objective[count + 1:] = 1
result = linprog(objective, A_ub=np.array(inequalities), b_ub=bounds,
A_eq=np.array(equalities) if equalities else None, b_eq=values or None,
bounds=[(None, None)] * count + [(0, None)] * (1 + len(edges)), method="highs")
if not result.success:
raise ValueError(f"Expression layers could not be placed: {result.message}")
centers = result.x[:count]
positions, point_positions, right = {}, {}, 0
vertical_blocks = {block_of[link.from_node] for link in vertical}
for rank in range(depth + 1):
layer = layers[rank]
width = max(max(sizes[node].width for node in blocks[item]) if item < len(blocks) else 16 for item in layer)
padding = gap / 2 if any(item in vertical_blocks for item in layer) else 0
if rank:
right += gap
right += width + padding * 2
for item in layer:
if item < len(blocks):
for node in blocks[item]:
positions[node] = (right - padding - sizes[node].width, -centers[item] - offsets[node])
else:
point_positions[item] = (right, -centers[item])
routes = []
for link, path in passages:
points = [point_positions[item] for item in path]
points = [point for index, point in enumerate(points) if index == 0 or index == len(points)-1
or not abs(points[index-1][1] - point[1]) < 1e-6 or not abs(points[index+1][1] - point[1]) < 1e-6]
routes.append((link, points))
for link in vertical:
source, target = link.from_node, link.to_node
x1, y1 = positions[source]
x2, y2 = positions[target]
middle = (y1 - sizes[source].height + y2) / 2
routes.append((link, [(x1 + sizes[source].width + gap / 2, y1 - sizes[source].sockets[link.from_socket]),
(x1 + sizes[source].width + gap / 2, middle),
(x2 - gap / 2, middle), (x2 - gap / 2, y2 - sizes[target].sockets[link.to_socket])]))
return FieldLayout(positions, right, result.x[count], routes)