- 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.
222 lines
10 KiB
Python
222 lines
10 KiB
Python
"""Lay out expression dependencies with ordered operands and reserved wire passages."""
|
|
|
|
from collections import defaultdict
|
|
from dataclasses import dataclass
|
|
from graphlib import TopologicalSorter
|
|
|
|
import numpy as np
|
|
from scipy.optimize import linprog
|
|
|
|
from .flow import peer_links
|
|
|
|
|
|
@dataclass
|
|
class FieldLayout:
|
|
positions: dict
|
|
width: float
|
|
height: float
|
|
routes: list
|
|
|
|
|
|
def field_layout(nodes, parents, peers, sizes, *, gap, target_height):
|
|
"""Order dependency layers, reserve long-link passages, and solve socket heights."""
|
|
roots = {node: node for node in nodes}
|
|
|
|
def root(node):
|
|
while roots[node] != node:
|
|
node = roots[node]
|
|
return node
|
|
|
|
for sources in peers:
|
|
members = [node for node in sources if node in roots]
|
|
if sum(sizes[node].height for node in members) + gap * (len(members) - 1) > 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)
|
|
for sources in peers:
|
|
members = [node for node in sources if node in roots]
|
|
if members and all(root(node) == root(members[0]) for node in members):
|
|
groups[root(members[0])].sort(key=lambda node: sources.index(node) if node in sources else len(sources))
|
|
blocks = list(groups.values())
|
|
block_of = {node: index for index, block in enumerate(blocks) for node in block}
|
|
children = {index: set() for index in range(len(blocks))}
|
|
for node in nodes:
|
|
for parent in parents[node]:
|
|
if parent in block_of and block_of[parent] != block_of[node]:
|
|
children[block_of[parent]].add(block_of[node])
|
|
ranks = {}
|
|
for block in TopologicalSorter(children).static_order():
|
|
ranks[block] = max((ranks[child] + 1 for child in children[block]), default=0)
|
|
depth = max(ranks.values())
|
|
ranks = {block: depth - rank for block, rank in ranks.items()}
|
|
heights = [sum(sizes[node].height for node in block) + gap * (len(block) - 1) for block in blocks]
|
|
offsets = {}
|
|
for index, block in enumerate(blocks):
|
|
cursor = -heights[index] / 2
|
|
for node in block:
|
|
offsets[node] = cursor
|
|
cursor += sizes[node].height + gap
|
|
layers = defaultdict(list)
|
|
for block, rank in ranks.items():
|
|
layers[rank].append(block)
|
|
operand_columns = {rank for node in nodes for link in peer_links(node)
|
|
if link.from_node in block_of
|
|
for rank in range(ranks[block_of[link.from_node]], ranks[block_of[node]] + 1)}
|
|
# Keep an operand block and its junction together when spreading tall layers.
|
|
columns, column = defaultdict(list), 0
|
|
for rank in sorted(layers):
|
|
used = 0
|
|
for block in layers[rank]:
|
|
if rank not in operand_columns and used and used + gap + heights[block] > target_height:
|
|
column += 1
|
|
used = 0
|
|
columns[column].append(block)
|
|
ranks[block] = column
|
|
used += heights[block] + (gap if used else 0)
|
|
column += 1
|
|
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, vertical = [], [], []
|
|
for link in links:
|
|
first, last = block_of[link.from_node], block_of[link.to_node]
|
|
if first == last:
|
|
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, []
|
|
for rank in range(ranks[first] + 1, ranks[last]):
|
|
point = len(heights)
|
|
heights.append(16)
|
|
ranks[point] = rank
|
|
layers[rank].append(point)
|
|
edges.append((previous, point, previous_offset, 0))
|
|
previous, previous_offset = point, 0
|
|
path.append(point)
|
|
edges.append((previous, last, previous_offset, end_offset))
|
|
if path:
|
|
passages.append((link, path))
|
|
incoming, outgoing = defaultdict(list), defaultdict(list)
|
|
for first, last, a, b in edges:
|
|
outgoing[first].append((last, a, b))
|
|
incoming[last].append((first, b, a))
|
|
centers = {}
|
|
|
|
def stack(layer):
|
|
height = sum(heights[item] for item in layer) + sum(separation(a, b) for a, b in zip(layer, layer[1:]))
|
|
cursor = -height / 2
|
|
for index, item in enumerate(layer):
|
|
centers[item] = cursor + heights[item] / 2
|
|
cursor += heights[item] + (separation(item, layer[index + 1]) if index + 1 < len(layer) else 0)
|
|
|
|
def separation(first, last):
|
|
return gap if first < len(blocks) and last < len(blocks) else gap / 2
|
|
|
|
for layer in layers.values():
|
|
stack(layer)
|
|
for _ in range(4):
|
|
for ranks_in_order, adjacent in ((range(depth + 1), incoming), (range(depth, -1, -1), outgoing)):
|
|
for rank in ranks_in_order:
|
|
layer = layers[rank]
|
|
def desired(item):
|
|
neighbors = adjacent[item]
|
|
return sum(centers[other] + their_offset - own_offset
|
|
for other, own_offset, their_offset in neighbors) / len(neighbors) if neighbors else centers[item]
|
|
layer.sort(key=lambda item: (desired(item), item))
|
|
stack(layer)
|
|
|
|
count = len(heights)
|
|
variable_count = count + 1 + len(edges)
|
|
inequalities, bounds, equalities, values = [], [], [], []
|
|
|
|
def constraint(terms, maximum):
|
|
row = np.zeros(variable_count)
|
|
for index, coefficient in terms:
|
|
row[index] += coefficient
|
|
inequalities.append(row)
|
|
bounds.append(maximum)
|
|
|
|
for index, height in enumerate(heights):
|
|
constraint([(index, -1)], -height / 2)
|
|
constraint([(index, 1), (count, -1)], -height / 2)
|
|
for layer in layers.values():
|
|
for first, last in zip(layer, layer[1:]):
|
|
constraint([(first, 1), (last, -1)], -(heights[first] + heights[last]) / 2 - separation(first, last))
|
|
for index, (first, last, a, b) in enumerate(edges):
|
|
slack = count + 1 + index
|
|
constraint([(first, 1), (last, -1), (slack, -1)], b - a)
|
|
constraint([(last, 1), (first, -1), (slack, -1)], a - b)
|
|
for node in nodes:
|
|
inputs = [link.from_socket for link in peer_links(node)]
|
|
if len(inputs) < 2 or any(socket.node not in block_of for socket in inputs):
|
|
continue
|
|
output = next(socket for socket in node.outputs if socket in sizes[node].sockets)
|
|
first, last = inputs[0], inputs[-1]
|
|
row = np.zeros(variable_count)
|
|
row[block_of[node]] += 2
|
|
row[block_of[first.node]] -= 1
|
|
row[block_of[last.node]] -= 1
|
|
equalities.append(row)
|
|
values.append(offsets[first.node] + sizes[first.node].sockets[first]
|
|
+ offsets[last.node] + sizes[last.node].sockets[last]
|
|
- 2 * (offsets[node] + sizes[node].sockets[output]))
|
|
objective = np.zeros(variable_count)
|
|
objective[count] = len(edges) + 1
|
|
objective[count + 1:] = 1
|
|
result = linprog(objective, A_ub=np.array(inequalities), b_ub=bounds,
|
|
A_eq=np.array(equalities) if equalities else None, b_eq=values or None,
|
|
bounds=[(None, None)] * count + [(0, None)] * (1 + len(edges)), method="highs")
|
|
if not result.success:
|
|
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:
|
|
right += gap
|
|
right += width + padding * 2
|
|
for item in layer:
|
|
if item < len(blocks):
|
|
for node in blocks[item]:
|
|
positions[node] = (right - padding - sizes[node].width, -centers[item] - offsets[node])
|
|
else:
|
|
point_positions[item] = (right, -centers[item])
|
|
routes = []
|
|
for link, path in passages:
|
|
points = [point_positions[item] for item in path]
|
|
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)
|