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:
2026-09-07 19:21:03 +08:00
parent a0adf79db0
commit 7dec3b7ca4
21 changed files with 2075 additions and 373 deletions

View 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)