- 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.
186 lines
11 KiB
Python
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))
|