"""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))