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

186 lines
11 KiB
Python

"""Route shared results on peripheral tracks with local consumer branches."""
from collections import defaultdict
from ..common.nodes import replace_link_source
from .connections import curve_footprint, socket_position, link_endpoints
from .dimensions import estimate_node_dimensions
from .flow import geometry_paths, origin_socket, peer_links
from .frames import FRAME_PADDING, FRAME_HEADING, FRAME_MIN_NODES
from .zones import node_zones
TRACK_GAP = 96
PORT_RUN = 40
FRAME_CLEARANCE = 160
def route_references(node_group, bodies, *, peripheral_nodes=frozenset()):
"""Keep execution links direct and distribute shared results outside field regions."""
nodes = list(node_group.nodes)
repeats = [members for entry, _, members in node_zones(node_group)
if entry.bl_idname == "GeometryNodeRepeatInput"]
sizes = {node: estimate_node_dimensions(node) for node in nodes}
positions = {node: tuple(node.location_absolute) for node in nodes}
region_of = {node: index for index, (members, _) in enumerate(bodies) for node in members}
producer_regions = {index for index, (members, _) in enumerate(bodies) if members & peripheral_nodes}
boxes = [(node, (positions[node][0], positions[node][0] + sizes[node].width,
positions[node][1], positions[node][1] - sizes[node].height))
for node in nodes if node.bl_idname not in {"NodeReroute", "NodeFrame"}]
padding = FRAME_CLEARANCE - TRACK_GAP
regions = [(left - FRAME_PADDING - padding, right + FRAME_PADDING + padding,
top + FRAME_HEADING + padding, bottom - FRAME_PADDING - padding)
for _, (left, right, top, bottom) in bodies]
frames = {index for index, (members, _) in enumerate(bodies)
if sum(node.bl_idname not in {"NodeGroupInput", "NodeReroute"} for node in members) >= FRAME_MIN_NODES}
_, paths = geometry_paths(node_group)
execution = {(first, last) for path in paths for first, last in zip(path, path[1:])}
references = defaultdict(list)
for node in nodes:
if node.bl_idname == "NodeReroute" and not node.get("arrangement_input"):
continue
for socket in node.inputs:
for link in list(socket.links):
if link.from_node.get("arrangement_input"):
continue
source = origin_socket(link.from_socket)
if any(source.node in members and node in members for members in repeats):
continue
if source.node.bl_idname == "NodeGroupInput":
continue
if (source.node, node) in execution:
continue
if source.node in region_of and region_of[source.node] == region_of.get(node):
continue
start = socket_position(source, positions, sizes)
end = link_endpoints(link, positions, sizes)[1]
if link in peer_links(node):
end = (end[0], start[1])
distance = end[0] - start[0]
if socket.type != "GEOMETRY":
references[source, False].append((link, end))
continue
if 0 <= distance <= 480:
continue
if distance > 0 and (len(source.links) == 1 or distance <= 800):
footprint = curve_footprint(start, end)
endpoint_regions = {region_of[member] for member in (source.node, node) if member in region_of}
obstacles = [box for member, box in boxes if member not in {source.node, node}]
obstacles += [(left + padding, right - padding, top - padding, bottom + padding)
for index, (left, right, top, bottom) in enumerate(regions)
if index in frames and index not in endpoint_regions]
if all(not any(a < right and b > left and bottom < top and upper > lower
for left, right, top, lower in footprint)
for a, b, upper, bottom in obstacles):
if link.from_socket != source:
replace_link_source(node_group, link, source)
continue
references[source, distance < 0].append((link, end))
references = {key: deliveries for key, deliveries in references.items()
if key[0].type == "GEOMETRY" or len({link.to_node for link, _ in deliveries}) > 1}
tracks = []
replaced = {link for deliveries in references.values() for link, _ in deliveries}
wire_corridors = []
for link in node_group.links:
start, end = link_endpoints(link, positions, sizes)
if link.from_socket.type == "GEOMETRY" and link not in replaced:
wire_corridors.extend(curve_footprint(start, end))
if link.from_socket.type == "GEOMETRY" and abs(start[1] - end[1]) < 1 and abs(start[0] - end[0]) > PORT_RUN:
tracks.append((min(start[0], end[0]), max(start[0], end[0]), start[1]))
def route(source, point):
node = node_group.nodes.new("NodeReroute")
node["arrangement_reference"] = True
node.location = (point[0] - 8, point[1] + 8)
node_group.links.new(source, node.inputs[0])
return node.outputs[0]
ordered = sorted(references, key=lambda item: (item[0].type != "GEOMETRY", item[0].node.name, item[0].identifier, item[1]))
for source, backward in ordered:
deliveries = references[source, backward]
start = socket_position(source, positions, sizes)
peripheral = len(deliveries) > 1
forward_pipe = peripheral and (source.type != "GEOMETRY" or source.node in peripheral_nodes)
level_entry = forward_pipe and source.type != "GEOMETRY"
source_region = region_of.get(source.node)
source_members = bodies[source_region][0] if source_region is not None else {source.node}
def path(level):
arrivals = [(end[0] - max(PORT_RUN, abs(level - end[1]) * 0.8), link, end)
for link, end in deliveries]
launch = min(x for x, _, _ in arrivals)
run = max(PORT_RUN, abs(level - start[1]) * 0.8)
launch = start[0] + run if forward_pipe or launch >= start[0] + run else min(launch, start[0] - run)
return launch, arrivals
endpoints = {source.node, *(link.to_node for link, _ in deliveries)}
obstacles = [box for node, box in boxes if node not in endpoints]
obstacles += [box for index, box in enumerate(regions) if not forward_pipe or index != source_region]
boundaries = [box for _, box in boxes] + regions if peripheral else obstacles
candidates = {edge for y in [start[1], *(end[1] for _, end in deliveries)]
for edge in (y - TRACK_GAP, y + TRACK_GAP)}
if level_entry:
candidates.add(start[1])
candidates.update({
*(edge for _, _, top, bottom in boundaries for edge in (top + TRACK_GAP, bottom - TRACK_GAP)),
*(edge for _, _, y in tracks for edge in (y + TRACK_GAP, y - TRACK_GAP))})
def clear(level):
launch, arrivals = path(level)
if forward_pipe and launch > min(x for x, _, _ in arrivals):
return False
if any(abs(level - y) < TRACK_GAP for y in [*([start[1]] if not level_entry else []), *(end[1] for _, end in deliveries)]):
return False
left = min(launch, *(x for x, _, _ in arrivals))
right = max(launch, *(x for x, _, _ in arrivals))
if peripheral:
scope_left = min(left, start[0], *(end[0] for _, end in deliveries))
scope_right = max(right, start[0], *(end[0] for _, end in deliveries))
scope = [box for box in [*(box for node, box in boxes if node not in source_members and node not in peripheral_nodes),
*(box for index, box in enumerate(regions) if index != source_region and index not in producer_regions)]
if box[0] < scope_right and box[1] > scope_left]
if scope and min(box[3] for box in scope) - TRACK_GAP < level < max(box[2] for box in scope) + TRACK_GAP:
return False
return (all(not bottom - TRACK_GAP < level < top + TRACK_GAP
for a, b, top, bottom in obstacles if a < right and b > left)
and all(abs(level - previous) >= TRACK_GAP - 0.01
for a, b, previous in tracks if a < right and b > left))
available = [y for y in candidates if clear(y)]
if not available:
raise ValueError(f"No forward reference channel for {source.node.name}")
def cost(level):
length = abs(level - start[1]) + sum(abs(level - end[1]) for _, end in deliveries)
launch, arrivals = path(level)
length += max(0, start[0] - launch) * 2
ramps = [(start, (launch, level))]
ramps += [((x, level), end) for x, _, end in arrivals]
footprints = [curve_footprint(a, b) for a, b in ramps]
crossings = sum(any(a < right and b > left and bottom < upper and top > lower
for left, right, upper, lower in footprint)
for node, (a, b, top, bottom) in boxes if node not in endpoints
for footprint in footprints)
overlaps = sum(min(right, b) - max(left, a) > TRACK_GAP
and max(lower, bottom) - min(upper, top) < TRACK_GAP
for footprint in footprints for left, right, upper, lower in footprint
if upper - lower < (right - left) * 0.25
for a, b, top, bottom in wire_corridors
if top - bottom < (b - a) * 0.25)
return overlaps * 4000 + crossings * 400 + length, abs(level - start[1]), -level
level = min(available, key=cost)
launch, arrivals = path(level)
root = route(source, (launch, level))
trunk, previous_x = root, launch
for x, link, end in sorted(arrivals, key=lambda item: item[0]):
if x - previous_x >= PORT_RUN * 2:
trunk = route(trunk, (x, level))
previous_x = x
actual_end = link_endpoints(link, positions, sizes)[1]
branch = route(trunk, (end[0] - PORT_RUN, end[1])) if end != actual_end else trunk
replace_link_source(node_group, link, branch)
tracks.append((min(launch, *(x for x, _, _ in arrivals)),
max(launch, *(x for x, _, _ in arrivals)), level))
wire_corridors.extend(curve_footprint(start, (launch, level)))
for x, _, end in arrivals:
wire_corridors.extend(curve_footprint((x, level), end))