Files
nan 198fcbf55b
Some checks failed
Test / test (push) Has been cancelled
Initial public release
2026-09-19 21:31:36 +08:00

408 lines
18 KiB
Python

"""Plan calculation bodies and shared wire passages in the same column layout."""
import math
from collections import defaultdict
from graphlib import TopologicalSorter
from statistics import median
def plan_layout(nodes, edges, size, blocks, core):
"""Return node positions and connection paths without modifying Blender data."""
ids = {node: i for i, node in enumerate(nodes)}
edge_ids = [(ids[e.from_node], ids[e.to_node]) for e in edges]
edge_types = [e.from_socket.type for e in edges]
block_of = {i: k for k, block in enumerate(blocks) for i in block["members"]}
def bounds(positions):
return (
min(x for x, y in positions.values()),
min(y for x, y in positions.values()),
max(x + size[i].width for i, (x, y) in positions.items()),
max(y + size[i].height for i, (x, y) in positions.items()),
)
def endpoint(edge, positions):
a, b = ids[edge.from_node], ids[edge.to_node]
return (
(positions[a][0] + size[a].width, positions[a][1] + size[a].sockets[edge.from_socket]),
(positions[b][0], positions[b][1] + size[b].sockets[edge.to_socket]),
)
source_keys = [(a, edges[e].from_socket.identifier) for e, (a, b) in enumerate(edge_ids)]
def simplify(path):
result = []
for point in path:
if result and math.dist(result[-1], point) < 1e-6:
continue
while (
len(result) > 1
and abs(result[-2][1] - result[-1][1]) < 1e-6
and abs(point[1] - result[-1][1]) < 1e-6
):
result.pop()
result.append(point)
return result
def leaf(i):
return dict(
members={i},
w=size[i].width,
h=size[i].height,
positions={i: (0.0, 0.0)},
routes={},
inputs={
e: (0.0, size[i].sockets[edges[e].to_socket]) for e, (a, b) in enumerate(edge_ids) if b == i
},
outputs={
e: (size[i].width, size[i].sockets[edges[e].from_socket])
for e, (a, b) in enumerate(edge_ids)
if a == i
},
)
def plan_scope(items, rank_map=None):
"""Reserve relay rows at every crossed column, including nested boundaries."""
items = {("item", k): v for k, v in enumerate(items)}
owner = {i: k for k, item in items.items() for i in item["members"]}
members = set(owner)
incoming = [e for e, (a, b) in enumerate(edge_ids) if b in members and a not in members]
outgoing = [e for e, (a, b) in enumerate(edge_ids) if a in members and b not in members]
if rank_map is not None:
incoming = []
outgoing = []
boundary = {}
for e in incoming:
boundary["in", e] = ("in", source_keys[e])
for e in outgoing:
boundary["out", e] = ("out", source_keys[e])
for key in set(boundary.values()):
items[key] = dict(members=set(), w=0.0, h=32.0, positions={}, routes={}, inputs={}, outputs={})
links = []
for e, (a, b) in enumerate(edge_ids):
if a not in members and b not in members:
continue
if rank_map is not None and (a not in members or b not in members):
continue
u = owner[a] if a in members else boundary["in", e]
v = owner[b] if b in members else boundary["out", e]
if u == v:
continue
sy = items[u]["outputs"][e][1] if a in members else 16.0
ty = items[v]["inputs"][e][1] if b in members else 16.0
weight = 3.0 if edge_types[e] in {"GEOMETRY", "SHADER"} else 1.0
links.append((e, u, v, sy, ty, weight))
parents = {k: set() for k in items}
children = {k: set() for k in items}
for e, u, v, sy, ty, weight in links:
parents[v].add(u)
children[u].add(v)
topo = list(TopologicalSorter(parents).static_order())
earliest = {}
for k in topo:
earliest[k] = max((earliest[p] + 1 for p in parents[k]), default=0)
depth = max(earliest.values(), default=0)
ranks = {}
for k in reversed(topo):
ranks[k] = min((ranks[c] - 1 for c in children[k]), default=depth)
for k in items:
if k[0] == "in":
ranks[k] = 0
if rank_map is not None:
ranks = {k: rank_map[min(item["members"])] for k, item in items.items()}
# Relay vertices are space reservations, not routes fitted to placed nodes.
chains = {}
arcs = {}
for e, u, v, sy, ty, weight in links:
chain = [u]
for rank in range(ranks[u] + 1, ranks[v]):
key = ("relay", source_keys[e], rank)
if key not in items:
items[key] = dict(
members=set(), w=0.0, h=36.0, positions={}, routes={}, inputs={}, outputs={}
)
ranks[key] = rank
chain.append(key)
chain.append(v)
chains[e] = chain
for j, (a, b) in enumerate(zip(chain, chain[1:])):
ay = sy if j == 0 else items[a]["h"] / 2
by = ty if j == len(chain) - 2 else items[b]["h"] / 2
arcs[a, b, ay, by] = max(arcs.get((a, b, ay, by), 0), weight)
layers = defaultdict(list)
for k in items:
layers[ranks[k]].append(k)
for rank in range(min(ranks.values()), max(ranks.values()) + 1):
layers[rank]
families = {}
for k, item in items.items():
if item["members"]:
i = min(item["members"])
families[k] = ("block", block_of[i]) if i in block_of else ("core", i)
else:
families[k] = k
for e, chain in chains.items():
a, b = edge_ids[e]
if a in block_of and block_of.get(a) == block_of.get(b):
for k in chain[1:-1]:
families[k] = ("block", block_of[a])
neighbors = defaultdict(list)
for (a, b, ay, by), weight in arcs.items():
if a[0] == "relay" and b[0] == "relay":
weight *= 30.0
neighbors[a].append((b, ay, by, weight))
neighbors[b].append((a, by, ay, weight))
gap = 64.0
def separation(a, b):
if families[a] != families[b] and (families[a][0] == "block" or families[b][0] == "block"):
return 180.0
return gap if items[a]["members"] and items[b]["members"] else 36.0
y = {}
for layer in layers.values():
cursor = 0.0
for k in layer:
y[k] = cursor
cursor += items[k]["h"] + gap
def pack(layer, desired, weights):
offsets = {}
cursor = 0.0
previous = None
for k in layer:
if previous is not None:
cursor += items[previous]["h"] + separation(previous, k)
offsets[k] = cursor
previous = k
pools = []
for k in layer:
pools.append(([k], weights[k], (desired[k] - offsets[k]) * weights[k]))
while len(pools) > 1 and pools[-2][2] / pools[-2][1] > pools[-1][2] / pools[-1][1]:
b = pools.pop()
a = pools.pop()
pools.append((a[0] + b[0], a[1] + b[1], a[2] + b[2]))
for keys, weight, total in pools:
for k in keys:
y[k] = total / weight + offsets[k]
for sweep in range(16):
for rank in sorted(layers, reverse=bool(sweep % 2)):
layer = layers[rank]
desired = {}
weights = {}
for k in layer:
adjacent = [
(y[b] + by - ay, weight)
for b, ay, by, weight in neighbors[k]
if (ranks[b] < rank) != bool(sweep % 2)
]
if not adjacent:
adjacent = [(y[b] + by - ay, weight) for b, ay, by, weight in neighbors[k]]
weights[k] = sum(w for value, w in adjacent) or 1.0
desired[k] = sum(value * w for value, w in adjacent) / weights[k] if adjacent else y[k]
family_centers = defaultdict(list)
for k in layer:
family_centers[families[k]].append(desired[k] + items[k]["h"] / 2)
centers = {key: sum(values) / len(values) for key, values in family_centers.items()}
layer.sort(
key=lambda k: (
centers[families[k]],
repr(families[k]),
desired[k] + items[k]["h"] / 2,
repr(k),
)
)
pack(layer, desired, weights)
# Coordinate relaxation holds the planned row order and never moves obstacles
# in response to a routed wire. All channels remain participants in the solve.
for sweep in range(24):
for rank in sorted(layers, reverse=bool(sweep % 2)):
desired = {}
weights = {}
for k in layers[rank]:
adj = neighbors[k]
weights[k] = sum(w for b, ay, by, w in adj) or 1.0
desired[k] = (
sum((y[b] + by - ay) * w for b, ay, by, w in adj) / weights[k] if adj else y[k]
)
pack(layers[rank], desired, weights)
top = min(y.values(), default=0.0)
y = {k: value - top for k, value in y.items()}
widths = {rank: max((items[k]["w"] for k in layer), default=0.0) for rank, layer in layers.items()}
x = {}
cursor = 0.0
for rank in sorted(layers):
x[rank] = cursor
cursor += widths[rank] + 200.0
width = cursor - 200.0
if rank_map is not None:
x = {rank: planned["columns"][rank] for rank in layers}
widths = {rank: planned["column_widths"][rank] for rank in layers}
height = max(y[k] + items[k]["h"] for k in items)
result = dict(members=members, w=width, h=height, positions={}, routes={}, inputs={}, outputs={})
for k, item in items.items():
ox, oy = x[ranks[k]], y[k]
result["positions"].update({i: (ox + px, oy + py) for i, (px, py) in item["positions"].items()})
for e, path in item["routes"].items():
result["routes"].setdefault(e, []).append([(ox + px, oy + py) for px, py in path])
for e, u, v, sy, ty, weight in links:
path = []
chain = chains[e]
for j, (a, b) in enumerate(zip(chain, chain[1:])):
first_y = y[a] + (sy if j == 0 else items[a]["h"] / 2)
last_y = y[b] + (ty if j == len(chain) - 2 else items[b]["h"] / 2)
first_x = x[ranks[a]] + items[a]["w"]
last_x = x[ranks[b]]
path.append((first_x, first_y))
if items[a]["w"] < widths[ranks[a]]:
path.append((x[ranks[a]] + widths[ranks[a]] + 60.0, first_y))
path.append((last_x, last_y))
result["routes"].setdefault(e, []).append(simplify(path))
for e, key in ((e, boundary["in", e]) for e in incoming):
result["inputs"][e] = (0.0, y[key] + 16.0)
for e, key in ((e, boundary["out", e]) for e in outgoing):
result["outputs"][e] = (width, y[key] + 16.0)
for e, pieces in result["routes"].items():
pieces.sort(key=lambda path: path[0][0])
result["routes"][e] = simplify([point for path in pieces for point in path])
result["ranks"] = {i: ranks[k] for i, k in owner.items()}
result["columns"] = x
result["column_widths"] = widths
return result
planned = plan_scope([leaf(i) for i in range(len(nodes))])
seed = planned["positions"]
for block in blocks:
local = plan_scope([leaf(i) for i in block["members"]], rank_map=planned["ranks"])
preferred = (
sum(seed[i][1] + size[i].height / 2 for i in block["members"]) / len(block["members"])
- local["h"] / 2
)
seed.update({i: (x, y + preferred) for i, (x, y) in local["positions"].items()})
planned["routes"].update(
{e: [(x, y + preferred) for x, y in path] for e, path in local["routes"].items()}
)
atoms = []
node_atom = {}
for members in [b["members"] for b in blocks] + [[i] for i in sorted(core)]:
member_set = set(members)
internal = [e for e, (a, b) in enumerate(edge_ids) if a in member_set and b in member_set]
left, top, right, bottom = bounds({i: seed[i] for i in members})
for e in internal:
top = min(top, *(p[1] for p in planned["routes"][e]))
bottom = max(bottom, *(p[1] for p in planned["routes"][e]))
k = len(atoms)
atoms.append(
dict(
members=member_set,
x=left,
w=right - left,
h=bottom - top,
preferred=top,
internal=internal,
bus=False,
)
)
node_atom.update({i: k for i in members})
external = [e for e, (a, b) in enumerate(edge_ids) if node_atom[a] != node_atom[b]]
buses = {}
edge_bus = {}
for e in external:
a, b = edge_ids[e]
ra, rb = planned["ranks"][a], planned["ranks"][b]
if rb == ra + 1:
continue
key = source_keys[e]
if key not in buses:
buses[key] = []
buses[key].append(e)
for key, links in buses.items():
a = edge_ids[links[0]][0]
ra = planned["ranks"][a]
left = planned["columns"][ra + 1] - 60.0
right = max(planned["columns"][planned["ranks"][edge_ids[e][1]]] - 60.0 for e in links)
ys = [p[1] for e in links for p in planned["routes"][e][1:-1]]
k = len(atoms)
atoms.append(
dict(
members=set(),
x=left,
w=right - left,
h=24.0,
preferred=median(ys) - 12.0,
internal=[],
bus=True,
)
)
edge_bus.update({e: k for e in links})
# Shapes and long-wire reservations are packed in the same interval constraint
# graph. No obstacle test or routing result can change these constraints later.
atom_order = sorted(range(len(atoms)), key=lambda k: (atoms[k]["preferred"] + atoms[k]["h"] / 2, k))
above = defaultdict(list)
below = defaultdict(list)
for j, a in enumerate(atom_order):
for b in atom_order[j + 1 :]:
u, v = atoms[a], atoms[b]
if u["x"] < v["x"] + v["w"] + 32 and v["x"] < u["x"] + u["w"] + 32:
gap = 32.0 if u["bus"] or v["bus"] else 220.0
distance = u["h"] + gap
above[b].append((a, distance))
below[a].append((b, distance))
ay = {}
for k in atom_order:
ay[k] = max([atoms[k]["preferred"], *(ay[a] + distance for a, distance in above[k])])
neighbors = defaultdict(list)
for e in external:
a, b = edge_ids[e]
u, v = node_atom[a], node_atom[b]
sy = seed[a][1] + size[a].sockets[edges[e].from_socket] - atoms[u]["preferred"]
ty = seed[b][1] + size[b].sockets[edges[e].to_socket] - atoms[v]["preferred"]
weight = 3.0 if edge_types[e] in {"GEOMETRY", "SHADER"} else 1.0
if e in edge_bus:
weight *= 0.15
sequence = [(u, sy), (v, ty)] if e not in edge_bus else [(u, sy), (edge_bus[e], 12.0), (v, ty)]
for (k, ky), (j, jy) in zip(sequence, sequence[1:]):
neighbors[k].append((j, ky, jy, weight))
neighbors[j].append((k, jy, ky, weight))
for sweep in range(80):
for k in atom_order if sweep % 2 else reversed(atom_order):
adjacent = neighbors[k]
preferred = (
sum((ay[j] + jy - ky) * w for j, ky, jy, w in adjacent) / sum(w for j, ky, jy, w in adjacent)
if adjacent
else ay[k]
)
low = max((ay[j] + d for j, d in above[k]), default=-1e9)
high = min((ay[j] - d for j, d in below[k]), default=1e9)
ay[k] = max(low, min(high, preferred))
shift = min(ay.values())
ay = {k: value - shift for k, value in ay.items()}
positions = {i: (seed[i][0], seed[i][1] + ay[k] - atoms[k]["preferred"]) for i, k in node_atom.items()}
routes = {}
for k, atom in enumerate(atoms):
for e in atom["internal"]:
routes[e] = [(x, y + ay[k] - atom["preferred"]) for x, y in planned["routes"][e]]
for e in external:
a, b = edge_ids[e]
start, end = endpoint(edges[e], positions)
ra, rb = planned["ranks"][a], planned["ranks"][b]
exit_x = planned["columns"][ra] + planned["column_widths"][ra] + 60.0
entry_x = planned["columns"][rb] - 60.0
if e in edge_bus:
k = edge_bus[e]
level = ay[k] + 12.0
path = [start, (exit_x, start[1]), (atoms[k]["x"], level), (entry_x, level), end]
else:
path = (
[start, end]
if size[a].width == planned["column_widths"][ra]
else [start, (exit_x, start[1]), end]
)
routes[e] = simplify(path)
for block in blocks:
block["box"] = bounds({i: positions[i] for i in block["members"]})
return positions, routes