Files
AnyImage/tools/nodes/arrangement/arrange.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

682 lines
34 KiB
Python

"""Arrange geometry paths, local fields, and shared references after construction."""
from collections import defaultdict
from dataclasses import dataclass, field
from graphlib import TopologicalSorter
import numpy as np
from scipy.optimize import LinearConstraint, minimize
from .dimensions import estimate_node_dimensions, visible_socket
from .flow import graph, geometry_paths, origin_socket, peer_links, shared_geometry_outputs, socket_links
from .connections import curve_footprint, link_endpoints
from .fields import field_layout
from .frames import create_field_frames, remove_field_frames
from .references import route_references
from .passages import route_field_passages
from .sections import clear_repeat_regions, enclose_repeat_branches, reserve_field_sections
from .sources import place_shared_sources
from .zones import node_zones
from ..common.nodes import replace_link_source
GEOMETRY_GAP = 100
FIELD_GAP = 40
BRANCH_GAP = 160
SECTION_GAP = 200
FIELD_HEIGHT = 720
ROUTE_GAP = 40
ROUTE_RUN = 120
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 _peer_inputs(node, ancestors):
sources = list(dict.fromkeys(
link.from_node for link in peer_links(node)
if link.from_node.bl_idname != "NodeGroupInput"
))
upstream = set().union(*(ancestors[source] for source in sources))
return [source for source in sources if source not in upstream]
def _localize_peer_inputs(node_group, ancestors, children):
"""Give shared inputs local ports without moving their public source."""
for node in list(node_group.nodes):
links = peer_links(node)
if len(links) < 2:
continue
sources = [link.from_node for link in links]
upstream = set().union(*(ancestors[source] for source in sources))
for link in links:
source = link.from_node
if source.bl_idname == "NodeReroute":
source["arrangement_input"] = True
continue
# Zone exits keep their own lane when a later peer merge is placed.
if source.bl_idname not in {"NodeGroupInput", "GeometryNodeRepeatOutput"} and len(children[source]) <= 1 and sources.count(source) == 1 and source not in upstream:
continue
port = node_group.nodes.new("NodeReroute")
port["arrangement_input"] = True
node_group.links.new(link.from_socket, port.inputs[0])
replace_link_source(node_group, link, port.outputs[0])
def _columns(nodes, parents, peers, sizes, gap):
"""Allocate columns with equal right edges for independent peer inputs."""
roots = {node: node for node in nodes}
def root(node):
while roots[node] != node:
node = roots[node]
return node
for sources in peers:
members = [source for source in sources if source in roots]
for source in members[1:]:
roots[root(source)] = root(members[0])
dependencies = {root(node): [] for node in nodes}
for node in nodes:
for parent in parents[node]:
if parent in roots and root(parent) not in dependencies[root(node)]:
dependencies[root(node)].append(root(parent))
levels = {}
for component in TopologicalSorter(dependencies).static_order():
levels[component] = max((levels[p] + 1 for p in dependencies[component]), default=0)
columns = defaultdict(list)
for node in nodes:
columns[levels[root(node)]].append(node)
right, positions = 0, {}
for level in sorted(columns):
right += max(sizes[node].width for node in columns[level])
for node in columns[level]:
positions[node] = right - sizes[node].width
right += gap
return positions, columns
def _field_regions(nodes, core, children, order):
"""Group connected calculations by the geometry inputs that consume them."""
targets = {}
fields = set(nodes) - core
for node in reversed(order):
targets[node] = frozenset(link.to_socket for socket in node.outputs for link in socket.links
if link.to_node in core) | frozenset().union(
*(targets[child] for child in children[node] if child not in core))
for node in order:
if node.get("arrangement_exit"):
targets[node] = targets[origin_socket(node.outputs[0]).node]
regions = defaultdict(list)
for node in order:
if node in fields:
regions[targets[node]].append(node)
result = []
for sockets, region in regions.items():
remaining = set(region)
neighbors = {node: set(children[node]) & remaining for node in region}
for node, adjacent in list(neighbors.items()):
for other in list(adjacent):
neighbors[other].add(node)
for node in region:
if node not in remaining:
continue
component, pending = set(), [node]
while pending:
current = pending.pop()
if current not in remaining:
continue
remaining.remove(current)
component.add(current)
pending.extend(neighbors[current])
result.append(([member for member in region if member in component], {socket.node for socket in sockets}))
return result
def _localize_module_outputs(node_group, core, children):
"""Expose reused results at the boundary of their calculation module."""
shared = set(shared_geometry_outputs(node_group.nodes))
for link in list(node_group.links):
source = link.from_node
if link.to_node not in core or source in core or source.bl_idname == "NodeGroupInput":
continue
if link.from_socket in shared or not any(node not in core for node in children[source]):
continue
port = node_group.nodes.new("NodeReroute")
port["arrangement_exit"] = True
node_group.links.new(link.from_socket, port.inputs[0])
replace_link_source(node_group, link, port.outputs[0])
def _localize_inputs(node_group):
"""Give each operation local interface reads, separating geometry from values."""
controls = [node for node in node_group.nodes if node.bl_idname == "NodeGroupInput"]
uses = defaultdict(list)
for link in node_group.links:
if link.from_node in controls:
uses[link.to_node, link.from_socket.type == "GEOMETRY"].append((link, link.from_socket.identifier))
for inputs in uses.values():
local = node_group.nodes.new("NodeGroupInput")
for link, identifier in inputs:
source = next(socket for socket in local.outputs if socket.identifier == identifier)
replace_link_source(node_group, link, source)
for node in controls:
node_group.nodes.remove(node)
def _presentation(node_group):
for node in node_group.nodes:
node.select = False
for socket in node.outputs:
socket.hide = not socket.is_linked
if node.bl_idname in {"ShaderNodeMath", "ShaderNodeVectorMath"}:
node.hide = all(socket.is_linked for socket in node.inputs if visible_socket(socket))
def _place_geometry_paths(paths, geometry_x, sizes, order):
"""Reuse fixed horizontal lanes; field sizes never participate in lane choice."""
paths = [list(path) for path in paths]
for path in list(paths):
for node in path:
paired = getattr(node, "paired_output", None)
if paired is None or paired not in geometry_x:
continue
first = next(nodes for nodes in paths if node in nodes)
last = next(nodes for nodes in paths if paired in nodes)
if first is not last:
first.extend(last)
paths.remove(last)
pitch = max(sizes[node].height for path in paths for node in path) + SECTION_GAP
path_of = {node: index for index, path in enumerate(paths) for node in path}
preceding = {index: set() for index in range(len(paths))}
for node in order:
inputs = list(dict.fromkeys(path_of[link.from_node] for link in peer_links(node)
if link.from_node in path_of))
for first, second in zip(inputs, inputs[1:]):
pending, seen = [first], set()
while pending:
current = pending.pop()
if current not in seen:
seen.add(current)
pending.extend(preceding[current])
if second not in seen:
preceding[second].add(first)
lanes, positions, rows = defaultdict(list), {}, {}
pending = set(preceding)
while pending:
ready = [index for index in pending if not preceding[index] & pending]
index = min(ready, key=lambda index: (-len(paths[index]), geometry_x[paths[index][0]], index))
path = paths[index]
left = min(geometry_x[node] for node in path)
right = max(geometry_x[node] + sizes[node].width for node in path)
minimum = max((rows[previous] + 1 for previous in preceding[index]), default=-len(paths))
for lane in sorted(range(minimum, len(paths) * 2), key=lambda value: (abs(value), value)):
if all(right + FIELD_GAP <= a or left >= b + FIELD_GAP for a, b in lanes[lane]):
break
lanes[lane].append((left, right))
rows[index] = lane
pending.remove(index)
for node in path:
positions[node] = (geometry_x[node], -lane * pitch)
# Solve the lane heights together so successive merges cannot create a staircase.
equalities, centers, inequalities, separations = [], [], [], []
for node in order:
sources = [link.from_socket for link in peer_links(node)]
if node not in path_of or len(sources) < 2 or any(socket.node not in path_of for socket in sources):
continue
output = next(socket for socket in node.outputs if socket in sizes[node].sockets)
first, last = sources[0], sources[-1]
row = np.zeros(len(paths))
row[path_of[node]] += 2
row[path_of[first.node]] -= 1
row[path_of[last.node]] -= 1
equalities.append(row)
centers.append(2 * sizes[node].sockets[output] - sizes[first.node].sockets[first] - sizes[last.node].sockets[last])
spans = [(min(geometry_x[node] for node in path), max(geometry_x[node] + sizes[node].width for node in path))
for path in paths]
local_ports = {index for index, path in enumerate(paths)
if all(node.get("arrangement_input") for node in path)}
pairs = {(first, second) for second, sources in preceding.items() for first in sources}
for first in range(len(paths)):
for second in range(first + 1, len(paths)):
if first in local_ports or second in local_ports:
continue
if spans[first][0] < spans[second][1] + FIELD_GAP and spans[first][1] + FIELD_GAP > spans[second][0]:
pairs.add((first, second) if rows[first] < rows[second] else (second, first))
for first, second in sorted(pairs):
row = np.zeros(len(paths))
row[first], row[second] = 1, -1
inequalities.append(row)
gap = FIELD_GAP if first in local_ports or second in local_ports else SECTION_GAP
separations.append(max(sizes[node].height for node in paths[first]) + gap)
weights = np.array([max(1, len(path)) ** 2 for path in paths], dtype=float)
weights /= weights.max()
constraints = []
if equalities:
constraints.append(LinearConstraint(np.array(equalities), np.array(centers) / pitch, np.array(centers) / pitch))
if inequalities:
constraints.append(LinearConstraint(np.array(inequalities), np.array(separations) / pitch, np.inf))
compact = []
for node in order:
if node not in path_of:
continue
for link in peer_links(node):
if link.from_node in path_of and path_of[link.from_node] in local_ports:
row = np.zeros(len(paths))
row[path_of[link.from_node]], row[path_of[node]] = 1, -1
compact.append(row)
metric = np.diag(weights)
if compact:
matrix = np.array(compact)
metric += matrix.T @ matrix * 4
solution = minimize(lambda values: values @ metric @ values,
np.array([-rows[index] for index in range(len(paths))], dtype=float),
jac=lambda values: 2 * metric @ values, constraints=constraints,
method="SLSQP", options={"ftol": 1e-12, "maxiter": 300})
if not solution.success:
raise ValueError(f"Geometry lane constraints could not be placed: {solution.message}")
for index, path in enumerate(paths):
for node in path:
positions[node] = (positions[node][0], float(solution.x[index] * pitch))
for node in order:
if node not in positions:
continue
links = [link for socket in node.inputs if socket.type == "GEOMETRY" for link in socket.links
if link.from_node in positions]
if not links:
continue
x = positions[node][0]
required = []
for link in links:
source = link.from_socket
source_y = positions[source.node][1] - sizes[source.node].sockets[source]
target_y = positions[node][1] - sizes[node].sockets[link.to_socket]
required.append(positions[source.node][0] + sizes[source.node].width
+ max(GEOMETRY_GAP, abs(target_y - source_y) * 0.8))
extra = max(0, max(required) - x)
if extra:
for member, (left, y) in positions.items():
if left >= x:
positions[member] = (left + extra, y)
return positions
def _geometry_corridors(paths, positions, sizes):
"""Reserve continuous breathing room along every horizontal geometry path."""
corridors = []
for path in paths:
if all(node.get("arrangement_input") for node in path):
continue
boxes = [_node_box(node, positions, sizes) for node in path]
corridors.append((min(box[0] for box in boxes) - GEOMETRY_GAP / 2,
max(box[1] for box in boxes) + GEOMETRY_GAP / 2,
max(box[2] for box in boxes), min(box[3] for box in boxes)))
for port in positions:
if not port.get("arrangement_input") or port.outputs[0].type != "GEOMETRY":
continue
source = origin_socket(port.outputs[0]).node
if source not in positions:
continue
left = positions[source][0] + sizes[source].width
right, level = positions[port][0] + 8, positions[port][1] - 8
if right > left:
corridors.append((left, right, level + ROUTE_GAP, level - ROUTE_GAP))
return corridors
def _node_box(node, positions, sizes):
x, y = positions[node]
return x, x + sizes[node].width, y, y - sizes[node].height
def _overlap(first, second, gap):
a, b, top, bottom = first
c, d, upper, lower = second
return a < d + gap and b > c - gap and top > lower - gap and bottom < upper + gap
@dataclass
class BranchSpace:
"""Keep module bodies and their exit channels in one placement reservation."""
bodies: list = field(default_factory=list)
exits: list = field(default_factory=list)
routes: list = field(default_factory=list)
def connections(self, outlets, placed, sizes):
exits, routes = [], []
for links in outlets:
for link in links:
start, end = link_endpoints(link, placed, sizes)
corners = []
run = max(ROUTE_RUN, abs(end[1] - start[1]) * 0.8)
if end[0] - start[0] > max(ROUTE_RUN * 4, run + ROUTE_RUN * 2):
corners = [(start[0] + ROUTE_RUN, start[1]),
(max(start[0] + ROUTE_RUN, end[0] - run), start[1])]
points = [start, *dict.fromkeys(corners), end]
boxes = [box for a, b in zip(points, points[1:]) for box in curve_footprint(a, b)]
exits.append((link, boxes))
routes.append((link, list(dict.fromkeys(corners))))
return exits, routes
def fits(self, body, exits):
if any(_overlap(body, box, ROUTE_GAP) for _, boxes in self.exits for box in boxes):
return False
for link, boxes in exits:
if any(_overlap(box, rectangle, ROUTE_GAP) for nodes, rectangle in self.bodies
if link.to_node not in nodes for box in boxes):
return False
return True
def crossings(self, exits):
return sum(any(_overlap(box, previous, 8) for box in boxes for previous in channel)
for link, boxes in exits for other, channel in self.exits
if link.to_node != other.to_node and link.from_socket != other.from_socket)
def _place_field_region(local, width, height, destinations, owners, positions, sizes, occupied, limits, core, space, *, allow_opposite=False, peripheral=False, repeat_consumer=False):
"""Place an upstream module together with its forward-facing exit channels."""
right = min(positions[node][0] for node in destinations) - FIELD_GAP
baseline = sum(positions[node][1] for node in owners) / len(owners)
output = next(node for node in core if node.bl_idname == "NodeGroupOutput" and node.is_active_output)
ceiling = max(positions[node][1] for node in owners)
floor = min(positions[node][1] - sizes[node].height for node in owners)
axis_top = positions[output][1]
axis_bottom = axis_top - sizes[output].height
core_left = min(positions[node][0] for node in core)
side = 1 if floor > axis_top else -1 if ceiling < axis_bottom else 0
if allow_opposite:
side = 0
desired = right - width
front_allowed = all(link.from_node in local
for node in local for socket in node.inputs for link in socket.links)
outlets, constraints = [], []
for node, (x, y) in local.items():
for socket in node.outputs:
links = list(socket.links)
if socket.type != "GEOMETRY" and not peripheral:
returns = [link for link in links if link.to_node in core]
if returns:
outlets.append(returns)
for link in links:
if link.to_node in positions and link.to_node not in local:
target_x, target_y = positions[link.to_node]
constraints.append((target_x - x - sizes[node].width,
y - sizes[node].sockets[socket],
target_y - sizes[link.to_node].sockets[link.to_socket]))
constraints.sort()
exit_slope = 0.2 if width < 1000 and not peripheral and len(destinations) == 1 and destinations[0] in core and all(
socket.type != "GEOMETRY" for node in local for socket in node.outputs
if any(link.to_node in positions and link.to_node not in local for link in socket.links)
) else 0.8
if repeat_consumer and not peripheral:
exit_slope = 0.4
candidates = {desired, *(left - BRANCH_GAP - width for left, _, _, _ in occupied),
*(end + BRANCH_GAP for _, end, _, _ in occupied)}
best, seen = None, set()
for left in sorted(candidates, key=lambda x: (abs(x - desired), x)):
tops = {ceiling + BRANCH_GAP + height, floor - BRANCH_GAP,
limits[0], limits[1] + height,
*(top + BRANCH_GAP + height for _, _, top, _ in occupied),
*(bottom - BRANCH_GAP for _, _, _, bottom in occupied)}
tops.update(top + BRANCH_GAP + height for _, boxes in space.exits for _, _, top, _ in boxes)
tops.update(bottom - BRANCH_GAP for _, boxes in space.exits for _, _, _, bottom in boxes)
if front_allowed:
tops.add(baseline + height / 2)
for top in sorted(tops, key=lambda y: abs(y - height / 2 - baseline)):
if top > limits[0] or top - height < limits[1]:
continue
placed_left = min(left, desired)
placed_left = min([placed_left, *(end - max(GEOMETRY_GAP, abs(source_y + top - target_y) * exit_slope)
for end, source_y, target_y in constraints)])
# Leave clearance at saturated link constraints for solver roundoff.
placed_left -= 0.01
in_front = placed_left + width + BRANCH_GAP <= core_left
if not (in_front and front_allowed) and ((side == 1 and top - height < ceiling + BRANCH_GAP - 1e-5)
or (side == -1 and top > floor - BRANCH_GAP + 1e-5)):
continue
point = (placed_left, top)
if point in seen:
continue
seen.add(point)
distance = abs(placed_left - desired)
base_score = (sum(end - placed_left + abs(source_y + top - target_y) * 1.5
for end, source_y, target_y in constraints) / len(constraints)
if constraints else distance + abs(top - height / 2 - baseline) * 1.5)
if best is not None and round(base_score, 6) > best[0]:
continue
overlapping = [box for box in occupied
if placed_left < box[1] + BRANCH_GAP and placed_left + width > box[0] - BRANCH_GAP]
if any(top > bottom - BRANCH_GAP + 1e-5 and top - height < upper + BRANCH_GAP - 1e-5
for _, _, upper, bottom in overlapping):
continue
placed = positions | {node: (x + placed_left, y + top) for node, (x, y) in local.items()}
exits, routes = space.connections(outlets, placed, sizes)
body = (placed_left, placed_left + width, top, top - height)
if not space.fits(body, exits):
continue
score = base_score + space.crossings(exits) * BRANCH_GAP * 4
candidate = (round(score, 6), round(distance, 6), -top, placed_left, top)
if best is None or candidate < best:
best = candidate
best_exits, best_routes = exits, routes
if best is None:
if not allow_opposite:
return _place_field_region(
local, width, height, destinations, owners, positions, sizes,
occupied, limits, core, space, allow_opposite=True, peripheral=peripheral,
repeat_consumer=repeat_consumer,
)
raise ValueError("The field region does not fit its reserved scope")
_, _, _, left, top = best
for node, (x, y) in local.items():
positions[node] = (x + left, y + top)
body = (left, left + width, top, top - height)
occupied.append(body)
space.bodies.append((set(local), body))
space.exits.extend(best_exits)
space.routes.extend(best_routes)
def _route_field_links(node_group, layouts, positions):
"""Materialize the passages reserved between expression columns."""
for layout in layouts:
anchor = next(iter(layout.positions))
dx = positions[anchor][0] - layout.positions[anchor][0]
dy = positions[anchor][1] - layout.positions[anchor][1]
for link, points in layout.routes:
source = link.from_socket
for x, y in points:
node = node_group.nodes.new("NodeReroute")
node["arrangement_branch"] = True
node.location = (x + dx - 8, y + dy + 8)
node_group.links.new(source, node.inputs[0])
source = node.outputs[0]
replace_link_source(node_group, link, source)
def _route_branch_exits(node_group, routes):
"""Materialize the channels reserved during placement, sharing trunk corners."""
deliveries = defaultdict(list)
for link, points in routes:
deliveries[link.from_socket].append((link, points))
for source, branches in deliveries.items():
corners = {}
for point in sorted({point for _, points in branches for point in points}):
node = node_group.nodes.new("NodeReroute")
node["arrangement_branch"] = True
node.location = (point[0] - 8, point[1] + 8)
node_group.links.new(source, node.inputs[0])
source = corners[point] = node.outputs[0]
for link, points in branches:
if points:
replace_link_source(node_group, link, corners[points[-1]])
def _route_peer_bypasses(node_group, sizes, positions, ancestors):
"""Use an entry and an exit corner for a branch that bypasses processing."""
for port in list(node_group.nodes):
if not port.get("arrangement_input") or port.outputs[0].type != "GEOMETRY":
continue
source = origin_socket(port.outputs[0])
junction = port.outputs[0].links[0].to_node
processed = [origin_socket(link.from_socket).node for link in peer_links(junction)
if source.node in ancestors[origin_socket(link.from_socket).node]]
if not processed:
continue
left = positions[source.node][0] + sizes[source.node].width
right, level = positions[port][0] + 8, positions[port][1] - 8
if right - left < ROUTE_GAP * 2:
continue
branch_nodes = {node for end in processed for node in ancestors[end] | {end}
if source.node in ancestors[node] and any(socket.type == "GEOMETRY" for socket in node.outputs)
and node.bl_idname != "NodeReroute"}
first = min(positions[node][0] for node in branch_nodes)
run = max(ROUTE_RUN, abs(level - positions[source.node][1] + sizes[source.node].sockets[source]) * 0.8)
x = min(right - ROUTE_GAP, max(left + min(run, (right - left) / 2), first - ROUTE_GAP))
entry = node_group.nodes.new("NodeReroute")
entry["arrangement_bypass"] = True
node_group.links.new(source, entry.inputs[0])
replace_link_source(node_group, port.inputs[0].links[0], entry.outputs[0])
positions[entry] = (x - 8, level + 8)
sizes[entry] = estimate_node_dimensions(entry)
def arrange_nodes(node_group):
"""Fix geometry lanes, pack shallow field regions, and route shared references."""
remove_field_frames(node_group)
_remove_routes(node_group)
parents, children, order, ancestors = graph(node_group)
_localize_peer_inputs(node_group, ancestors, children)
parents, children, order, ancestors = graph(node_group)
geometry_parents, paths = geometry_paths(node_group)
core = set(geometry_parents)
_localize_module_outputs(node_group, core, children)
_localize_inputs(node_group)
_presentation(node_group)
parents, children, order, ancestors = graph(node_group)
sizes = {node: estimate_node_dimensions(node) for node in order}
for node, size in sizes.items():
node.width = size.width
peers = [_peer_inputs(node, ancestors) for node in order]
regions = _field_regions(node_group.nodes, core, children, order)
for region, _ in regions:
outlets = [link.from_node for node in order if node in core for socket in node.inputs
for link in socket_links(socket) if link.from_node in region]
peers.append(list(dict.fromkeys(outlets)))
# Preserve execution order through shared geometry and field calculations.
preceding = {}
column_parents = {}
for node in order:
sources = set().union(*(preceding[parent] for parent in parents[node]))
if node in core:
column_parents[node] = sources
preceding[node] = {node} if node in core else sources
geometry_x, _ = _columns([node for node in order if node in core], column_parents, peers, sizes, GEOMETRY_GAP)
positions = _place_geometry_paths(paths, geometry_x, sizes, order)
space = BranchSpace()
layouts, field_layouts = [], []
for region, targets in regions:
layout = field_layout(region, parents, peers, sizes, gap=FIELD_GAP, target_height=FIELD_HEIGHT)
field_layouts.append(layout)
layouts.append((region, targets, layout.positions, layout.width, layout.height))
reserve_field_sections(node_group, layouts, positions, sizes, gap=BRANCH_GAP)
occupied = _geometry_corridors(paths, positions, sizes)
reserve = max([FIELD_HEIGHT, *(height for _, _, _, _, height in layouts)]) * 2 + BRANCH_GAP * 2
limits = (max(y for _, y in positions.values()) + reserve,
min(y - sizes[node].height for node, (_, y) in positions.items()) - reserve)
pending = list(layouts)
shared_nodes = {socket.node for socket in shared_geometry_outputs(node_group.nodes)}
peripheral_regions = set()
for region, _, _, _, _ in layouts:
uses = defaultdict(set)
for node in region:
for socket in node.outputs:
for link in socket.links:
if link.to_node not in region:
uses[origin_socket(socket)].add(link.to_node)
if any(len(consumers) > 1 and (socket.type != "GEOMETRY" or not consumers & core)
for socket, consumers in uses.items()):
peripheral_regions.add(region[0])
while True:
peripheral_nodes = {node for region, _, _, _, _ in layouts if region[0] in peripheral_regions for node in region}
upstream = {region[0] for region, _, _, _, _ in layouts
if region[0] not in peripheral_regions
and (consumers := {link.to_node for node in region for socket in node.outputs
for link in socket.links if link.to_node not in region})
and consumers <= peripheral_nodes}
if not upstream:
break
peripheral_regions.update(upstream)
def delivery_order(layout):
ports = [link.to_socket for node in layout[0] for socket in node.outputs
for link in socket.links if link.to_node in core]
return min(((positions[socket.node][0], list(socket.node.inputs).index(socket))
for socket in ports), default=(float("inf"), 0))
repeat_consumers = {node for entry, end, members in node_zones(node_group)
if entry.bl_idname == "GeometryNodeRepeatInput"
for node in members & core if node not in {entry, end}}
while pending:
ready = [layout for layout in pending if all(
link.to_node in positions or link.to_node in layout[0]
for node in layout[0] for socket in node.outputs for link in socket.links)]
layout = min(ready, key=lambda item: (item[0][0] in peripheral_regions or any(node in shared_nodes for node in item[0]),
delivery_order(item), order.index(item[0][0])))
pending.remove(layout)
region, targets, local, width, height = layout
consumers = list(dict.fromkeys(link.to_node for node in region for socket in node.outputs
for link in socket.links if link.to_node not in region and link.to_node in positions))
destinations = consumers or [node for node in order if node in targets] or [paths[0][-1]]
owners = [node for node in order if node in targets] or [paths[0][-1]]
_place_field_region(local, width, height, destinations, owners, positions, sizes, occupied, limits, core, space,
peripheral=region[0] in peripheral_regions,
repeat_consumer=any(node in repeat_consumers for node in destinations))
enclose_repeat_branches(node_group, positions, sizes, space.bodies, gap=BRANCH_GAP)
clear_repeat_regions(node_group, positions, sizes, space.bodies, gap=BRANCH_GAP)
repeat_members = set().union(*(members for entry, _, members in node_zones(node_group)
if entry.bl_idname == "GeometryNodeRepeatInput"))
peripheral_nodes.difference_update(repeat_members)
place_shared_sources(space.bodies, peripheral_nodes, positions, sizes, gap=BRANCH_GAP)
_, space.routes = space.connections([[link] for link, _ in space.routes], positions, sizes)
_route_field_links(node_group, field_layouts, positions)
_route_branch_exits(node_group, space.routes)
_route_peer_bypasses(node_group, sizes, positions, ancestors)
for node, position in positions.items():
node.location = position
route_references(node_group, space.bodies, peripheral_nodes=peripheral_nodes)
while unused := [node for node in node_group.nodes
if node.bl_idname == "NodeReroute" and not node.outputs[0].is_linked]:
for members, _ in space.bodies:
members.difference_update(unused)
for node in unused:
node_group.nodes.remove(node)
route_field_passages(node_group, space.bodies)
create_field_frames(node_group, space.bodies, core)
_presentation(node_group)