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:
2026-09-12 12:19:07 +08:00
parent dd32ccdc44
commit 5b2dc59f79
57 changed files with 2253 additions and 700 deletions

View File

@@ -0,0 +1,58 @@
"""Frame complex field calculations by their consuming inputs."""
from .flow import origin_socket
FRAME_PADDING = 32
FRAME_HEADING = 48
FRAME_MIN_NODES = 12
def remove_field_frames(node_group):
"""Detach generated containers before calculating absolute positions again."""
frames = {node for node in node_group.nodes if node.get("arrangement_frame")}
for node in node_group.nodes:
if node.parent in frames:
position = tuple(node.location_absolute)
node.parent = None
node.location = position
for frame in frames:
node_group.nodes.remove(frame)
def create_field_frames(node_group, bodies, core):
"""Keep a complex calculation and its internal wire passages in one frame."""
for members, (left, right, top, bottom) in bodies:
if sum(node.bl_idname not in {"NodeGroupInput", "NodeReroute"} for node in members) < FRAME_MIN_NODES:
continue
members = set(members)
outlets = [link for node in members for socket in node.outputs for link in socket.links
if link.to_node not in members]
targets = []
pending = list(outlets)
visited = set()
while pending:
link = pending.pop()
if link.to_node in core:
targets.append(link)
elif link.to_node not in visited:
visited.add(link.to_node)
pending.extend(edge for socket in link.to_node.outputs for edge in socket.links)
target = min(targets, key=lambda link: (link.to_node.location_absolute.x,
link.to_node.name, link.to_socket.name), default=None)
pending = {node for node in node_group.nodes if node.bl_idname == "NodeReroute"
and node.get("arrangement_branch") and origin_socket(node.outputs[0]).node in members}
while ready := {node for node in pending if all(link.to_node in members for link in node.outputs[0].links)}:
members.update(ready)
pending.difference_update(ready)
frame = node_group.nodes.new("NodeFrame")
frame["arrangement_frame"] = True
frame.name = f"{target.to_node.name} · {target.to_socket.name}" if target else "Field Calculation"
frame.label = frame.name
frame.shrink = False
frame.location = (left - FRAME_PADDING, top + FRAME_HEADING)
frame.width = right - left + FRAME_PADDING * 2
frame.height = top - bottom + FRAME_HEADING + FRAME_PADDING
frame.use_custom_color = True
frame.color = (0.12, 0.16, 0.18)
for node in members:
node.parent = frame