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.
This commit is contained in:
2026-09-12 12:19:07 +08:00
parent dd32ccdc44
commit 5b2dc59f79
57 changed files with 2253 additions and 700 deletions

View File

@@ -8,10 +8,16 @@ import numpy as np
from scipy.optimize import LinearConstraint, minimize
from .dimensions import estimate_node_dimensions, visible_socket
from .zones import node_zones
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
@@ -23,15 +29,6 @@ ROUTE_GAP = 40
ROUTE_RUN = 120
def _replace_link(node_group, link, source):
"""Replace a source without changing a multi-input socket's branch 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 _remove_routes(node_group):
previous = [node for node in node_group.nodes if node.bl_idname == "NodeReroute"]
@@ -41,7 +38,7 @@ def _remove_routes(node_group):
if not any(link.from_node in previous for link in links):
continue
if not socket.is_multi_input:
_replace_link(node_group, links[0], origin_socket(links[0].from_socket))
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:
@@ -88,10 +85,10 @@ def _localize_peer_inputs(node_group, ancestors, children):
port = node_group.nodes.new("NodeReroute")
port["arrangement_input"] = True
node_group.links.new(link.from_socket, port.inputs[0])
_replace_link(node_group, link, port.outputs[0])
replace_link_source(node_group, link, port.outputs[0])
def _columns(nodes, parents, peers, sizes, gap, spacing=None):
def _columns(nodes, parents, peers, sizes, gap):
"""Allocate columns with equal right edges for independent peer inputs."""
roots = {node: node for node in nodes}
@@ -117,7 +114,6 @@ def _columns(nodes, parents, peers, sizes, gap, spacing=None):
columns[levels[root(node)]].append(node)
right, positions = 0, {}
for level in sorted(columns):
right += max(((spacing or {}).get(node, 0) for node in columns[level]), default=0)
right += max(sizes[node].width for node in columns[level])
for node in columns[level]:
positions[node] = right - sizes[node].width
@@ -126,13 +122,13 @@ def _columns(nodes, parents, peers, sizes, gap, spacing=None):
def _field_regions(nodes, core, children, order):
"""Group calculations by their first geometry consumers and shared scope."""
"""Group connected calculations by the geometry inputs that consume them."""
targets = {}
fields = set(nodes) - core
for node in reversed(order):
targets[node] = frozenset({node}) if node in core else frozenset().union(
*(targets[child] for child in children[node])
)
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]
@@ -140,7 +136,26 @@ def _field_regions(nodes, core, children, order):
for node in order:
if node in fields:
regions[targets[node]].append(node)
return [(region, owners) for owners, region in regions.items()]
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):
@@ -155,7 +170,7 @@ def _localize_module_outputs(node_group, core, children):
port = node_group.nodes.new("NodeReroute")
port["arrangement_exit"] = True
node_group.links.new(link.from_socket, port.inputs[0])
_replace_link(node_group, link, port.outputs[0])
replace_link_source(node_group, link, port.outputs[0])
def _localize_inputs(node_group):
@@ -169,7 +184,7 @@ def _localize_inputs(node_group):
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(node_group, link, source)
replace_link_source(node_group, link, source)
for node in controls:
node_group.nodes.remove(node)
@@ -384,7 +399,7 @@ class BranchSpace:
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, trunks, space, horizontal=None, *, allow_opposite=False):
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)
@@ -398,23 +413,13 @@ def _place_field_region(local, width, height, destinations, owners, positions, s
if allow_opposite:
side = 0
desired = right - width
front_allowed = not horizontal and all(link.from_node in local
front_allowed = all(link.from_node in local
for node in local for socket in node.inputs for link in socket.links)
shared = [] if horizontal else shared_geometry_outputs(local)
references = set(shared_geometry_outputs(local))
outlets, constraints, entries = [], [], []
outlets, constraints = [], []
for node, (x, y) in local.items():
if horizontal:
for socket in node.inputs:
for link in socket.links:
if link.from_node in positions:
source_x, source_y = positions[link.from_node]
entries.append((source_x + sizes[link.from_node].width - x,
source_y - sizes[link.from_node].sockets[link.from_socket],
y - sizes[node].sockets[socket]))
for socket in node.outputs:
links = list(socket.links)
if socket not in references:
if socket.type != "GEOMETRY" and not peripheral:
returns = [link for link in links if link.to_node in core]
if returns:
outlets.append(returns)
@@ -424,12 +429,15 @@ def _place_field_region(local, width, height, destinations, owners, positions, s
constraints.append((target_x - x - sizes[node].width,
y - sizes[node].sockets[socket],
target_y - sizes[link.to_node].sockets[link.to_socket]))
trunk_end = max(positions[node][0] for node in {*owners, *destinations})
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)}
if horizontal:
candidates = {left for left in candidates if horizontal[0] <= left and left + width <= horizontal[1]}
candidates.add(horizontal[0])
best, seen = None, set()
for left in sorted(candidates, key=lambda x: (abs(x - desired), x)):
tops = {ceiling + BRANCH_GAP + height, floor - BRANCH_GAP,
@@ -443,16 +451,11 @@ def _place_field_region(local, width, height, destinations, owners, positions, s
for top in sorted(tops, key=lambda y: abs(y - height / 2 - baseline)):
if top > limits[0] or top - height < limits[1]:
continue
minimum = max([horizontal[0], *(start + max(GEOMETRY_GAP, abs(top + target_y - source_y) * 0.8)
for start, source_y, target_y in entries)]) if horizontal else -float("inf")
placed_left = max(left, minimum) if horizontal else min(left, desired)
placed_left = min([placed_left, *(end - max(GEOMETRY_GAP, abs(target_y - top - source_y) * 0.8)
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)])
if not horizontal:
# Leave clearance at saturated link constraints for solver roundoff.
placed_left -= 0.01
if horizontal and not minimum <= placed_left <= horizontal[1] - width:
continue
# 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)):
@@ -462,26 +465,23 @@ def _place_field_region(local, width, height, destinations, owners, positions, s
continue
seen.add(point)
distance = abs(placed_left - desired)
base_score = distance + abs(top - height / 2 - baseline) * 1.5
if best is not None and base_score > best[0]:
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
channels = [(placed_left + local[socket.node][0] + sizes[socket.node].width,
top + local[socket.node][1] - sizes[socket.node].sockets[socket]) for socket in shared]
if any(a < trunk_end and b > start and bottom - ROUTE_GAP < level < upper + ROUTE_GAP
for start, level in channels for a, b, upper, bottom in occupied):
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 = (score, distance, -top, placed_left, top)
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
@@ -489,7 +489,8 @@ def _place_field_region(local, width, height, destinations, owners, positions, s
if not allow_opposite:
return _place_field_region(
local, width, height, destinations, owners, positions, sizes,
occupied, limits, core, trunks, space, horizontal, allow_opposite=True,
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
@@ -500,38 +501,29 @@ def _place_field_region(local, width, height, destinations, owners, positions, s
space.bodies.append((set(local), body))
space.exits.extend(best_exits)
space.routes.extend(best_routes)
for socket in shared:
start = positions[socket.node][0] + sizes[socket.node].width
level = positions[socket.node][1] - sizes[socket.node].sockets[socket]
trunks[socket] = level
occupied.append((start, trunk_end, level + ROUTE_GAP, level - ROUTE_GAP))
def _route_field_links(node_group, layouts, positions):
"""Materialize the passages reserved between expression columns."""
protected = set()
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
protected.add((source, link.to_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(node_group, link, source)
return protected
replace_link_source(node_group, link, source)
def _route_branch_exits(node_group, routes):
"""Materialize the channels reserved during placement, sharing trunk corners."""
deliveries, protected = defaultdict(list), set()
deliveries = defaultdict(list)
for link, points in routes:
protected.add((link.from_socket, link.to_socket))
deliveries[link.from_socket].append((link, points))
for source, branches in deliveries.items():
corners = {}
@@ -543,8 +535,7 @@ def _route_branch_exits(node_group, routes):
source = corners[point] = node.outputs[0]
for link, points in branches:
if points:
_replace_link(node_group, link, corners[points[-1]])
return protected
replace_link_source(node_group, link, corners[points[-1]])
def _route_peer_bypasses(node_group, sizes, positions, ancestors):
@@ -571,116 +562,14 @@ def _route_peer_bypasses(node_group, sizes, positions, ancestors):
entry = node_group.nodes.new("NodeReroute")
entry["arrangement_bypass"] = True
node_group.links.new(source, entry.inputs[0])
_replace_link(node_group, port.inputs[0].links[0], entry.outputs[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 _route_references(node_group, sizes, positions, regions, paths, trunks, protected):
"""Share forward trunks and branch next to each consumer for every socket type."""
references = defaultdict(list)
path_of = {node: index for index, path in enumerate(paths) for node in path}
peer_edges = {link for node in node_group.nodes if len(links := peer_links(node)) > 1 for link in links}
for link in list(node_group.links):
if (link.from_socket, link.to_socket) in protected or link.from_node.get("arrangement_branch") or link.to_node.get("arrangement_branch"):
continue
if link.from_node.get("arrangement_bypass") or link.to_node.get("arrangement_bypass"):
continue
if (link.from_node in path_of and link.to_node in path_of
and path_of[link.from_node] == path_of[link.to_node] and len(link.from_socket.links) == 1):
continue
if link in peer_edges:
continue
if positions[link.to_node][0] - positions[link.from_node][0] - sizes[link.from_node].width > ROUTE_RUN * 4:
references[link.from_socket].append(link)
boxes = {node: _node_box(node, positions, sizes) for node in positions}
tracks = []
def route(source, x, y):
node = node_group.nodes.new("NodeReroute")
node.location = (x - 8, y + 8)
node_group.links.new(source, node.inputs[0])
return node.outputs[0]
for source, links in references.items():
if len(links) < 2 and source.type != "GEOMETRY":
continue
left = positions[source.node][0] + sizes[source.node].width
right = max(positions[link.to_node][0] for link in links)
start_y = positions[source.node][1] - sizes[source.node].sockets[source]
destinations = {link.to_node for link in links}
obstacles = [(bottom - ROUTE_GAP, top + ROUTE_GAP) for node, (a, b, top, bottom) in boxes.items()
if node not in destinations and a < right and b > left]
for a, b, top, bottom in regions:
if a >= right or b <= left:
continue
endpoints = destinations | {source.node}
if any(a <= positions[node][0] <= b and bottom <= positions[node][1] <= top for node in endpoints):
continue
obstacles.append((bottom - ROUTE_GAP, top + ROUTE_GAP))
obstacles += [(y - ROUTE_GAP, y + ROUTE_GAP) for a, b, y in tracks if a < right and b > left]
if len(links) == 1:
target = links[0]
target_y = positions[target.to_node][1] - sizes[target.to_node].sockets[target.to_socket]
if abs(target_y - start_y) < ROUTE_GAP and all(not lower < start_y < upper for lower, upper in obstacles):
continue
first_target = min(positions[link.to_node][0] for link in links)
reach = (first_target - left) / 2.4
lower, upper = start_y - reach, start_y + reach
for link in links:
y = positions[link.to_node][1] - sizes[link.to_node].sockets[link.to_socket]
reach = (positions[link.to_node][0] - left) / 2.4
lower, upper = max(lower, y - reach), min(upper, y + reach)
candidates = [start_y, lower, upper, *(edge for interval in obstacles for edge in interval)]
available = [y for y in candidates if lower <= y <= upper and all(not a < y < b for a, b in obstacles)]
if source in trunks:
level = trunks[source]
elif not available:
continue
else:
level = min(available, key=lambda y: (abs(y - start_y) + sum(
abs(y - positions[link.to_node][1] + sizes[link.to_node].sockets[link.to_socket])
for link in links) / len(links), -y))
entry_run = min(max(ROUTE_RUN, abs(level - start_y) * 0.8), (first_target - left) / 3)
trunk = route(source, left + entry_run, level)
tracks.append((left + entry_run, right, level))
previous_x = left + entry_run
branches = []
for link in links:
target_x = positions[link.to_node][0]
target_y = positions[link.to_node][1] - sizes[link.to_node].sockets[link.to_socket]
exit_run = min(max(ROUTE_RUN, abs(target_y - level) * 0.8), (target_x - left) / 3)
branches.append((target_x - exit_run, link))
for x, link in sorted(branches, key=lambda item: item[0]):
if x != previous_x:
trunk = route(trunk, x, level)
previous_x = x
_replace_link(node_group, link, trunk)
def _zone_width(interior, layout, sizes):
"""Reserve the body and the actual socket runs at each end of a paired scope."""
body_height = max(sizes[node].height for node in interior)
spans = []
for top in (layout.height + BRANCH_GAP, -body_height - BRANCH_GAP):
left, right = FIELD_GAP, layout.width + FIELD_GAP
for node, (x, y) in layout.positions.items():
for socket in node.inputs:
for link in socket.links:
if link.from_node not in layout.positions and link.from_node not in interior:
delta = top + y - sizes[node].sockets[socket] + sizes[link.from_node].sockets[link.from_socket]
left = max(left, max(GEOMETRY_GAP, abs(delta) * 0.8) - x)
for socket in node.outputs:
for link in socket.links:
if link.to_node in interior:
delta = top + y - sizes[node].sockets[socket] + sizes[link.to_node].sockets[link.to_socket]
right = max(right, x + sizes[node].width + max(GEOMETRY_GAP, abs(delta) * 0.8))
spans.append(left + right)
return max(spans) + BRANCH_GAP * 2
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)
@@ -700,68 +589,64 @@ def arrange_nodes(node_group):
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)))
spacing, zones, field_layouts = {}, [], []
scoped = set()
for entry, end, members in reversed(node_zones(node_group)):
if entry not in core or end not in core:
continue
interior = (members & core) - {entry}
fields = [node for node in order if node in members - core - scoped]
if fields:
scoped.update(fields)
layout = field_layout(fields, parents, peers, sizes, gap=FIELD_GAP, target_height=FIELD_HEIGHT)
field_layouts.append(layout)
local, width, height = layout.positions, layout.width, layout.height
span = _zone_width(interior, layout, sizes)
for node in core:
if entry in geometry_parents[node]:
spacing[node] = max(spacing.get(node, 0), span)
zones.append((entry, end, interior, fields, local, width, height))
column_parents = {node: list(sources) for node, sources in geometry_parents.items()}
for node in core:
if node.get("arrangement_input"):
source = origin_socket(node.outputs[0]).node
if source in core:
column_parents[node] = [source]
geometry_x, _ = _columns([node for node in order if node in core], column_parents, peers, sizes, GEOMETRY_GAP, spacing)
# 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)
occupied = _geometry_corridors(paths, positions, sizes)
trunks = {}
space = BranchSpace()
layouts = []
layouts, field_layouts = [], []
for region, targets in regions:
remaining = [node for node in region if node not in scoped]
if remaining:
layout = field_layout(remaining, parents, peers, sizes, gap=FIELD_GAP, target_height=FIELD_HEIGHT)
field_layouts.append(layout)
layouts.append((remaining, targets, layout.positions, layout.width, layout.height))
reserve = max([FIELD_HEIGHT, *(height for _, _, _, _, height in layouts),
*(height for _, _, _, _, _, _, height in zones)]) * 2 + BRANCH_GAP * 2
reserve += sum(height + BRANCH_GAP * 2 for _, _, _, _, _, _, height in zones)
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)
for entry, end, interior, fields, local, width, height in zones:
destinations = [min((node for node in interior if node.bl_idname != "NodeReroute"),
key=lambda node: (positions[node][0], node.name))]
horizontal = (positions[entry][0] + sizes[entry].width + FIELD_GAP, positions[end][0] - FIELD_GAP)
_place_field_region(local, width, height, destinations, destinations, positions, sizes, occupied, limits, core, trunks, space, horizontal)
members = interior | {entry} | set(fields)
boxes = [_node_box(node, positions, sizes) for node in members]
occupied.append((min(box[0] for box in boxes), max(box[1] for box in boxes),
max(box[2] for box in boxes), min(box[3] for box in boxes)))
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: (any(node in shared_nodes for node in item[0]),
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
@@ -769,15 +654,28 @@ def arrange_nodes(node_group):
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, trunks, space)
protected = _route_field_links(node_group, field_layouts, positions)
protected.update(_route_branch_exits(node_group, space.routes))
_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, sizes, positions, occupied, paths, trunks, protected)
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)