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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user