- Removed obsolete files: sections.py, semantics.json, and sources.py. - Introduced new modules: branches.py and layout.py for improved calculation block identification and layout planning. - Implemented a unified layout strategy for geometry nodes, ensuring consistent spacing and organization. - Added comprehensive documentation for the new layout design and its goals. - Established a migration plan for transitioning existing assets to the new layout system. - Updated tests to validate the new arrangement logic and ensure compatibility with existing geometry node structures.
131 lines
5.6 KiB
Python
131 lines
5.6 KiB
Python
"""Materialize the joint calculation and connection plan in geometry node trees."""
|
|
|
|
from .branches import calculation_blocks
|
|
from .dimensions import estimate_node_dimensions, visible_socket
|
|
from .flow import origin_socket, socket_links
|
|
from .layout import plan_layout
|
|
from ..common.nodes import prepare_node_group, replace_link_source
|
|
|
|
|
|
def _remove_routes(node_group):
|
|
previous = [node for node in node_group.nodes if node.bl_idname == "NodeReroute"]
|
|
|
|
for node in [node for node in node_group.nodes if node not in previous]:
|
|
for socket in node.inputs:
|
|
links = socket_links(socket)
|
|
if not any(link.from_node in previous for link in links):
|
|
continue
|
|
if not socket.is_multi_input:
|
|
replace_link_source(node_group, links[0], origin_socket(links[0].from_socket))
|
|
continue
|
|
sources = [origin_socket(link.from_socket) for link in links]
|
|
for link in links:
|
|
node_group.links.remove(link)
|
|
seen = set()
|
|
for source in reversed(sources):
|
|
original = source
|
|
if source in seen:
|
|
# Blender coalesces identical direct links; retain repeated inputs.
|
|
duplicate = node_group.nodes.new("NodeReroute")
|
|
node_group.links.new(source, duplicate.inputs[0])
|
|
source = duplicate.outputs[0]
|
|
seen.add(original)
|
|
node_group.links.new(source, socket)
|
|
for node in previous:
|
|
node_group.nodes.remove(node)
|
|
|
|
|
|
def _localize_inputs(node_group, nodes, blocks, core):
|
|
"""Read interface values locally in each consuming calculation body."""
|
|
ids = {node: i for i, node in enumerate(nodes)}
|
|
block_of = {i: k for k, block in enumerate(blocks) for i in block["members"]}
|
|
for node in list(nodes):
|
|
if node.bl_idname != "NodeGroupInput":
|
|
continue
|
|
original = ("core", ids[node]) if ids[node] in core else ("block", block_of[ids[node]])
|
|
uses = list(node_group.links)
|
|
direct = [ids[edge.to_node] for edge in uses if edge.from_node == node and ids[edge.to_node] in core]
|
|
if direct:
|
|
original = ("core", direct[0])
|
|
local_reads = {original: node}
|
|
for edge in uses:
|
|
if edge.from_node != node:
|
|
continue
|
|
target = ids[edge.to_node]
|
|
owner = ("block", block_of[target]) if target in block_of else ("core", target)
|
|
if owner not in local_reads:
|
|
clone = node_group.nodes.new("NodeGroupInput")
|
|
ids[clone] = len(nodes)
|
|
nodes.append(clone)
|
|
if owner[0] == "block":
|
|
blocks[owner[1]]["members"].append(ids[clone])
|
|
block_of[ids[clone]] = owner[1]
|
|
else:
|
|
core.add(ids[clone])
|
|
local_reads[owner] = clone
|
|
source = next(
|
|
socket
|
|
for socket in local_reads[owner].outputs
|
|
if socket.identifier == edge.from_socket.identifier
|
|
)
|
|
if source != edge.from_socket:
|
|
replace_link_source(node_group, edge, source)
|
|
|
|
|
|
def arrange_nodes(node_group):
|
|
"""Arrange calculation bodies and shared channels before creating reroutes."""
|
|
for frame in [node for node in node_group.nodes if node.bl_idname == "NodeFrame"]:
|
|
for child in [node for node in node_group.nodes if node.parent == frame]:
|
|
position = tuple(child.location_absolute)
|
|
child.parent = None
|
|
child.location = position
|
|
node_group.nodes.remove(frame)
|
|
_remove_routes(node_group)
|
|
nodes = list(node_group.nodes)
|
|
if not nodes:
|
|
return
|
|
blocks, core = calculation_blocks(nodes, list(node_group.links))
|
|
_localize_inputs(node_group, nodes, blocks, core)
|
|
prepare_node_group(node_group)
|
|
for node in nodes:
|
|
node.select = False
|
|
if node.bl_idname in {"ShaderNodeMath", "ShaderNodeVectorMath", "FunctionNodeIntegerMath"}:
|
|
node.hide = all(socket.is_linked for socket in node.inputs if visible_socket(socket))
|
|
sizes = {i: estimate_node_dimensions(node) for i, node in enumerate(nodes)}
|
|
edges = list(node_group.links)
|
|
positions, routes = plan_layout(nodes, edges, sizes, blocks, core)
|
|
for i, (x, y) in positions.items():
|
|
nodes[i].location = (x, -y)
|
|
nodes[i].width = sizes[i].width
|
|
cache = {}
|
|
for index, path in routes.items():
|
|
edge = edges[index]
|
|
source = edge.from_socket
|
|
for x, y in path[1:-1]:
|
|
key = (source, round(x, 4), round(y, 4))
|
|
reroute = cache.get(key)
|
|
if reroute is None:
|
|
reroute = node_group.nodes.new("NodeReroute")
|
|
reroute.location = (x - 8, -y + 8)
|
|
node_group.links.new(source, reroute.inputs[0])
|
|
cache[key] = reroute
|
|
source = reroute.outputs[0]
|
|
if len(path) > 2:
|
|
replace_link_source(node_group, edge, source)
|
|
for block in blocks:
|
|
if len(block["members"]) < 5:
|
|
continue
|
|
left, top, right, bottom = block["box"]
|
|
frame = node_group.nodes.new("NodeFrame")
|
|
frame["arrangement_frame"] = True
|
|
frame.name = frame.label = block["title"]
|
|
frame.shrink = False
|
|
frame.location = (left - 28, -top + 48)
|
|
frame.width = right - left + 56
|
|
frame.height = bottom - top + 76
|
|
for i in block["members"]:
|
|
absolute = nodes[i].location.copy()
|
|
nodes[i].parent = frame
|
|
nodes[i].location = absolute - frame.location
|
|
prepare_node_group(node_group)
|