Add depth surface processing and layout tools
- Implemented `cutout_depth_surface.py` for face separation and connected thickness calculations for depth surface assets. - Created `cutout_depth_surface_layout.py` to arrange nodes for staggered calculation branches in depth surface processing. - Authored `geometry_layouts.py` to define layouts for various geometry assets, including image cutouts and depth planes. - Introduced `node_layout.py` for shared presentation rules, facilitating node arrangement and validation in geometry-node layouts.
This commit is contained in:
138
tools/node_assets/node_layout.py
Normal file
138
tools/node_assets/node_layout.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""Shared presentation rules for authored geometry-node layouts."""
|
||||
|
||||
|
||||
def arrange_nodes(group, positions, input_regions):
|
||||
"""Place authored branches and rebuild their local Group Inputs."""
|
||||
controls = [
|
||||
(link.to_socket, link.from_socket.name)
|
||||
for link in group.links
|
||||
if link.from_node.bl_idname == "NodeGroupInput"
|
||||
]
|
||||
for node in list(group.nodes):
|
||||
if node.bl_idname == "NodeGroupInput":
|
||||
group.nodes.remove(node)
|
||||
assert set(positions) == {node.name for node in group.nodes}, group.name
|
||||
for name, location in positions.items():
|
||||
group.nodes[name].location = location
|
||||
for targets, location in input_regions:
|
||||
node = group.nodes.new("NodeGroupInput")
|
||||
node.location = location
|
||||
for socket, name in controls:
|
||||
if socket.node.name in targets:
|
||||
group.links.new(node.outputs[name], socket)
|
||||
assert all(socket.is_linked for socket, _ in controls), group.name
|
||||
for node in group.nodes:
|
||||
node.select = False
|
||||
node.width = (
|
||||
200
|
||||
if "Attribute" in node.bl_idname or node.bl_idname == "GeometryNodeGroup"
|
||||
else 160
|
||||
)
|
||||
node.hide = node.bl_idname in {
|
||||
"ShaderNodeMath",
|
||||
"ShaderNodeVectorMath",
|
||||
} and all(
|
||||
socket.is_linked
|
||||
for socket in node.inputs
|
||||
if socket.enabled and not socket.hide
|
||||
)
|
||||
for socket in node.outputs:
|
||||
socket.hide = not socket.is_linked
|
||||
|
||||
|
||||
def validate_node_layout(group):
|
||||
"""Check visible bounds and sockets, allowing direct backward references."""
|
||||
nodes = [n for n in group.nodes if n.bl_idname not in {"NodeReroute", "NodeFrame"}]
|
||||
assert all(not n.label for n in nodes), group.name
|
||||
assert not any(n.bl_idname == "FunctionNodeInputString" for n in nodes), group.name
|
||||
assert all(s.is_linked or s.hide for n in nodes for s in n.outputs), group.name
|
||||
validate_peer_inputs(group)
|
||||
for link in group.links:
|
||||
if (
|
||||
link.from_node.bl_idname != "NodeReroute"
|
||||
or link.to_node.bl_idname == "NodeReroute"
|
||||
):
|
||||
continue
|
||||
source = link.from_node
|
||||
while source.bl_idname == "NodeReroute":
|
||||
source = source.inputs[0].links[0].from_node
|
||||
assert source.location.x <= link.to_node.location.x, (
|
||||
group.name,
|
||||
source.name,
|
||||
link.to_node.name,
|
||||
)
|
||||
for index, node in enumerate(nodes):
|
||||
width = node.dimensions.x or node.width
|
||||
height = node.dimensions.y or (30 if node.hide else node.height)
|
||||
for other in nodes[index + 1 :]:
|
||||
other_width = other.dimensions.x or other.width
|
||||
other_height = other.dimensions.y or (30 if other.hide else other.height)
|
||||
overlap = (
|
||||
node.location.x < other.location.x + other_width
|
||||
and node.location.x + width > other.location.x
|
||||
and node.location.y - height < other.location.y
|
||||
and node.location.y > other.location.y - other_height
|
||||
)
|
||||
assert not overlap, (group.name, node.name, other.name)
|
||||
if node.bl_idname == "GeometryNodeRepeatInput":
|
||||
assert (
|
||||
node.paired_output and node.paired_output.location.x > node.location.x
|
||||
)
|
||||
|
||||
|
||||
def validate_peer_inputs(group):
|
||||
"""Align peer branch ends while leaving shared upstream sources in place."""
|
||||
parents = {node: set() for node in group.nodes}
|
||||
consumers = {node: set() for node in group.nodes}
|
||||
for link in group.links:
|
||||
parents[link.to_node].add(link.from_node)
|
||||
consumers[link.from_node].add(link.to_node)
|
||||
|
||||
def ancestors(node):
|
||||
found = set()
|
||||
pending = list(parents[node])
|
||||
while pending:
|
||||
parent = pending.pop()
|
||||
if parent in found:
|
||||
continue
|
||||
found.add(parent)
|
||||
pending.extend(parents[parent])
|
||||
return found
|
||||
|
||||
for node in group.nodes:
|
||||
if node.bl_idname not in {
|
||||
"GeometryNodeJoinGeometry",
|
||||
"GeometryNodeMenuSwitch",
|
||||
"GeometryNodeSwitch",
|
||||
"ShaderNodeCombineXYZ",
|
||||
}:
|
||||
continue
|
||||
combine = node.bl_idname == "ShaderNodeCombineXYZ"
|
||||
sources = set()
|
||||
for socket in node.inputs:
|
||||
if not combine and socket.type != "GEOMETRY":
|
||||
continue
|
||||
for link in socket.links:
|
||||
source = link.from_node
|
||||
while source.bl_idname == "NodeReroute":
|
||||
source = source.inputs[0].links[0].from_node
|
||||
if combine or source.bl_idname != "NodeGroupInput":
|
||||
sources.add(source)
|
||||
upstream = set().union(*(ancestors(source) for source in sources))
|
||||
peers = sources - upstream
|
||||
if combine:
|
||||
peers = {source for source in peers if consumers[source] == {node}}
|
||||
edges = [source.location.x + source.width for source in peers]
|
||||
assert not edges or max(edges) - min(edges) < 1.0, (
|
||||
group.name,
|
||||
node.name,
|
||||
[source.name for source in peers],
|
||||
)
|
||||
if node.bl_idname == "GeometryNodeMenuSwitch":
|
||||
rows = [
|
||||
link.from_node.location.y
|
||||
for socket in node.inputs
|
||||
for link in socket.links
|
||||
if link.from_node in peers
|
||||
]
|
||||
assert rows == sorted(rows, reverse=True), (group.name, node.name)
|
||||
Reference in New Issue
Block a user