- 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.
276 lines
14 KiB
Python
276 lines
14 KiB
Python
"""Find curved passages around unrelated calculation frames."""
|
|
|
|
from heapq import heappop, heappush
|
|
from math import hypot
|
|
|
|
from ..common.nodes import replace_link_source
|
|
from .connections import curve_footprint, link_endpoints
|
|
from .dimensions import estimate_node_dimensions
|
|
from .flow import origin_socket, peer_links
|
|
from .frames import FRAME_HEADING, FRAME_MIN_NODES, FRAME_PADDING
|
|
from .zones import node_zones, repeat_regions
|
|
|
|
PASSAGE_GAP = 96
|
|
PASSAGE_SLOPE = 0.2
|
|
|
|
|
|
def _intersects(start, end, boxes):
|
|
bend = max(40, abs(end[0] - start[0]) * 0.42)
|
|
left, right = min(start[0], end[0] - bend), max(start[0] + bend, end[0])
|
|
bottom, top = sorted((start[1], end[1]))
|
|
boxes = [box for box in boxes if box[0] < right and box[1] > left and box[3] < top and box[2] > bottom]
|
|
if not boxes:
|
|
return False
|
|
return any(a < right and b > left and bottom < top and upper > lower
|
|
for left, right, top, lower in curve_footprint(start, end)
|
|
for a, b, upper, bottom in boxes)
|
|
|
|
|
|
def _passage(start, end, boxes, tracks, *, monotonic=False, forward_start=False, bounds=None):
|
|
"""Find a short sequence of unobstructed, slanted Bezier segments."""
|
|
points = [start, end]
|
|
for gap in (PASSAGE_GAP, PASSAGE_GAP * 2, PASSAGE_GAP * 4):
|
|
points.extend((x, y) for left, right, top, bottom in boxes
|
|
for x in (left - gap, right + gap) for y in (top + gap, bottom - gap))
|
|
points.extend((x, y) for left, right, top, bottom in boxes
|
|
for x in (left - (top - bottom) - PASSAGE_GAP, right + (top - bottom) + PASSAGE_GAP)
|
|
for y in (top + PASSAGE_GAP, bottom - PASSAGE_GAP))
|
|
points = list(dict.fromkeys(points))
|
|
if bounds is not None:
|
|
points = points[:2] + [point for point in points[2:] if bounds[0] <= point[0] <= bounds[1]]
|
|
direction = 1 if end[0] >= start[0] else -1
|
|
result = _search_passage(points, boxes, tracks, direction, forward_start)
|
|
if result is not None or monotonic:
|
|
return result
|
|
result = _search_passage(points, boxes, tracks, 0, forward_start)
|
|
if result is not None:
|
|
return result
|
|
raise ValueError("No unobstructed passage between calculation regions")
|
|
|
|
|
|
def _search_passage(points, boxes, tracks, direction, forward_start):
|
|
"""Prefer a continuous horizontal direction whenever obstacles permit it."""
|
|
costs, previous, pending = {0: 0}, {}, [(0, 0)]
|
|
while pending:
|
|
cost, index = heappop(pending)
|
|
if cost != costs[index]:
|
|
continue
|
|
if index == 1:
|
|
path = []
|
|
while index:
|
|
path.append(points[index])
|
|
index = previous[index]
|
|
return list(reversed(path))
|
|
x, y = points[index]
|
|
for target, (u, v) in enumerate(points):
|
|
if target == index or abs(u - x) < max(40, abs(v - y) * PASSAGE_SLOPE):
|
|
continue
|
|
if direction and (u - x) * direction <= 0:
|
|
continue
|
|
if forward_start and index == 0 and u <= x:
|
|
continue
|
|
if abs(v - y) < 0.01:
|
|
if u - x < PASSAGE_GAP:
|
|
continue
|
|
if any(a < u and b > x and abs(y - level) < PASSAGE_GAP
|
|
for a, b, level in tracks):
|
|
continue
|
|
distance = cost + hypot(u - x, v - y) + max(0, x - u) + PASSAGE_GAP
|
|
if distance >= costs.get(target, float("inf")) or _intersects((x, y), (u, v), boxes):
|
|
continue
|
|
costs[target], previous[target] = distance, index
|
|
heappush(pending, (distance, target))
|
|
return None
|
|
|
|
|
|
def route_field_passages(node_group, bodies):
|
|
"""Allow wires into their own calculations and route transit wires around frames."""
|
|
frames = [(members, (left - FRAME_PADDING, right + FRAME_PADDING,
|
|
top + FRAME_HEADING, bottom - FRAME_PADDING))
|
|
for members, (left, right, top, bottom) in bodies
|
|
if sum(node.bl_idname not in {"NodeGroupInput", "NodeReroute"} for node in members) >= FRAME_MIN_NODES]
|
|
positions = {node: tuple(node.location_absolute) for node in node_group.nodes}
|
|
sizes = {node: estimate_node_dimensions(node) for node in node_group.nodes}
|
|
for entry, end, members in node_zones(node_group):
|
|
if entry.bl_idname != "GeometryNodeRepeatInput":
|
|
continue
|
|
for node in members:
|
|
if node.bl_idname == "NodeReroute":
|
|
x, y = positions[node]
|
|
x = max(positions[entry][0] + sizes[entry].width + 40,
|
|
min(x, positions[end][0] - 40 - sizes[node].width))
|
|
node.location = positions[node] = (x, y)
|
|
repeats = repeat_regions(node_group, positions, sizes)
|
|
frames.extend(repeats)
|
|
for node in node_group.nodes:
|
|
if node.bl_idname != "NodeReroute":
|
|
continue
|
|
x, y = positions[node]
|
|
if not any(node not in members and left < x + 8 < right and bottom < y - 8 < top
|
|
for members, (left, right, top, bottom) in repeats):
|
|
continue
|
|
blocked = [box for members, box in frames if node not in members and box[0] < x + 8 < box[1]]
|
|
levels = {edge for _, _, top, bottom in blocked
|
|
for edge in (top + PASSAGE_GAP, bottom - PASSAGE_GAP)}
|
|
level = min((level for level in levels if all(not bottom < level < top
|
|
for _, _, top, bottom in blocked)), key=lambda level: abs(level - y))
|
|
node.location = positions[node] = (x, level + 8)
|
|
for link in list(node_group.links):
|
|
if link.from_socket.type != "GEOMETRY" or link.from_node.bl_idname == "NodeReroute":
|
|
continue
|
|
start, end = link_endpoints(link, positions, sizes)
|
|
distance = start[0] - end[0]
|
|
if any(link.from_node in members and link.to_node in members for members, _ in repeats):
|
|
continue
|
|
if distance <= 2000:
|
|
continue
|
|
# Bound the right-facing handle before a long return towards the left.
|
|
run = distance * 0.08 + PASSAGE_GAP
|
|
offset = max(PASSAGE_GAP * 2, run * PASSAGE_SLOPE)
|
|
point = (start[0] - run, start[1] + (offset if end[1] > start[1] else -offset))
|
|
levels = {start[1] - offset, start[1] + offset,
|
|
*(edge for _, (_, _, top, bottom) in frames
|
|
for edge in (top + PASSAGE_GAP, bottom - PASSAGE_GAP))}
|
|
clear_levels = [level for level in levels
|
|
if abs(level - start[1]) >= PASSAGE_GAP and abs(level - end[1]) >= PASSAGE_GAP
|
|
and all(not (left < point[0] < right and bottom < level < top)
|
|
for _, (left, right, top, bottom) in frames)]
|
|
point = (point[0], min(clear_levels, key=lambda level: (abs(level - point[1]), -level)))
|
|
corner = node_group.nodes.new("NodeReroute")
|
|
corner["arrangement_reference"] = True
|
|
corner["arrangement_return"] = True
|
|
corner.location = (point[0] - 8, point[1] + 8)
|
|
positions[corner] = tuple(corner.location_absolute)
|
|
sizes[corner] = estimate_node_dimensions(corner)
|
|
node_group.links.new(link.from_socket, corner.inputs[0])
|
|
replace_link_source(node_group, link, corner.outputs[0])
|
|
tracks = []
|
|
for link in node_group.links:
|
|
start, end = link_endpoints(link, positions, sizes)
|
|
if abs(start[1] - end[1]) < 0.01:
|
|
tracks.append((min(start[0], end[0]), max(start[0], end[0]), start[1]))
|
|
for link in list(node_group.links):
|
|
consumers, pending = set(), [link.to_node]
|
|
while pending:
|
|
node = pending.pop()
|
|
if node in consumers:
|
|
continue
|
|
consumers.add(node)
|
|
if node.bl_idname == "NodeReroute":
|
|
pending.extend(edge.to_node for edge in node.outputs[0].links)
|
|
source = origin_socket(link.from_socket).node
|
|
bounds = next(((box[0] + 60, box[1] - 60) for members, box in repeats
|
|
if source in members and consumers & members), None)
|
|
obstacles = [box for members, box in frames if source not in members and not consumers & members]
|
|
if (link.from_socket.type != "GEOMETRY" and not link.from_node.get("arrangement_reference")
|
|
and not link.to_node.get("arrangement_reference")):
|
|
obstacles = [box for members, box in repeats if source not in members and not consumers & members]
|
|
start, end = link_endpoints(link, positions, sizes)
|
|
if not _intersects(start, end, obstacles):
|
|
continue
|
|
try:
|
|
path = _passage(start, end, obstacles, tracks,
|
|
bounds=bounds,
|
|
forward_start=link.from_socket.type != "GEOMETRY"
|
|
and link.from_node.bl_idname != "NodeReroute")
|
|
except ValueError as error:
|
|
raise ValueError(f"Cannot route {source.name} to {link.to_node.name}: {start} -> {end}") from error
|
|
output, previous = link.from_socket, start
|
|
for point in path[:-1]:
|
|
corner = node_group.nodes.new("NodeReroute")
|
|
corner["arrangement_reference"] = True
|
|
corner.location = (point[0] - 8, point[1] + 8)
|
|
node_group.links.new(output, corner.inputs[0])
|
|
output = corner.outputs[0]
|
|
if abs(previous[1] - point[1]) < 0.01:
|
|
tracks.append((previous[0], point[0], point[1]))
|
|
previous = point
|
|
replace_link_source(node_group, link, output)
|
|
_simplify_passages(node_group, frames, bodies)
|
|
|
|
|
|
def _simplify_passages(node_group, frames, bodies):
|
|
"""Shortcut redundant turns without removing shared junctions or crossing frames."""
|
|
positions = {node: tuple(node.location_absolute) for node in node_group.nodes}
|
|
sizes = {node: estimate_node_dimensions(node) for node in node_group.nodes}
|
|
internal = set().union(*(members for _, _, members in node_zones(node_group)))
|
|
while True:
|
|
removed = False
|
|
for corner in list(node_group.nodes):
|
|
if corner.get("arrangement_return") or corner in internal:
|
|
continue
|
|
if not corner.get("arrangement_reference") or len(corner.inputs[0].links) != 1:
|
|
continue
|
|
if len(corner.outputs[0].links) != 1:
|
|
continue
|
|
incoming = corner.inputs[0].links[0]
|
|
start, _ = link_endpoints(incoming, positions, sizes)
|
|
points = [start]
|
|
chain, current = [], corner
|
|
while current.get("arrangement_reference") and len(current.outputs[0].links) == 1:
|
|
chain.append(current)
|
|
outgoing = current.outputs[0].links[0]
|
|
if outgoing in peer_links(outgoing.to_node):
|
|
break
|
|
middle, end = link_endpoints(outgoing, positions, sizes)
|
|
points.append(middle)
|
|
# A shortcut must remove an actual reversal, not a useful pipe approach.
|
|
forward_entry = (incoming.from_socket.type != "GEOMETRY"
|
|
and incoming.from_node.bl_idname != "NodeReroute")
|
|
if (not forward_entry or end[0] > start[0]) and any(
|
|
(b[0] - a[0]) * (end[0] - start[0]) < 0
|
|
for a, b in zip(points, points[1:] + [end])):
|
|
dx, dy = end[0] - start[0], end[1] - start[1]
|
|
consumers, pending = set(), [outgoing.to_node]
|
|
while pending:
|
|
node = pending.pop()
|
|
if node in consumers:
|
|
continue
|
|
consumers.add(node)
|
|
if node.bl_idname == "NodeReroute":
|
|
pending.extend(edge.to_node for edge in node.outputs[0].links)
|
|
source = origin_socket(incoming.from_socket).node
|
|
obstacles = [box for members, box in frames
|
|
if source not in members and not consumers & members]
|
|
horizontal = abs(dy) < 0.01
|
|
blocked = abs(dx) < max(40, abs(dy) * PASSAGE_SLOPE) or (horizontal and dx < PASSAGE_GAP)
|
|
if horizontal and not blocked:
|
|
for edge in node_group.links:
|
|
if edge.from_node in chain or edge.to_node in chain:
|
|
continue
|
|
a, b = link_endpoints(edge, positions, sizes)
|
|
if abs(a[1] - b[1]) < 0.01 and abs(a[1] - start[1]) < PASSAGE_GAP:
|
|
blocked |= min(a[0], b[0]) < end[0] and max(a[0], b[0]) > start[0]
|
|
path = [end] if not blocked and not _intersects(start, end, obstacles) else None
|
|
if path is None and abs(dx) >= max(40, abs(dy) * PASSAGE_SLOPE):
|
|
tracks = []
|
|
for edge in node_group.links:
|
|
if edge.from_node in chain or edge.to_node in chain:
|
|
continue
|
|
a, b = link_endpoints(edge, positions, sizes)
|
|
if abs(a[1] - b[1]) < 0.01:
|
|
tracks.append((min(a[0], b[0]), max(a[0], b[0]), a[1]))
|
|
path = _passage(start, end, obstacles, tracks, monotonic=True)
|
|
if path is not None:
|
|
output = incoming.from_socket
|
|
for point in path[:-1]:
|
|
node = node_group.nodes.new("NodeReroute")
|
|
node["arrangement_reference"] = True
|
|
node.location = (point[0] - 8, point[1] + 8)
|
|
positions[node] = tuple(node.location_absolute)
|
|
sizes[node] = estimate_node_dimensions(node)
|
|
node_group.links.new(output, node.inputs[0])
|
|
output = node.outputs[0]
|
|
replace_link_source(node_group, outgoing, output)
|
|
for members, _ in bodies:
|
|
members.difference_update(chain)
|
|
for node in reversed(chain):
|
|
node_group.nodes.remove(node)
|
|
removed = True
|
|
break
|
|
current = outgoing.to_node
|
|
if removed:
|
|
break
|
|
if not removed:
|
|
return
|