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

@@ -33,6 +33,22 @@ def field_layout(nodes, parents, peers, sizes, *, gap, target_height):
continue
for node in members[1:]:
roots[root(node)] = root(members[0])
# Stack exclusive calculation chains without disturbing ordered operands.
operands = {node for sources in peers if len(sources) > 1 for node in sources}
chain_heights = {node: sizes[node].height for node in nodes}
simple = {"ShaderNodeMath", "ShaderNodeVectorMath", "ShaderNodeClamp"}
for node in nodes:
sources = [parent for parent in parents[node] if parent in roots]
if node.bl_idname not in simple or node in operands or len(sources) != 1:
continue
source = sources[0]
if source in operands or {link.to_node for socket in source.outputs for link in socket.links} != {node}:
continue
first, last = root(source), root(node)
height = chain_heights[first] + gap + chain_heights[last]
if first != last and height <= target_height:
roots[last] = first
chain_heights[first] = height
groups = defaultdict(list)
for node in nodes:
groups[root(node)].append(node)
@@ -80,11 +96,12 @@ def field_layout(nodes, parents, peers, sizes, *, gap, target_height):
layers = columns
depth = column - 1
links = [link for node in nodes for socket in node.inputs for link in socket.links if link.from_node in block_of]
edges, passages = [], []
edges, passages, vertical = [], [], []
for link in links:
first, last = block_of[link.from_node], block_of[link.to_node]
if first == last:
raise ValueError("Equal operands must not contain an internal dependency")
vertical.append(link)
continue
start_offset = offsets[link.from_node] + sizes[link.from_node].sockets[link.from_socket]
end_offset = offsets[link.to_node] + sizes[link.to_node].sockets[link.to_socket]
previous, previous_offset, path = first, start_offset, []
@@ -173,18 +190,18 @@ def field_layout(nodes, parents, peers, sizes, *, gap, target_height):
raise ValueError(f"Expression layers could not be placed: {result.message}")
centers = result.x[:count]
positions, point_positions, right = {}, {}, 0
vertical_blocks = {block_of[link.from_node] for link in vertical}
for rank in range(depth + 1):
layer = layers[rank]
width = max(max(sizes[node].width for node in blocks[item]) if item < len(blocks) else 16 for item in layer)
padding = gap / 2 if any(item in vertical_blocks for item in layer) else 0
if rank:
run = max([gap, *(abs(centers[first] + a - centers[last] - b) * 0.8
for first, last, a, b in edges if ranks[last] == rank)])
right += run
right += width
right += gap
right += width + padding * 2
for item in layer:
if item < len(blocks):
for node in blocks[item]:
positions[node] = (right - sizes[node].width, -centers[item] - offsets[node])
positions[node] = (right - padding - sizes[node].width, -centers[item] - offsets[node])
else:
point_positions[item] = (right, -centers[item])
routes = []
@@ -193,4 +210,12 @@ def field_layout(nodes, parents, peers, sizes, *, gap, target_height):
points = [point for index, point in enumerate(points) if index == 0 or index == len(points)-1
or not abs(points[index-1][1] - point[1]) < 1e-6 or not abs(points[index+1][1] - point[1]) < 1e-6]
routes.append((link, points))
for link in vertical:
source, target = link.from_node, link.to_node
x1, y1 = positions[source]
x2, y2 = positions[target]
middle = (y1 - sizes[source].height + y2) / 2
routes.append((link, [(x1 + sizes[source].width + gap / 2, y1 - sizes[source].sockets[link.from_socket]),
(x1 + sizes[source].width + gap / 2, middle),
(x2 - gap / 2, middle), (x2 - gap / 2, y2 - sizes[target].sockets[link.to_socket])]))
return FieldLayout(positions, right, result.x[count], routes)