Files
AnyImage/tools/nodes/arrangement/flow.py
nan 89087fc2e2 feat: Add new Geometry Nodes groups for image relief and shadeless shader
- Implemented the "O Image Relief Plane" Geometry Nodes group to create a fixed-base image relief using depth images and various parameters.
- Created the "O Shadeless" shader node group to display color without scene lighting while preserving shadows and reflections.
- Introduced a preview feature for node groups, allowing for interactive visualization of node arrangements in an HTML format.
- Added a template HTML file for rendering node previews with customizable views and controls.
2026-09-09 21:24:57 +08:00

152 lines
5.8 KiB
Python

"""Infer geometry flow and socket relationships for layout and preview."""
import json
from collections import defaultdict
from graphlib import TopologicalSorter
from pathlib import Path
from .dimensions import visible_socket
SEMANTICS = json.loads(Path(__file__).with_name("semantics.json").read_text(encoding="utf-8"))
SHARED_GEOMETRY_USES = 3
def socket_links(socket):
return sorted(socket.links, key=lambda link: link.multi_input_sort_id, reverse=True)
def origin_socket(socket):
while socket.node.bl_idname == "NodeReroute":
socket = socket.node.inputs[0].links[0].from_socket
return socket
def graph(node_group):
def key(node):
if node.bl_idname in {"NodeGroupInput", "NodeReroute"}:
return (1, tuple(sorted((link.to_node.name, link.to_socket.identifier,
socket_links(link.to_socket).index(link))
for socket in node.outputs for link in socket.links)))
return (0, node.name)
nodes = sorted(node_group.nodes, key=key)
parents = {node: [] for node in nodes}
children = {node: set() for node in nodes}
for link in node_group.links:
if link.from_node not in parents[link.to_node]:
parents[link.to_node].append(link.from_node)
children[link.from_node].add(link.to_node)
for sources in parents.values():
sources.sort(key=key)
order = list(TopologicalSorter(parents).static_order())
ancestors = {}
for node in order:
ancestors[node] = set(parents[node]) | set().union(*(ancestors[p] for p in parents[node]))
return parents, children, order, ancestors
def geometry_paths(node_group):
"""Trace every geometry input and split paths at forks and merges."""
output = next((node for node in node_group.nodes if node.bl_idname == "NodeGroupOutput" and node.is_active_output), None)
parents = {}
uses = defaultdict(set)
for link in node_group.links:
if link.from_socket.type == "GEOMETRY" and link.to_node.bl_idname != "NodeReroute":
uses[origin_socket(link.from_socket).node].add(link.to_node)
paired = {member for node in node_group.nodes if getattr(node, "paired_output", None) is not None
for member in (node, node.paired_output)}
result_nodes, pending = set(), [*paired, *([output] if output else [])]
while pending:
node = pending.pop()
if node in result_nodes:
continue
result_nodes.add(node)
pending.extend(origin_socket(link.from_socket).node
for socket in node.inputs if socket.type == "GEOMETRY" for link in socket.links)
shared = {node for node, targets in uses.items()
if len(targets) >= SHARED_GEOMETRY_USES and node not in paired
and (not any(socket.type == "GEOMETRY" and socket.is_linked for socket in node.inputs)
or len(targets & result_nodes) >= SHARED_GEOMETRY_USES)}
def source_node(link):
return link.from_node if link.from_node.get("arrangement_input") else origin_socket(link.from_socket).node
def collect(node):
if node in parents:
return
parents[node] = list(dict.fromkeys(
source_node(link) for socket in node.inputs if socket.type == "GEOMETRY"
for link in socket_links(socket)
if source_node(link).bl_idname != "NodeGroupInput" and source_node(link) not in shared
)) if not node.get("arrangement_input") else []
for parent in parents[node]:
collect(parent)
roots = []
if output is not None:
collect(output)
roots.append(output)
for node in node_group.nodes:
if node in paired and node not in parents:
collect(node)
roots.append(node)
children = {node: set() for node in parents}
for node, sources in parents.items():
for source in sources:
children[source].add(node)
paths, assigned, pending = [], set(), roots
while pending:
node = pending.pop(0)
path = []
while node not in assigned:
path.append(node)
assigned.add(node)
sources = parents[node]
if len(sources) != 1 or len(children[sources[0]]) != 1:
pending.extend(source for source in sources if source not in assigned)
break
node = sources[0]
if path:
paths.append(list(reversed(path)))
return parents, paths
def shared_geometry_outputs(nodes):
"""Find geometry outputs reused by several distinct operations."""
shared = []
for node in nodes:
for socket in node.outputs:
if socket.type != "GEOMETRY":
continue
targets, pending = set(), [link.to_node for link in socket.links]
while pending:
target = pending.pop()
if target.bl_idname == "NodeReroute":
pending.extend(link.to_node for link in target.outputs[0].links)
else:
targets.add(target)
if len(targets) >= SHARED_GEOMETRY_USES:
shared.append(socket)
return shared
def peer_links(node):
"""Read ordered data operands from the semantic table, excluding controls."""
rule = SEMANTICS.get(node.bl_idname)
if rule is None or ("operations" in rule and node.operation not in rule["operations"]):
return []
return [link for socket in list(node.inputs)[rule["peer_inputs_from"]:]
if visible_socket(socket) and socket.type != "CUSTOM"
for link in socket_links(socket)]
def node_roles(node_group):
"""Classify by contribution to the result, not by available socket types."""
core, _ = geometry_paths(node_group)
return {node: "geometry" if node in core else "field" for node in node_group.nodes}