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:
@@ -3,6 +3,14 @@
|
||||
|
||||
import bpy
|
||||
from node_graph import expand_smooth
|
||||
from cutout_depth_surface import (
|
||||
store_surface_camera,
|
||||
remove_surface_attributes,
|
||||
split_depth_surface,
|
||||
thicken_depth_surface,
|
||||
)
|
||||
from cutout_depth_surface_layout import layout_depth_surface
|
||||
from geometry_layouts import layout_geometry
|
||||
|
||||
from anyimage.operators.cutout_tool.shape import (
|
||||
BALLOON_ATTRIBUTE_NAME,
|
||||
@@ -427,81 +435,6 @@ def _position_field(group, depth_image, location):
|
||||
return position_texture.outputs["Color"]
|
||||
|
||||
|
||||
def _capture_edge_length(group, geometry, name, location):
|
||||
nodes = group.nodes
|
||||
links = group.links
|
||||
x, y = location
|
||||
edge_vertices = nodes.new("GeometryNodeInputMeshEdgeVertices")
|
||||
edge_vertices.location = (x, y + 250.0)
|
||||
edge_length = nodes.new("ShaderNodeVectorMath")
|
||||
edge_length.operation = "DISTANCE"
|
||||
edge_length.location = (x + 220.0, y + 250.0)
|
||||
links.new(edge_vertices.outputs["Position 1"], edge_length.inputs[0])
|
||||
links.new(edge_vertices.outputs["Position 2"], edge_length.inputs[1])
|
||||
capture = nodes.new("GeometryNodeCaptureAttribute")
|
||||
capture.domain = "POINT"
|
||||
capture.capture_items.clear()
|
||||
capture.capture_items.new("FLOAT", name)
|
||||
capture.location = (x + 440.0, y)
|
||||
links.new(geometry, capture.inputs["Geometry"])
|
||||
links.new(edge_length.outputs["Value"], capture.inputs[name])
|
||||
return capture.outputs["Geometry"], capture.outputs[name], edge_length.outputs["Value"]
|
||||
|
||||
|
||||
def _remove_stretched_points(
|
||||
group,
|
||||
geometry,
|
||||
original_edge_length,
|
||||
current_edge_length,
|
||||
stretch_limit,
|
||||
location,
|
||||
):
|
||||
nodes = group.nodes
|
||||
links = group.links
|
||||
x, y = location
|
||||
|
||||
remaining_ratio = _math(nodes, "DIVIDE", (x + 220.0, y + 300.0))
|
||||
links.new(original_edge_length, remaining_ratio.inputs[0])
|
||||
links.new(current_edge_length, remaining_ratio.inputs[1])
|
||||
stretched = _math(nodes, "LESS_THAN", (x + 450.0, y + 200.0))
|
||||
links.new(remaining_ratio.outputs[0], stretched.inputs[0])
|
||||
links.new(stretch_limit, stretched.inputs[1])
|
||||
delete_stretched = nodes.new("GeometryNodeDeleteGeometry")
|
||||
delete_stretched.domain = "POINT"
|
||||
delete_stretched.mode = "ALL"
|
||||
delete_stretched.location = (x + 680.0, y)
|
||||
links.new(geometry, delete_stretched.inputs["Geometry"])
|
||||
links.new(stretched.outputs[0], delete_stretched.inputs["Selection"])
|
||||
|
||||
edge_neighbors = nodes.new("GeometryNodeInputMeshEdgeNeighbors")
|
||||
edge_neighbors.location = (x + 930.0, y + 500.0)
|
||||
loose_edge = _math(nodes, "LESS_THAN", (x + 1150.0, y + 500.0))
|
||||
loose_edge.inputs[1].default_value = 1.0
|
||||
links.new(edge_neighbors.outputs["Face Count"], loose_edge.inputs[0])
|
||||
delete_loose_edges = nodes.new("GeometryNodeDeleteGeometry")
|
||||
delete_loose_edges.domain = "EDGE"
|
||||
delete_loose_edges.mode = "ALL"
|
||||
delete_loose_edges.location = (x + 1370.0, y)
|
||||
links.new(delete_stretched.outputs["Geometry"], delete_loose_edges.inputs["Geometry"])
|
||||
links.new(loose_edge.outputs[0], delete_loose_edges.inputs["Selection"])
|
||||
|
||||
vertex_neighbors = nodes.new("GeometryNodeInputMeshVertexNeighbors")
|
||||
vertex_neighbors.location = (x + 1620.0, y + 500.0)
|
||||
loose_point = _math(nodes, "LESS_THAN", (x + 1840.0, y + 500.0))
|
||||
loose_point.inputs[1].default_value = 1.0
|
||||
links.new(vertex_neighbors.outputs["Face Count"], loose_point.inputs[0])
|
||||
delete_loose_points = nodes.new("GeometryNodeDeleteGeometry")
|
||||
delete_loose_points.domain = "POINT"
|
||||
delete_loose_points.mode = "ALL"
|
||||
delete_loose_points.location = (x + 2060.0, y)
|
||||
links.new(
|
||||
delete_loose_edges.outputs["Geometry"],
|
||||
delete_loose_points.inputs["Geometry"],
|
||||
)
|
||||
links.new(loose_point.outputs[0], delete_loose_points.inputs["Selection"])
|
||||
return delete_loose_points.outputs["Geometry"]
|
||||
|
||||
|
||||
def _plane_depth(group, direction, reference_depth, location):
|
||||
nodes = group.nodes
|
||||
links = group.links
|
||||
@@ -673,20 +606,22 @@ def _build_depth_surface(
|
||||
geometry,
|
||||
thickness,
|
||||
depth_scale,
|
||||
stretch_limit,
|
||||
split,
|
||||
uniform_scale,
|
||||
reference_depth,
|
||||
depth_image,
|
||||
cleanup,
|
||||
):
|
||||
nodes = group.nodes
|
||||
links = group.links
|
||||
geometry, original_edge_length, current_edge_length = _capture_edge_length(
|
||||
group,
|
||||
geometry,
|
||||
"Original Edge Length",
|
||||
(400.0, 850.0),
|
||||
geometry = split_depth_surface(
|
||||
group, geometry, depth_image, split, uniform_scale, depth_scale
|
||||
)
|
||||
geometry = _island_cleanup(group, geometry, cleanup, (1800.0, 850.0))
|
||||
original_camera = _position_field(group, depth_image, (-200.0, -200.0))
|
||||
geometry, camera_position = store_surface_camera(
|
||||
group, geometry, original_camera, split
|
||||
)
|
||||
camera_position = _position_field(group, depth_image, (-200.0, -200.0))
|
||||
scaled = nodes.new("ShaderNodeVectorMath")
|
||||
scaled.operation = "SCALE"
|
||||
scaled.location = (3100.0, -100.0)
|
||||
@@ -731,45 +666,12 @@ def _build_depth_surface(
|
||||
links.new(geometry, project_geometry.inputs["Geometry"])
|
||||
links.new(surface_position.outputs["Vector"], project_geometry.inputs["Position"])
|
||||
|
||||
geometry = _remove_stretched_points(
|
||||
group,
|
||||
project_geometry.outputs["Geometry"],
|
||||
original_edge_length,
|
||||
current_edge_length,
|
||||
stretch_limit,
|
||||
(4700.0, 400.0),
|
||||
)
|
||||
zero_z = nodes.new("ShaderNodeValue")
|
||||
zero_z.location = (7100.0, 100.0)
|
||||
zero_z.outputs["Value"].default_value = 0.0
|
||||
geometry = _set_geometry_z(
|
||||
group,
|
||||
geometry,
|
||||
zero_z.outputs["Value"],
|
||||
(7200.0, 400.0),
|
||||
)
|
||||
|
||||
back = _math(nodes, "SUBTRACT", (3900.0, -100.0))
|
||||
links.new(front.outputs[0], back.inputs[0])
|
||||
links.new(thickness, back.inputs[1])
|
||||
shaped = _set_segmented_layer_z(
|
||||
group,
|
||||
geometry,
|
||||
front.outputs[0],
|
||||
back.outputs[0],
|
||||
thickness,
|
||||
(8300.0, 400.0),
|
||||
)
|
||||
front_only = _set_geometry_z(
|
||||
group,
|
||||
geometry,
|
||||
front.outputs[0],
|
||||
(8500.0, -200.0),
|
||||
)
|
||||
geometry = project_geometry.outputs["Geometry"]
|
||||
shaped = thicken_depth_surface(group, geometry, thickness)
|
||||
geometry = _remove_zero_thickness(
|
||||
group, front_only, shaped, thickness, (11800.0, 400.0)
|
||||
group, project_geometry.outputs["Geometry"], shaped, thickness, (11800.0, 400.0)
|
||||
)
|
||||
return geometry
|
||||
return remove_surface_attributes(group, geometry)
|
||||
|
||||
|
||||
def build_node_group(shape):
|
||||
@@ -791,7 +693,7 @@ def build_node_group(shape):
|
||||
thickness.description = "Final thickness in scene distance"
|
||||
|
||||
depth_image = None
|
||||
stretch_limit = None
|
||||
split = None
|
||||
if shape == "DEPTH_BALLOON":
|
||||
depth_scale = _float_input(group, "Depth Scale", 1.0)
|
||||
depth_scale.description = "Scale the fitted-plane-corrected metric depth"
|
||||
@@ -828,19 +730,18 @@ def build_node_group(shape):
|
||||
)
|
||||
elif shape == "DEPTH_SURFACE":
|
||||
options = group.interface.new_panel(name="Options", default_closed=True)
|
||||
stretch_limit = _float_input(
|
||||
split = _float_input(
|
||||
group,
|
||||
"Stretch Limit",
|
||||
"Split",
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
subtype="FACTOR",
|
||||
parent=options,
|
||||
)
|
||||
stretch_limit.description = (
|
||||
"Delete faces when original edge length divided by projected edge "
|
||||
"length falls below this threshold; zero disables culling"
|
||||
)
|
||||
split.description = "Separate depth discontinuities; zero disables splitting"
|
||||
cleanup = _float_input(group, "Cleanup", 0.0, 0.0, None, parent=options)
|
||||
cleanup.description = "Remove split islands below this local-area threshold"
|
||||
reference_depth = _float_input(
|
||||
group,
|
||||
"Reference Depth",
|
||||
@@ -880,7 +781,8 @@ def build_node_group(shape):
|
||||
"Uniform Scale",
|
||||
"Reference Depth",
|
||||
"Depth Direction",
|
||||
"Stretch Limit",
|
||||
"Split",
|
||||
"Cleanup",
|
||||
},
|
||||
)
|
||||
geometry = controls.outputs["Geometry"]
|
||||
@@ -913,14 +815,19 @@ def build_node_group(shape):
|
||||
geometry,
|
||||
controls.outputs["Thickness"],
|
||||
controls.outputs["Depth Scale"],
|
||||
controls.outputs["Stretch Limit"],
|
||||
controls.outputs["Split"],
|
||||
controls.outputs["Uniform Scale"],
|
||||
controls.outputs["Reference Depth"],
|
||||
controls.outputs["Depth Image"],
|
||||
controls.outputs["Cleanup"],
|
||||
)
|
||||
output_location = (12100.0, 400.0)
|
||||
_localize_group_inputs(group, controls)
|
||||
_shade_output(group, geometry, output_location)
|
||||
if shape == "DEPTH_SURFACE":
|
||||
layout_depth_surface(group)
|
||||
else:
|
||||
layout_geometry(group)
|
||||
group.color_tag = "GEOMETRY"
|
||||
group.description = {
|
||||
"SURFACE": "Symmetrically thicken a Surface sheet.",
|
||||
@@ -983,19 +890,16 @@ def build_cutout_modifier_group(shape_groups):
|
||||
parent=options,
|
||||
)
|
||||
smooth.description = "Blend each smoothing pass into the selected shape"
|
||||
stretch_limit = _float_input(
|
||||
split = _float_input(
|
||||
group,
|
||||
"Stretch Limit",
|
||||
"Split",
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
subtype="FACTOR",
|
||||
parent=options,
|
||||
)
|
||||
stretch_limit.description = (
|
||||
"Delete Depth Surface faces when original edge length divided by projected "
|
||||
"edge length falls below this threshold; zero disables culling"
|
||||
)
|
||||
split.description = "Separate Depth Surface discontinuities; zero disables splitting"
|
||||
|
||||
reference_depth = _float_input(
|
||||
group,
|
||||
@@ -1054,7 +958,10 @@ def build_cutout_modifier_group(shape_groups):
|
||||
node.node_tree = shape_groups[shape]
|
||||
node.location = (-850.0, y)
|
||||
node.width = 220.0
|
||||
group.links.new(geometry, node.inputs["Geometry"])
|
||||
group.links.new(
|
||||
base_controls.outputs["Geometry"] if shape == "DEPTH_SURFACE" else geometry,
|
||||
node.inputs["Geometry"],
|
||||
)
|
||||
visible = {
|
||||
socket.name
|
||||
for socket in node.inputs
|
||||
@@ -1064,7 +971,10 @@ def build_cutout_modifier_group(shape_groups):
|
||||
for socket in node.inputs:
|
||||
if not socket.name or socket.name == "Geometry":
|
||||
continue
|
||||
group.links.new(controls.outputs[socket.name], socket)
|
||||
group.links.new(
|
||||
cleanup_area.outputs[0] if socket.name == "Cleanup" else controls.outputs[socket.name],
|
||||
socket,
|
||||
)
|
||||
branch_nodes[shape] = node
|
||||
|
||||
menu_switch = group.nodes.new("GeometryNodeMenuSwitch")
|
||||
@@ -1114,10 +1024,14 @@ def build_cutout_modifier_group(shape_groups):
|
||||
group.links.new(geometry, axis_switch.inputs["-Z"])
|
||||
group.links.new(orient.outputs["Geometry"], axis_switch.inputs["+X"])
|
||||
_shade_output(group, axis_switch.outputs["Output"], (3850.0, 550.0))
|
||||
layout_geometry(group)
|
||||
group.color_tag = "GEOMETRY"
|
||||
group.description = (
|
||||
"Build Surface, Balloon, Depth Balloon, or Depth Surface from one "
|
||||
"menu-controlled Cutout modifier."
|
||||
)
|
||||
for node in group.nodes:
|
||||
for socket in node.outputs:
|
||||
socket.hide = not socket.is_linked
|
||||
group.use_fake_user = True
|
||||
return group
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import bpy
|
||||
|
||||
from geometry_layouts import layout_geometry
|
||||
|
||||
|
||||
DEPTH_PLANE_NODE_GROUP_NAME = "O Image Depth Plane"
|
||||
|
||||
@@ -206,4 +208,5 @@ def build_depth_plane_group(mesh_plane_group):
|
||||
"Build a single-sided depth terrain block with a fixed base."
|
||||
)
|
||||
group.use_fake_user = True
|
||||
layout_geometry(group)
|
||||
return group
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import bpy
|
||||
|
||||
from geometry_layouts import layout_geometry
|
||||
|
||||
|
||||
MESH_PLANE_GROUP_NAME = "O Mesh Plane"
|
||||
|
||||
@@ -27,6 +29,31 @@ def new_group_input(nodes, location, visible_names):
|
||||
return node
|
||||
|
||||
|
||||
def localize_dimensions(group):
|
||||
"""Repeat the small source-bounds calculation near UV and mesh consumers."""
|
||||
sources = [
|
||||
group.nodes[name] for name in ("Bounding Box", "Vector Math", "Separate XYZ")
|
||||
]
|
||||
for targets in (
|
||||
{"Math.011", "Math.013"},
|
||||
{"Grid", "Combine XYZ.001", "Vector Math.001"},
|
||||
):
|
||||
copies = {source: group.nodes.new(source.bl_idname) for source in sources}
|
||||
copies[sources[1]].operation = "SUBTRACT"
|
||||
for link in list(group.links):
|
||||
if link.to_node in copies:
|
||||
source = copies.get(link.from_node, link.from_node)
|
||||
group.links.new(
|
||||
source.outputs[link.from_socket.identifier],
|
||||
copies[link.to_node].inputs[link.to_socket.identifier],
|
||||
)
|
||||
elif link.from_node in copies and link.to_node.name in targets:
|
||||
group.links.new(
|
||||
copies[link.from_node].outputs[link.from_socket.identifier],
|
||||
link.to_socket,
|
||||
)
|
||||
|
||||
|
||||
def build_mesh_plane_group():
|
||||
group = bpy.data.node_groups.new(MESH_PLANE_GROUP_NAME, "GeometryNodeTree")
|
||||
group.is_modifier = True
|
||||
@@ -256,4 +283,6 @@ def build_mesh_plane_group():
|
||||
"Build an aspect-adaptive image plane and add optional centered thickness."
|
||||
)
|
||||
group.use_fake_user = True
|
||||
localize_dimensions(group)
|
||||
layout_geometry(group)
|
||||
return group
|
||||
|
||||
296
tools/node_assets/cutout_depth_surface.py
Normal file
296
tools/node_assets/cutout_depth_surface.py
Normal file
@@ -0,0 +1,296 @@
|
||||
"""Face separation and connected thickness for Depth Surface assets."""
|
||||
|
||||
TEMPORARY_ATTRIBUTES = {
|
||||
"face_center": ("FLOAT_VECTOR", "FACE"),
|
||||
"face_camera": ("FLOAT_VECTOR", "FACE"),
|
||||
"corner_camera": ("FLOAT_VECTOR", "CORNER"),
|
||||
"cut_vertex": ("BOOLEAN", "CORNER"),
|
||||
"camera": ("FLOAT_VECTOR", "POINT"),
|
||||
"surface_position": ("FLOAT_VECTOR", "POINT"),
|
||||
"source_vertex": ("INT", "POINT"),
|
||||
"wall_layers": ("FLOAT", "POINT"),
|
||||
}
|
||||
|
||||
|
||||
def _store(group, geometry, name, value):
|
||||
node = group.nodes.new("GeometryNodeStoreNamedAttribute")
|
||||
node.data_type, node.domain = TEMPORARY_ATTRIBUTES[name]
|
||||
node.inputs["Name"].default_value = "o_depth_" + name
|
||||
group.links.new(geometry, node.inputs["Geometry"])
|
||||
group.links.new(value, node.inputs["Value"])
|
||||
return node.outputs["Geometry"]
|
||||
|
||||
|
||||
def _read(group, name):
|
||||
node = group.nodes.new("GeometryNodeInputNamedAttribute")
|
||||
node.data_type = TEMPORARY_ATTRIBUTES[name][0]
|
||||
node.inputs["Name"].default_value = "o_depth_" + name
|
||||
return node.outputs["Attribute"]
|
||||
|
||||
|
||||
def remove_surface_attributes(group, geometry):
|
||||
"""Clear the reserved temporary namespace with one wildcard operation."""
|
||||
node = group.nodes.new("GeometryNodeRemoveAttribute")
|
||||
node.pattern_mode = "WILDCARD"
|
||||
node.inputs["Name"].default_value = "o_depth_*"
|
||||
group.links.new(geometry, node.inputs["Geometry"])
|
||||
return node.outputs["Geometry"]
|
||||
|
||||
|
||||
def _math(group, operation, a, b=None):
|
||||
node = group.nodes.new("ShaderNodeMath")
|
||||
node.operation = operation
|
||||
for index, value in enumerate((a, b)):
|
||||
if value is None:
|
||||
continue
|
||||
if isinstance(value, (int, float)):
|
||||
node.inputs[index].default_value = value
|
||||
else:
|
||||
group.links.new(value, node.inputs[index])
|
||||
return node.outputs[0]
|
||||
|
||||
|
||||
def split_depth_surface(group, geometry, image, strength, uniform, depth_scale):
|
||||
"""Keep each face's camera sample on its own corners before splitting."""
|
||||
nodes, links = group.nodes, group.links
|
||||
uv = nodes.new("GeometryNodeInputNamedAttribute")
|
||||
uv.data_type = "FLOAT_VECTOR"
|
||||
uv.inputs["Name"].default_value = "UVMap"
|
||||
face_uv = nodes.new("GeometryNodeFieldOnDomain")
|
||||
face_uv.data_type = "FLOAT_VECTOR"
|
||||
face_uv.domain = "FACE"
|
||||
links.new(uv.outputs["Attribute"], face_uv.inputs["Value"])
|
||||
texture = nodes.new("GeometryNodeImageTexture")
|
||||
texture.interpolation = "Linear"
|
||||
texture.extension = "EXTEND"
|
||||
links.new(image, texture.inputs["Image"])
|
||||
links.new(face_uv.outputs["Value"], texture.inputs["Vector"])
|
||||
position = nodes.new("GeometryNodeInputPosition")
|
||||
faces = _store(group, geometry, "face_center", position.outputs[0])
|
||||
faces = _store(group, faces, "face_camera", texture.outputs["Color"])
|
||||
axes = nodes.new("ShaderNodeSeparateXYZ")
|
||||
links.new(_read(group, "face_camera"), axes.inputs[0])
|
||||
center_field = _read(group, "face_center")
|
||||
centers, depths = [], []
|
||||
for side in (0, 1):
|
||||
corner = nodes.new("GeometryNodeCornersOfEdge")
|
||||
corner.inputs["Sort Index"].default_value = side
|
||||
face = nodes.new("GeometryNodeFaceOfCorner")
|
||||
links.new(corner.outputs["Corner Index"], face.inputs["Corner Index"])
|
||||
for data_type, field, results in (
|
||||
("FLOAT_VECTOR", center_field, centers),
|
||||
("FLOAT", axes.outputs["Z"], depths),
|
||||
):
|
||||
sample = nodes.new("GeometryNodeFieldAtIndex")
|
||||
sample.data_type = data_type
|
||||
sample.domain = "FACE"
|
||||
links.new(face.outputs["Face Index"], sample.inputs["Index"])
|
||||
links.new(field, sample.inputs["Value"])
|
||||
results.append(sample.outputs["Value"])
|
||||
distance = nodes.new("ShaderNodeVectorMath")
|
||||
distance.operation = "DISTANCE"
|
||||
for index, center in enumerate(centers):
|
||||
links.new(center, distance.inputs[index])
|
||||
delta = _math(group, "ABSOLUTE", _math(group, "SUBTRACT", *depths))
|
||||
scaled = _math(group, "MULTIPLY", delta, uniform)
|
||||
scaled = _math(group, "MULTIPLY", scaled, _math(group, "ABSOLUTE", depth_scale))
|
||||
# Cross multiplication avoids division and stays invariant under uniform scaling.
|
||||
selected = _math(
|
||||
group,
|
||||
"GREATER_THAN",
|
||||
_math(group, "MULTIPLY", scaled, strength),
|
||||
_math(group, "MULTIPLY", distance.outputs["Value"], 2.5),
|
||||
)
|
||||
selected = _math(
|
||||
group,
|
||||
"MULTIPLY",
|
||||
selected,
|
||||
_math(group, "GREATER_THAN", distance.outputs["Value"], 0.0),
|
||||
)
|
||||
neighbors = nodes.new("GeometryNodeInputMeshEdgeNeighbors")
|
||||
selected = _math(
|
||||
group,
|
||||
"MULTIPLY",
|
||||
selected,
|
||||
_math(group, "COMPARE", neighbors.outputs["Face Count"], 2.0),
|
||||
)
|
||||
cut_edges = nodes.new("GeometryNodeFieldOnDomain")
|
||||
cut_edges.data_type = "FLOAT"
|
||||
cut_edges.domain = "EDGE"
|
||||
links.new(selected, cut_edges.inputs["Value"])
|
||||
cut_vertices = nodes.new("GeometryNodeFieldOnDomain")
|
||||
cut_vertices.data_type = "FLOAT"
|
||||
cut_vertices.domain = "POINT"
|
||||
links.new(cut_edges.outputs["Value"], cut_vertices.inputs["Value"])
|
||||
corners = _store(group, faces, "corner_camera", _read(group, "face_camera"))
|
||||
corners = _store(
|
||||
group,
|
||||
corners,
|
||||
"cut_vertex",
|
||||
_math(group, "GREATER_THAN", cut_vertices.outputs["Value"], 0.0),
|
||||
)
|
||||
split = nodes.new("GeometryNodeSplitEdges")
|
||||
links.new(corners, split.inputs["Mesh"])
|
||||
links.new(selected, split.inputs["Selection"])
|
||||
enabled = _math(group, "GREATER_THAN", strength, 0.0)
|
||||
choose = nodes.new("GeometryNodeSwitch")
|
||||
choose.input_type = "GEOMETRY"
|
||||
links.new(enabled, choose.inputs["Switch"])
|
||||
links.new(geometry, choose.inputs["False"])
|
||||
links.new(split.outputs["Mesh"], choose.inputs["True"])
|
||||
return choose.outputs["Output"]
|
||||
|
||||
|
||||
def store_surface_camera(group, geometry, original, strength):
|
||||
"""Change cut depth along the original ray, preserving every other vertex."""
|
||||
separated = _read(group, "corner_camera")
|
||||
enabled = _math(
|
||||
group,
|
||||
"MULTIPLY",
|
||||
_math(group, "GREATER_THAN", strength, 0.0),
|
||||
_read(group, "cut_vertex"),
|
||||
)
|
||||
original_axes = group.nodes.new("ShaderNodeSeparateXYZ")
|
||||
separated_axes = group.nodes.new("ShaderNodeSeparateXYZ")
|
||||
group.links.new(original, original_axes.inputs[0])
|
||||
group.links.new(separated, separated_axes.inputs[0])
|
||||
ratio = _math(
|
||||
group, "DIVIDE", separated_axes.outputs["Z"], original_axes.outputs["Z"]
|
||||
)
|
||||
projected = group.nodes.new("ShaderNodeVectorMath")
|
||||
projected.operation = "SCALE"
|
||||
group.links.new(original, projected.inputs[0])
|
||||
group.links.new(ratio, projected.inputs[3])
|
||||
choose = group.nodes.new("GeometryNodeSwitch")
|
||||
choose.input_type = "VECTOR"
|
||||
group.links.new(enabled, choose.inputs["Switch"])
|
||||
group.links.new(original, choose.inputs["False"])
|
||||
group.links.new(projected.outputs[0], choose.inputs["True"])
|
||||
geometry = _store(group, geometry, "camera", choose.outputs["Output"])
|
||||
return geometry, _read(group, "camera")
|
||||
|
||||
|
||||
def thicken_depth_surface(group, geometry, thickness):
|
||||
"""Connect caps and wall rings by source vertex and layer identity."""
|
||||
nodes, links = group.nodes, group.links
|
||||
position = nodes.new("GeometryNodeInputPosition")
|
||||
geometry = _store(group, geometry, "surface_position", position.outputs[0])
|
||||
flattened = nodes.new("ShaderNodeVectorMath")
|
||||
flattened.operation = "MULTIPLY"
|
||||
flattened.inputs[1].default_value = (1.0, 1.0, 0.0)
|
||||
links.new(position.outputs[0], flattened.inputs[0])
|
||||
sheet = nodes.new("GeometryNodeSetPosition")
|
||||
links.new(geometry, sheet.inputs["Geometry"])
|
||||
links.new(flattened.outputs[0], sheet.inputs["Position"])
|
||||
index = nodes.new("GeometryNodeInputIndex")
|
||||
geometry = _store(
|
||||
group, sheet.outputs["Geometry"], "source_vertex", index.outputs[0]
|
||||
)
|
||||
edge = nodes.new("GeometryNodeInputMeshEdgeVertices")
|
||||
length = nodes.new("ShaderNodeVectorMath")
|
||||
length.operation = "DISTANCE"
|
||||
links.new(edge.outputs["Position 1"], length.inputs[0])
|
||||
links.new(edge.outputs["Position 2"], length.inputs[1])
|
||||
neighbors = nodes.new("GeometryNodeInputMeshEdgeNeighbors")
|
||||
statistics = nodes.new("GeometryNodeAttributeStatistic")
|
||||
statistics.domain = "EDGE"
|
||||
statistics.data_type = "FLOAT"
|
||||
links.new(geometry, statistics.inputs["Geometry"])
|
||||
links.new(
|
||||
_math(group, "COMPARE", neighbors.outputs["Face Count"], 1.0),
|
||||
statistics.inputs["Selection"],
|
||||
)
|
||||
links.new(length.outputs["Value"], statistics.inputs["Attribute"])
|
||||
count = _math(
|
||||
group,
|
||||
"CEIL",
|
||||
_math(
|
||||
group,
|
||||
"DIVIDE",
|
||||
thickness,
|
||||
_math(group, "MAXIMUM", statistics.outputs["Median"], 1e-6),
|
||||
),
|
||||
)
|
||||
count = _math(group, "MINIMUM", _math(group, "MAXIMUM", count, 1.0), 128.0)
|
||||
saved = _store(group, geometry, "wall_layers", count)
|
||||
|
||||
# Weld in an integer identity grid, never in scene coordinates. Nearby cuts
|
||||
# retain distinct IDs; copies of one rim vertex share the same ID and layer.
|
||||
identity = nodes.new("ShaderNodeCombineXYZ")
|
||||
for operation, axis in (("MODULO", "X"), ("DIVIDE", "Y")):
|
||||
part = nodes.new("FunctionNodeIntegerMath")
|
||||
part.operation = operation
|
||||
part.inputs[1].default_value = 4096
|
||||
links.new(_read(group, "source_vertex"), part.inputs[0])
|
||||
links.new(part.outputs[0], identity.inputs[axis])
|
||||
|
||||
repeat_output = nodes.new("GeometryNodeRepeatOutput")
|
||||
repeat_input = nodes.new("GeometryNodeRepeatInput")
|
||||
repeat_input.pair_with_output(repeat_output)
|
||||
repeat_output.repeat_items.clear()
|
||||
repeat_output.repeat_items.new("GEOMETRY", "Geometry")
|
||||
repeat_output.repeat_items.new("GEOMETRY", "Sides")
|
||||
links.new(count, repeat_input.inputs["Iterations"])
|
||||
links.new(saved, repeat_input.inputs["Geometry"])
|
||||
extrude = nodes.new("GeometryNodeExtrudeMesh")
|
||||
extrude.mode = "FACES"
|
||||
extrude.inputs["Individual"].default_value = False
|
||||
extrusion_offset = nodes.new("ShaderNodeCombineXYZ")
|
||||
extrusion_offset.inputs["Z"].default_value = -1.0
|
||||
links.new(extrusion_offset.outputs[0], extrude.inputs["Offset"])
|
||||
links.new(repeat_input.outputs["Geometry"], extrude.inputs["Mesh"])
|
||||
sides = nodes.new("GeometryNodeSeparateGeometry")
|
||||
sides.domain = "FACE"
|
||||
links.new(extrude.outputs["Mesh"], sides.inputs["Geometry"])
|
||||
links.new(extrude.outputs["Side"], sides.inputs["Selection"])
|
||||
join_sides = nodes.new("GeometryNodeJoinGeometry")
|
||||
links.new(repeat_input.outputs["Sides"], join_sides.inputs["Geometry"])
|
||||
links.new(sides.outputs["Selection"], join_sides.inputs["Geometry"])
|
||||
links.new(sides.outputs["Inverted"], repeat_output.inputs["Geometry"])
|
||||
links.new(join_sides.outputs["Geometry"], repeat_output.inputs["Sides"])
|
||||
offset = nodes.new("ShaderNodeCombineXYZ")
|
||||
links.new(
|
||||
_math(group, "MULTIPLY", _read(group, "wall_layers"), -1.0), offset.inputs["Z"]
|
||||
)
|
||||
back = nodes.new("GeometryNodeSetPosition")
|
||||
links.new(saved, back.inputs["Geometry"])
|
||||
links.new(offset.outputs[0], back.inputs["Offset"])
|
||||
flip_back = nodes.new("GeometryNodeFlipFaces")
|
||||
links.new(back.outputs["Geometry"], flip_back.inputs["Mesh"])
|
||||
flip_sides = nodes.new("GeometryNodeFlipFaces")
|
||||
links.new(repeat_output.outputs["Sides"], flip_sides.inputs["Mesh"])
|
||||
join = nodes.new("GeometryNodeJoinGeometry")
|
||||
for value in (
|
||||
saved,
|
||||
flip_back.outputs["Mesh"],
|
||||
flip_sides.outputs["Mesh"],
|
||||
):
|
||||
links.new(value, join.inputs["Geometry"])
|
||||
encoded = nodes.new("GeometryNodeSetPosition")
|
||||
links.new(join.outputs["Geometry"], encoded.inputs["Geometry"])
|
||||
links.new(identity.outputs[0], encoded.inputs["Position"])
|
||||
merge = nodes.new("GeometryNodeMergeByDistance")
|
||||
merge.mode = "ALL"
|
||||
merge.inputs["Distance"].default_value = 0.1
|
||||
links.new(encoded.outputs["Geometry"], merge.inputs["Geometry"])
|
||||
displacement = nodes.new("ShaderNodeCombineXYZ")
|
||||
step = _math(group, "DIVIDE", thickness, _read(group, "wall_layers"))
|
||||
layer_position = nodes.new("GeometryNodeInputPosition")
|
||||
layer_axes = nodes.new("ShaderNodeSeparateXYZ")
|
||||
links.new(layer_position.outputs[0], layer_axes.inputs[0])
|
||||
links.new(
|
||||
_math(group, "MULTIPLY", layer_axes.outputs["Z"], step),
|
||||
displacement.inputs["Z"],
|
||||
)
|
||||
restored = nodes.new("ShaderNodeVectorMath")
|
||||
restored.operation = "ADD"
|
||||
links.new(_read(group, "surface_position"), restored.inputs[0])
|
||||
links.new(displacement.outputs[0], restored.inputs[1])
|
||||
result = nodes.new("GeometryNodeSetPosition")
|
||||
links.new(merge.outputs["Geometry"], result.inputs["Geometry"])
|
||||
links.new(restored.outputs[0], result.inputs["Position"])
|
||||
local_position = nodes.new("GeometryNodeInputPosition")
|
||||
local_axes = nodes.new("ShaderNodeSeparateXYZ")
|
||||
links.new(local_position.outputs[0], local_axes.inputs[0])
|
||||
links.new(local_axes.outputs["Z"], identity.inputs["Z"])
|
||||
return result.outputs["Geometry"]
|
||||
209
tools/node_assets/cutout_depth_surface_layout.py
Normal file
209
tools/node_assets/cutout_depth_surface_layout.py
Normal file
@@ -0,0 +1,209 @@
|
||||
"""Arrange staggered calculation branches on both sides of the geometry path."""
|
||||
|
||||
from node_layout import arrange_nodes
|
||||
|
||||
# Main geometry advances left to right. Local fields grow to either side of it;
|
||||
# a long calculation may start before the geometry input or span several stages.
|
||||
POSITIONS = {
|
||||
"Position": (0, 260),
|
||||
"Store Named Attribute": (0, 0),
|
||||
"Store Named Attribute.001": (260, 0),
|
||||
"Store Named Attribute.002": (520, 0),
|
||||
"Store Named Attribute.003": (780, 0),
|
||||
"Split Edges": (1060, 0),
|
||||
"Switch": (1360, 0),
|
||||
# Face sampling stays near its stores; selection forms a dense field cluster.
|
||||
"Named Attribute": (-400, -320),
|
||||
"Evaluate on Domain": (-180, -320),
|
||||
"Image Texture": (20, -320),
|
||||
"Named Attribute.003": (260, 260),
|
||||
"Named Attribute.001": (-1090, 1060),
|
||||
"Separate XYZ": (-870, 1060),
|
||||
"Named Attribute.002": (-390, 590),
|
||||
"Corners of Edge": (-1090, 910),
|
||||
"Face of Corner": (-910, 910),
|
||||
"Corners of Edge.001": (-1090, 770),
|
||||
"Face of Corner.001": (-910, 770),
|
||||
"Evaluate at Index": (-170, 750),
|
||||
"Evaluate at Index.002": (-170, 610),
|
||||
"Evaluate at Index.001": (-710, 1060),
|
||||
"Evaluate at Index.003": (-710, 920),
|
||||
"Vector Math": (10, 710),
|
||||
"Math": (-530, 1040),
|
||||
"Math.001": (-350, 1040),
|
||||
"Math.002": (-170, 1040),
|
||||
"Math.003": (-350, 830),
|
||||
"Math.004": (10, 1040),
|
||||
"Math.005": (190, 1040),
|
||||
"Math.006": (190, 790),
|
||||
"Math.007": (370, 980),
|
||||
"Math.008": (190, 620),
|
||||
"Math.009": (550, 910),
|
||||
"Edge Neighbors": (370, 590),
|
||||
"Math.010": (550, 590),
|
||||
"Math.011": (730, 810),
|
||||
"Evaluate on Domain.001": (730, 670),
|
||||
"Evaluate on Domain.002": (910, 670),
|
||||
"Math.012": (1090, 670),
|
||||
"Math.013": (1360, 280),
|
||||
# Cleanup is a separate compact cluster before projection.
|
||||
"Face Area": (1460, 780),
|
||||
"Mesh Island": (1460, 580),
|
||||
"Accumulate Field": (1680, 780),
|
||||
"Math.014": (1900, 580),
|
||||
"Delete Geometry": (2120, 0),
|
||||
# Camera-ray reconstruction runs below; scene projection runs above.
|
||||
"Named Attribute.004": (1290, -440),
|
||||
"Image Texture.001": (1510, -440),
|
||||
"Separate XYZ.001": (1740, -440),
|
||||
"Named Attribute.005": (1510, -710),
|
||||
"Separate XYZ.002": (1740, -710),
|
||||
"Math.017": (1960, -440),
|
||||
"Vector Math.001": (2180, -440),
|
||||
"Named Attribute.006": (1740, -940),
|
||||
"Math.015": (1510, -940),
|
||||
"Math.016": (1960, -940),
|
||||
"Switch.001": (2600, -240),
|
||||
"Store Named Attribute.004": (2920, 0),
|
||||
"Named Attribute.007": (2340, 1080),
|
||||
"Vector Math.002": (2560, 1080),
|
||||
"Separate XYZ.003": (2740, 1080),
|
||||
"Math.018": (3100, 730),
|
||||
"Math.019": (3280, 730),
|
||||
"Math.020": (2740, 910),
|
||||
"Combine XYZ": (2920, 1040),
|
||||
"Position.001": (2920, 790),
|
||||
"Mix": (3100, 950),
|
||||
"Separate XYZ.004": (3280, 950),
|
||||
"Combine XYZ.001": (3460, 850),
|
||||
"Set Position": (3870, 0),
|
||||
# Short preparation fields sit above the axis; the long wall-count branch
|
||||
# sits below, reading geometry back from the source-vertex store.
|
||||
"Position.002": (4560, 340),
|
||||
"Store Named Attribute.005": (4560, 0),
|
||||
"Vector Math.003": (4780, 340),
|
||||
"Set Position.001": (4780, 0),
|
||||
"Index": (5010, 340),
|
||||
"Store Named Attribute.006": (5010, 0),
|
||||
"Edge Vertices": (3470, -610),
|
||||
"Vector Math.004": (3700, -610),
|
||||
"Edge Neighbors.001": (3470, -850),
|
||||
"Math.021": (3700, -850),
|
||||
"Attribute Statistic": (3920, -610),
|
||||
"Math.022": (4150, -610),
|
||||
"Math.023": (4370, -610),
|
||||
"Math.024": (4590, -610),
|
||||
"Math.025": (4820, -610),
|
||||
"Math.026": (5040, -610),
|
||||
"Store Named Attribute.007": (5310, 0),
|
||||
"Repeat Input": (5620, 0),
|
||||
"Extrude Mesh": (5850, 0),
|
||||
"Combine XYZ.003": (5620, -340),
|
||||
"Separate Geometry": (6070, 0),
|
||||
"Join Geometry": (6300, -180),
|
||||
"Repeat Output": (6520, 0),
|
||||
"Flip Faces.001": (6740, 0),
|
||||
"Join Geometry.001": (6970, 0),
|
||||
"Named Attribute.010": (5620, -530),
|
||||
"Math.027": (5850, -530),
|
||||
"Combine XYZ.004": (6070, -530),
|
||||
"Set Position.002": (6300, -320),
|
||||
"Flip Faces": (6740, -320),
|
||||
# Identity decoding above and position restoration below have independent,
|
||||
# horizontally spread readers rather than equally tall vertical stacks.
|
||||
"Named Attribute.008": (6240, 880),
|
||||
"Integer Math": (6470, 880),
|
||||
"Named Attribute.009": (6240, 650),
|
||||
"Integer Math.001": (6470, 650),
|
||||
"Position.004": (6240, 410),
|
||||
"Separate XYZ.006": (6470, 410),
|
||||
"Combine XYZ.002": (6670, 650),
|
||||
"Set Position.003": (7410, 0),
|
||||
"Merge by Distance": (7640, 0),
|
||||
"Position.003": (6910, -950),
|
||||
"Separate XYZ.005": (7160, -950),
|
||||
"Named Attribute.011": (6900, -710),
|
||||
"Math.028": (7140, -580),
|
||||
"Math.029": (7380, -750),
|
||||
"Combine XYZ.005": (7600, -680),
|
||||
"Named Attribute.012": (7600, -370),
|
||||
"Vector Math.005": (7830, -340),
|
||||
"Set Position.004": (8080, 0),
|
||||
"Math.030": (8310, 260),
|
||||
"Switch.002": (8310, 0),
|
||||
"Remove Named Attribute": (9000, 0),
|
||||
"Set Shade Smooth": (9220, 0),
|
||||
"Group Output": (9440, 0),
|
||||
}
|
||||
|
||||
INPUT_REGIONS = (
|
||||
(("Store Named Attribute",), (-240, 0)),
|
||||
(("Switch",), (1100, -300)),
|
||||
(("Image Texture",), (-180, -190)),
|
||||
(("Math.002",), (-350, 1140)),
|
||||
(("Math.003",), (-530, 830)),
|
||||
(("Math.005",), (10, 1140)),
|
||||
(("Math.013",), (1100, 320)),
|
||||
(("Math.014",), (1680, 400)),
|
||||
(("Image Texture.001",), (1290, -270)),
|
||||
(("Math.015",), (1290, -940)),
|
||||
(("Vector Math.002",), (2340, 1200)),
|
||||
(("Math.018", "Math.019"), (2920, 680)),
|
||||
(("Mix",), (2920, 900)),
|
||||
(("Math.023",), (4150, -820)),
|
||||
(("Math.028",), (6900, -540)),
|
||||
(("Math.030",), (8080, 300)),
|
||||
)
|
||||
|
||||
|
||||
def _route(group, source_name, output, target_name, input_name, points):
|
||||
"""Place geometry references around the local branches they serve."""
|
||||
source = group.nodes[source_name].outputs[output]
|
||||
target = group.nodes[target_name].inputs[input_name]
|
||||
for link in list(group.links):
|
||||
if link.from_socket == source and link.to_socket == target:
|
||||
group.links.remove(link)
|
||||
for point in points:
|
||||
node = group.nodes.new("NodeReroute")
|
||||
node.location = point
|
||||
node.select = False
|
||||
group.links.new(source, node.inputs[0])
|
||||
source = node.outputs[0]
|
||||
group.links.new(source, target)
|
||||
|
||||
|
||||
def layout_depth_surface(group):
|
||||
"""Keep the geometry path readable while staggering long and short branches."""
|
||||
arrange_nodes(group, POSITIONS, INPUT_REGIONS)
|
||||
_route(
|
||||
group,
|
||||
"Repeat Input",
|
||||
"Sides",
|
||||
"Join Geometry",
|
||||
"Geometry",
|
||||
((5810, -170), (6280, -170)),
|
||||
)
|
||||
_route(
|
||||
group,
|
||||
"Store Named Attribute.007",
|
||||
"Geometry",
|
||||
"Join Geometry.001",
|
||||
"Geometry",
|
||||
((5530, 150), (6950, 150)),
|
||||
)
|
||||
_route(
|
||||
group,
|
||||
"Store Named Attribute.007",
|
||||
"Geometry",
|
||||
"Set Position.002",
|
||||
"Geometry",
|
||||
((5530, -270), (6280, -270)),
|
||||
)
|
||||
_route(
|
||||
group,
|
||||
"Set Position",
|
||||
"Geometry",
|
||||
"Switch.002",
|
||||
"True",
|
||||
((4110, 240), (8290, 240)),
|
||||
)
|
||||
339
tools/node_assets/geometry_layouts.py
Normal file
339
tools/node_assets/geometry_layouts.py
Normal file
@@ -0,0 +1,339 @@
|
||||
"""Authored processing stages for the shared image geometry assets."""
|
||||
|
||||
from node_layout import arrange_nodes
|
||||
|
||||
LAYOUTS = {
|
||||
"O Mesh Plane": (
|
||||
{
|
||||
"Bounding Box": (-2320, 440),
|
||||
"Vector Math": (-2100, 650),
|
||||
"Separate XYZ": (-1870, 650),
|
||||
"Math": (-1870, 990),
|
||||
"Math.001": (-1650, 850),
|
||||
"Math.002": (-1430, 850),
|
||||
"Math.003": (-1200, 850),
|
||||
"Math.004": (-980, 850),
|
||||
"Math.005": (-760, 850),
|
||||
"Math.006": (-1650, 510),
|
||||
"Math.007": (-1430, 510),
|
||||
"Math.008": (-1200, 510),
|
||||
"Math.009": (-980, 510),
|
||||
"Math.010": (-760, 510),
|
||||
"Grid": (520, 340),
|
||||
"Position": (-950, -480),
|
||||
"Separate XYZ.001": (-720, -480),
|
||||
"Math.011": (-500, -340),
|
||||
"Math.012": (-280, -340),
|
||||
"Math.013": (-500, -610),
|
||||
"Math.014": (-280, -610),
|
||||
"Combine XYZ": (0, -340),
|
||||
"Store Named Attribute": (770, 340),
|
||||
"Vector Math.001": (740, 710),
|
||||
"Vector Math.002": (960, 710),
|
||||
"Transform Geometry": (1030, 340),
|
||||
"Bounding Box.001": (-1170, -990),
|
||||
"Vector Math.003": (-950, -990),
|
||||
"Separate XYZ.002": (-720, -990),
|
||||
"Bounding Box.002": (-330, 140),
|
||||
"Vector Math.004": (-100, 140),
|
||||
"Separate XYZ.003": (120, 270),
|
||||
"Math.015": (-520, 1390),
|
||||
"Math.016": (-290, 1390),
|
||||
"Math.017": (-520, 1160),
|
||||
"Math.018": (-290, 1160),
|
||||
"Math.019": (-70, 1390),
|
||||
"Math.020": (150, 1390),
|
||||
"Math.021": (380, 1390),
|
||||
"Math.022": (600, 1390),
|
||||
"Math.023": (830, 1390),
|
||||
"Math.024": (1050, 1390),
|
||||
"Combine XYZ.001": (280, 0),
|
||||
"Cube": (520, -170),
|
||||
"Store Named Attribute.001": (770, -170),
|
||||
"Transform Geometry.001": (1030, -170),
|
||||
"Compare": (1320, 410),
|
||||
"Switch": (1360, 0),
|
||||
"Capture Attribute": (1320, -580),
|
||||
"Join Geometry": (1720, 0),
|
||||
"Set Material Index": (1940, 0),
|
||||
"Delete Geometry": (2170, 0),
|
||||
"Group Output": (2390, 0),
|
||||
},
|
||||
(
|
||||
(("Bounding Box",), (-2550, 440)),
|
||||
(("Math",), (-2100, 1020)),
|
||||
(("Math.021",), (150, 1600)),
|
||||
(("Combine XYZ.001",), (120, 60)),
|
||||
(("Compare",), (1140, 540)),
|
||||
(("Capture Attribute",), (1140, -580)),
|
||||
(("Bounding Box.001",), (-1390, -990)),
|
||||
(("Bounding Box.002",), (-550, 140)),
|
||||
),
|
||||
),
|
||||
"O Image Cutout": (
|
||||
{
|
||||
"Math": (-170, 580),
|
||||
"Mesh Island": (-620, 140),
|
||||
"Face Area": (-620, 340),
|
||||
"Accumulate Field": (-400, 340),
|
||||
"Math.001": (50, 310),
|
||||
"Delete Geometry": (430, 270),
|
||||
"Group": (1030, 520),
|
||||
"Group.001": (1030, 300),
|
||||
"Group.002": (1030, 80),
|
||||
"Group.003": (1030, -220),
|
||||
"Menu Switch": (1390, 240),
|
||||
"Repeat Input": (2170, 0),
|
||||
"Repeat Output": (5090, 0),
|
||||
"Vertex Neighbors": (2170, -780),
|
||||
"Compare": (2390, -780),
|
||||
"Edge Neighbors": (1940, -1020),
|
||||
"Compare.001": (2170, -1020),
|
||||
"Evaluate on Domain": (2390, -1020),
|
||||
"Boolean Math": (2610, -780),
|
||||
"Evaluate on Domain.001": (2840, -580),
|
||||
"Separate Geometry": (3110, -310),
|
||||
"Position": (3300, 540),
|
||||
"Blur Attribute": (3520, 540),
|
||||
"Position.001": (3110, -780),
|
||||
"Blur Attribute.001": (3340, -780),
|
||||
"Sample Nearest": (3340, -310),
|
||||
"Sample Index": (3590, -310),
|
||||
"Vertex Neighbors.001": (2390, 950),
|
||||
"Compare.002": (2610, 950),
|
||||
"Edge Neighbors.001": (2170, 1220),
|
||||
"Compare.003": (2390, 1220),
|
||||
"Evaluate on Domain.002": (2610, 1220),
|
||||
"Boolean Math.001": (2840, 950),
|
||||
"Switch": (3890, 340),
|
||||
"Mix": (4130, 340),
|
||||
"Normal": (3060, 1500),
|
||||
"Blur Attribute.002": (3280, 1500),
|
||||
"Vector Math": (3510, 1500),
|
||||
"Switch.001": (3730, 1500),
|
||||
"Math.002": (3950, 1360),
|
||||
"Switch.002": (4370, 410),
|
||||
"Position.002": (4370, 680),
|
||||
"Mix.001": (4660, 410),
|
||||
"Set Position": (4830, 0),
|
||||
"Transform Geometry": (5450, -340),
|
||||
"Menu Switch.001": (5740, 0),
|
||||
"Set Shade Smooth": (5970, 0),
|
||||
"Group Output": (6190, 0),
|
||||
},
|
||||
(
|
||||
(("Math",), (-400, 580)),
|
||||
(("Delete Geometry",), (430, 530)),
|
||||
(("Group",), (810, 520)),
|
||||
(("Group.001",), (810, 300)),
|
||||
(("Group.002",), (810, 80)),
|
||||
(("Group.003",), (810, -220)),
|
||||
(("Menu Switch",), (1390, 380)),
|
||||
(("Repeat Input",), (1940, 0)),
|
||||
(("Math.002",), (3730, 1220)),
|
||||
(("Menu Switch.001",), (5520, 340)),
|
||||
),
|
||||
),
|
||||
"O Image Depth Plane": (
|
||||
{
|
||||
"Group": (0, 0),
|
||||
"Transform Geometry": (520, 0),
|
||||
"Set Position": (2470, 0),
|
||||
"Set Shade Smooth": (2730, 0),
|
||||
"Group Output": (2990, 0),
|
||||
"Math": (60, 340),
|
||||
"Combine XYZ": (240, 340),
|
||||
"Named Attribute": (240, -450),
|
||||
"Image Texture": (460, -450),
|
||||
"Separate XYZ": (640, -450),
|
||||
"Math.001": (820, -450),
|
||||
"Math.002": (1000, -450),
|
||||
"Math.003": (1180, -450),
|
||||
"Math.004": (1180, -660),
|
||||
"Math.005": (1380, -450),
|
||||
"Math.006": (1740, -1060),
|
||||
"Switch": (1760, -450),
|
||||
"Position": (820, -820),
|
||||
"Separate XYZ.001": (1000, -820),
|
||||
"Math.007": (1180, -820),
|
||||
"Math.008": (1380, -820),
|
||||
"Math.009": (1560, -820),
|
||||
"Switch.001": (1760, -820),
|
||||
"Math.010": (1970, -520),
|
||||
"Combine XYZ.001": (2150, -520),
|
||||
},
|
||||
(
|
||||
(("Group",), (-240, 0)),
|
||||
(("Math",), (-120, 340)),
|
||||
(("Image Texture",), (240, -310)),
|
||||
(("Math.001",), (640, -310)),
|
||||
(("Math.002", "Math.003"), (820, -590)),
|
||||
(("Math.004",), (1000, -660)),
|
||||
(("Math.006",), (1560, -1060)),
|
||||
(("Math.007",), (1000, -980)),
|
||||
),
|
||||
),
|
||||
"O Image Balloon": (
|
||||
{
|
||||
"Named Attribute": (-950, 440),
|
||||
"Attribute Statistic": (-730, 1020),
|
||||
"Math": (-500, 1020),
|
||||
"Math.001": (-280, 1020),
|
||||
"Map Range": (-50, 1020),
|
||||
"Math.002": (-500, 440),
|
||||
"Math.003": (-500, 240),
|
||||
"Math.004": (-280, 240),
|
||||
"Extrude Mesh": (0, 0),
|
||||
"Switch": (220, 440),
|
||||
"Position": (20, 240),
|
||||
"Separate XYZ": (220, 240),
|
||||
"Combine XYZ": (420, 340),
|
||||
"Set Position": (690, 0),
|
||||
"Separate Geometry": (910, 0),
|
||||
"Flip Faces": (1140, 0),
|
||||
"Combine XYZ.001": (910, 530),
|
||||
"Set Position.001": (1140, 300),
|
||||
"Combine XYZ.002": (690, -550),
|
||||
"Set Position.002": (910, -300),
|
||||
"Flip Faces.001": (1140, -300),
|
||||
"Join Geometry": (1430, 0),
|
||||
"Merge by Distance": (1650, 0),
|
||||
"Math.005": (2100, 310),
|
||||
"Switch.001": (2100, 0),
|
||||
"Set Shade Smooth": (2320, 0),
|
||||
"Group Output": (2550, 0),
|
||||
},
|
||||
(
|
||||
(("Extrude Mesh",), (-220, 0)),
|
||||
(("Attribute Statistic",), (-950, 1020)),
|
||||
(("Math.002", "Math.003"), (-730, 240)),
|
||||
(("Set Position.001",), (910, 300)),
|
||||
(("Set Position.002",), (690, -300)),
|
||||
(("Math.005",), (1870, 340)),
|
||||
(("Switch.001",), (1870, -240)),
|
||||
),
|
||||
),
|
||||
"O Image Surface": (
|
||||
{
|
||||
"Math": (-90, 680),
|
||||
"Math.001": (140, 680),
|
||||
"Edge Vertices": (-1630, -340),
|
||||
"Vector Math": (-1410, -340),
|
||||
"Edge Neighbors": (-1630, -560),
|
||||
"Math.002": (-1410, -560),
|
||||
"Attribute Statistic": (-1190, -340),
|
||||
"Math.003": (-960, -340),
|
||||
"Math.004": (-740, -340),
|
||||
"Float to Integer": (-520, -340),
|
||||
"Integer Math": (-290, -340),
|
||||
"Integer Math.001": (-70, -340),
|
||||
"Math.005": (190, -580),
|
||||
"Math.006": (410, -580),
|
||||
"Combine XYZ": (1630, 480),
|
||||
"Set Position": (1860, 260),
|
||||
"Repeat Input": (410, 0),
|
||||
"Extrude Mesh": (640, 0),
|
||||
"Separate Geometry": (860, 0),
|
||||
"Join Geometry": (1080, -180),
|
||||
"Repeat Output": (1310, 0),
|
||||
"Combine XYZ.001": (1410, 270),
|
||||
"Set Position.001": (1630, 0),
|
||||
"Flip Faces": (1860, 0),
|
||||
"Combine XYZ.002": (1410, -470),
|
||||
"Set Position.002": (1630, -260),
|
||||
"Flip Faces.001": (1860, -260),
|
||||
"Join Geometry.001": (2150, 0),
|
||||
"Merge by Distance": (2370, 0),
|
||||
"Math.007": (2820, 290),
|
||||
"Switch": (2820, 0),
|
||||
"Set Shade Smooth": (3040, 0),
|
||||
"Group Output": (3270, 0),
|
||||
},
|
||||
(
|
||||
(("Math",), (-310, 680)),
|
||||
(("Attribute Statistic",), (-1410, -170)),
|
||||
(("Math.004",), (-960, -610)),
|
||||
(("Math.005",), (-30, -580)),
|
||||
(("Repeat Input",), (190, 0)),
|
||||
(("Set Position",), (1630, 260)),
|
||||
(("Set Position.002",), (1410, -260)),
|
||||
(("Math.007",), (2600, 310)),
|
||||
(("Switch",), (2600, -240)),
|
||||
),
|
||||
),
|
||||
"O Image Depth Balloon": (
|
||||
{
|
||||
"Named Attribute": (-1120, 650),
|
||||
"Attribute Statistic": (-890, 650),
|
||||
"Math": (-670, 650),
|
||||
"Math.001": (-450, 650),
|
||||
"Map Range": (-220, 650),
|
||||
"Named Attribute.001": (-1790, -440),
|
||||
"Image Texture": (-1570, -440),
|
||||
"Separate XYZ": (-1340, -440),
|
||||
"Math.002": (-1120, -440),
|
||||
"Vector Math": (-2240, -950),
|
||||
"Separate XYZ.001": (-2010, -950),
|
||||
"Position": (-2240, -1260),
|
||||
"Separate XYZ.002": (-2010, -1260),
|
||||
"Math.003": (-1790, -950),
|
||||
"Math.004": (-1790, -1160),
|
||||
"Math.005": (-1570, -950),
|
||||
"Math.006": (-1340, -950),
|
||||
"Math.007": (-1120, -950),
|
||||
"Math.008": (-890, -440),
|
||||
"Math.009": (-670, -440),
|
||||
"Math.010": (-450, -440),
|
||||
"Math.011": (-670, 310),
|
||||
"Math.012": (-670, 70),
|
||||
"Math.013": (-450, 70),
|
||||
"Mix": (20, 440),
|
||||
"Math.014": (240, 710),
|
||||
"Switch": (460, 710),
|
||||
"Extrude Mesh": (0, 0),
|
||||
"Switch.001": (460, 310),
|
||||
"Position.001": (280, 530),
|
||||
"Separate XYZ.003": (460, 530),
|
||||
"Combine XYZ": (660, 430),
|
||||
"Set Position": (930, 0),
|
||||
"Separate Geometry": (1150, 0),
|
||||
"Flip Faces": (1600, 0),
|
||||
"Combine XYZ.001": (1380, 570),
|
||||
"Set Position.001": (1600, 330),
|
||||
"Combine XYZ.002": (1150, -570),
|
||||
"Set Position.002": (1380, -320),
|
||||
"Flip Faces.001": (1600, -320),
|
||||
"Join Geometry": (1870, 0),
|
||||
"Merge by Distance": (2320, 0),
|
||||
"Position.002": (1600, 1120),
|
||||
"Separate XYZ.004": (1820, 1120),
|
||||
"Combine XYZ.003": (2050, 950),
|
||||
"Set Position.003": (2320, 680),
|
||||
"Math.015": (2560, -270),
|
||||
"Switch.002": (2560, 0),
|
||||
"Set Shade Smooth": (2790, 0),
|
||||
"Group Output": (3010, 0),
|
||||
},
|
||||
(
|
||||
(("Extrude Mesh",), (-220, 0)),
|
||||
(("Attribute Statistic",), (-1120, 850)),
|
||||
(("Image Texture",), (-1790, -270)),
|
||||
(("Math.002",), (-1340, -750)),
|
||||
(("Vector Math",), (-2460, -950)),
|
||||
(("Math.007",), (-1340, -1160)),
|
||||
(("Math.010",), (-670, -650)),
|
||||
(("Math.011", "Math.012"), (-890, 310)),
|
||||
(("Switch",), (240, 920)),
|
||||
(("Set Position.001",), (1380, 330)),
|
||||
(("Set Position.002",), (1150, -320)),
|
||||
(("Set Position.003",), (2100, 610)),
|
||||
(("Math.015",), (2340, -270)),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def layout_geometry(group):
|
||||
"""Apply the authored layout for one geometry asset."""
|
||||
positions, inputs = LAYOUTS[group.name]
|
||||
arrange_nodes(group, positions, inputs)
|
||||
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)
|
||||
@@ -4,12 +4,14 @@ from pathlib import Path
|
||||
import bpy
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parents[2]
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
sys.path.insert(0, str(PROJECT_ROOT / "src"))
|
||||
sys.path.insert(
|
||||
0,
|
||||
str(next((PROJECT_ROOT / "src" / "anyimage" / "wheels").glob("blendjob-*.whl"))),
|
||||
)
|
||||
|
||||
from node_layout import validate_node_layout
|
||||
from anyimage.common.material import (
|
||||
DEPTH_MATERIAL_NODE_GROUP_NAME,
|
||||
IMAGE_MATERIAL_NODE_GROUP_NAME,
|
||||
@@ -137,6 +139,9 @@ def main():
|
||||
cutout,
|
||||
}
|
||||
loaded = {*public_groups, *cutout_groups.values()}
|
||||
for group in loaded:
|
||||
if group.bl_idname == "GeometryNodeTree":
|
||||
validate_node_layout(group)
|
||||
assert all(group.use_fake_user for group in public_groups)
|
||||
assert all(group.users > 0 for group in cutout_groups.values())
|
||||
if bpy.app.version >= (5, 0, 0):
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import sys
|
||||
from collections import Counter
|
||||
from itertools import pairwise
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parents[2]
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
sys.path.insert(0, str(PROJECT_ROOT / "src"))
|
||||
sys.path.insert(
|
||||
0,
|
||||
str(next((PROJECT_ROOT / "src" / "anyimage" / "wheels").glob("blendjob-*.whl"))),
|
||||
)
|
||||
|
||||
from node_layout import validate_node_layout
|
||||
|
||||
from anyimage.common.object import (
|
||||
modifier_input_identifier,
|
||||
set_modifier_input,
|
||||
@@ -24,6 +29,130 @@ def evaluated_mesh(obj):
|
||||
return evaluated, result
|
||||
|
||||
|
||||
def validate_depth_surface_layout(group):
|
||||
"""Check the geometry path, stage spacing and drawn node bounds."""
|
||||
nodes = [node for node in group.nodes if node.bl_idname != "NodeReroute"]
|
||||
main = [
|
||||
group.nodes[name]
|
||||
for name in (
|
||||
"Store Named Attribute",
|
||||
"Store Named Attribute.001",
|
||||
"Store Named Attribute.002",
|
||||
"Store Named Attribute.003",
|
||||
"Split Edges",
|
||||
"Switch",
|
||||
"Delete Geometry",
|
||||
"Store Named Attribute.004",
|
||||
"Set Position",
|
||||
"Store Named Attribute.005",
|
||||
"Set Position.001",
|
||||
"Store Named Attribute.006",
|
||||
"Store Named Attribute.007",
|
||||
"Repeat Input",
|
||||
"Extrude Mesh",
|
||||
"Separate Geometry",
|
||||
"Repeat Output",
|
||||
"Flip Faces.001",
|
||||
"Join Geometry.001",
|
||||
"Set Position.003",
|
||||
"Merge by Distance",
|
||||
"Set Position.004",
|
||||
"Switch.002",
|
||||
"Remove Named Attribute",
|
||||
"Set Shade Smooth",
|
||||
"Group Output",
|
||||
)
|
||||
]
|
||||
assert len({node.location.y for node in main}) == 1
|
||||
assert all(right.location.x > left.location.x for left, right in pairwise(main))
|
||||
for left, right in (
|
||||
("Switch", "Delete Geometry"),
|
||||
("Delete Geometry", "Store Named Attribute.004"),
|
||||
("Set Position", "Store Named Attribute.005"),
|
||||
("Switch.002", "Remove Named Attribute"),
|
||||
):
|
||||
a, b = group.nodes[left], group.nodes[right]
|
||||
assert b.location.x - (a.location.x + a.width) >= 280
|
||||
assert not any(n.bl_idname == "FunctionNodeInputString" for n in nodes)
|
||||
assert min(n.location.x for n in nodes) < main[0].location.x
|
||||
|
||||
def height(node):
|
||||
if node.dimensions.y:
|
||||
return node.dimensions.y
|
||||
if node.hide or node.bl_idname == "NodeReroute":
|
||||
return 30.0
|
||||
if node.bl_idname == "GeometryNodeJoinGeometry":
|
||||
return 60.0
|
||||
return node.height
|
||||
|
||||
for index, node in enumerate(nodes):
|
||||
width = node.dimensions.x or node.width
|
||||
for other in nodes[index + 1 :]:
|
||||
other_width = other.dimensions.x or other.width
|
||||
overlap = (
|
||||
node.location.x < other.location.x + other_width
|
||||
and node.location.x + width > other.location.x
|
||||
and node.location.y - height(node) < other.location.y
|
||||
and node.location.y > other.location.y - height(other)
|
||||
)
|
||||
assert not overlap, (node.name, other.name)
|
||||
repeat_input = next(
|
||||
node for node in nodes if node.bl_idname == "GeometryNodeRepeatInput"
|
||||
)
|
||||
repeat_output = repeat_input.paired_output
|
||||
inside = {repeat_output}
|
||||
pending = [repeat_input]
|
||||
while pending:
|
||||
node = pending.pop()
|
||||
if node in inside:
|
||||
continue
|
||||
inside.add(node)
|
||||
pending.extend(link.to_node for link in group.links if link.from_node == node)
|
||||
zone_bottom = min(node.location.y - height(node) for node in inside) - 60.0
|
||||
zone_top = max(node.location.y for node in inside) + 60.0
|
||||
for node in nodes:
|
||||
if repeat_input.location.x <= node.location.x <= repeat_output.location.x:
|
||||
assert (
|
||||
node in inside
|
||||
or node.location.y <= zone_bottom
|
||||
or node.location.y - height(node) >= zone_top
|
||||
), node.name
|
||||
|
||||
|
||||
def validate_depth_surface_attributes(group):
|
||||
"""Check temporary fields are typed, locally read and removed at output."""
|
||||
expected = {
|
||||
"o_depth_face_center": ("FLOAT_VECTOR", "FACE"),
|
||||
"o_depth_face_camera": ("FLOAT_VECTOR", "FACE"),
|
||||
"o_depth_corner_camera": ("FLOAT_VECTOR", "CORNER"),
|
||||
"o_depth_cut_vertex": ("BOOLEAN", "CORNER"),
|
||||
"o_depth_camera": ("FLOAT_VECTOR", "POINT"),
|
||||
"o_depth_surface_position": ("FLOAT_VECTOR", "POINT"),
|
||||
"o_depth_source_vertex": ("INT", "POINT"),
|
||||
"o_depth_wall_layers": ("FLOAT", "POINT"),
|
||||
}
|
||||
stores = {
|
||||
n.inputs["Name"].default_value: (n.data_type, n.domain)
|
||||
for n in group.nodes
|
||||
if n.bl_idname == "GeometryNodeStoreNamedAttribute"
|
||||
}
|
||||
assert stores == expected
|
||||
assert not any(n.bl_idname == "GeometryNodeCaptureAttribute" for n in group.nodes)
|
||||
removes = [n for n in group.nodes if n.bl_idname == "GeometryNodeRemoveAttribute"]
|
||||
assert len(removes) == 1
|
||||
assert removes[0].pattern_mode == "WILDCARD"
|
||||
assert removes[0].inputs["Name"].default_value == "o_depth_*"
|
||||
for node in group.nodes:
|
||||
if node.bl_idname == "GeometryNodeInputNamedAttribute":
|
||||
name = node.inputs["Name"].default_value
|
||||
assert name == "UVMap" or node.data_type == expected[name][0]
|
||||
assert all(
|
||||
abs(link.to_node.location.x - node.location.x) <= 600
|
||||
for link in group.links
|
||||
if link.from_node == node
|
||||
)
|
||||
|
||||
|
||||
def input_names(group):
|
||||
return [
|
||||
item.name
|
||||
@@ -83,6 +212,7 @@ def main():
|
||||
shape: bpy.data.node_groups[name]
|
||||
for shape, name in shapes.CUTOUT_NODE_GROUP_NAMES.items()
|
||||
}
|
||||
validate_depth_surface_layout(groups["DEPTH_SURFACE"])
|
||||
assert all(not group.is_modifier for group in groups.values())
|
||||
|
||||
menu_switch = next(
|
||||
@@ -157,7 +287,7 @@ def main():
|
||||
"Smooth Iterations",
|
||||
"Cleanup",
|
||||
"Smooth",
|
||||
"Stretch Limit",
|
||||
"Split",
|
||||
"Reference Depth",
|
||||
"Depth Direction",
|
||||
"Uniform Scale",
|
||||
@@ -180,7 +310,8 @@ def main():
|
||||
"Geometry",
|
||||
"Thickness",
|
||||
"Depth Scale",
|
||||
"Stretch Limit",
|
||||
"Split",
|
||||
"Cleanup",
|
||||
"Reference Depth",
|
||||
"Uniform Scale",
|
||||
"Depth Image",
|
||||
@@ -200,9 +331,10 @@ def main():
|
||||
)
|
||||
dependencies.setdefault(link.from_socket.name, set()).add(shape)
|
||||
assert dependencies == {
|
||||
"Geometry": {"DEPTH_SURFACE"},
|
||||
"Thickness": set(shapes.CUTOUT_SHAPES),
|
||||
"Depth Scale": {"DEPTH_BALLOON", "DEPTH_SURFACE"},
|
||||
"Stretch Limit": {"DEPTH_SURFACE"},
|
||||
"Split": {"DEPTH_SURFACE"},
|
||||
"Double Sided": {"DEPTH_BALLOON"},
|
||||
"Reference Depth": {"DEPTH_BALLOON", "DEPTH_SURFACE"},
|
||||
"Depth Direction": {"DEPTH_BALLOON"},
|
||||
@@ -232,7 +364,8 @@ def main():
|
||||
)
|
||||
assert all(
|
||||
not any(node.bl_idname == "GeometryNodeInputMeshIsland" for node in group.nodes)
|
||||
for group in groups.values()
|
||||
for shape, group in groups.items()
|
||||
if shape != "DEPTH_SURFACE"
|
||||
)
|
||||
assert any(node.bl_idname == "GeometryNodeBlurAttribute" for node in wrapper.nodes)
|
||||
smooth = next(
|
||||
@@ -252,77 +385,47 @@ def main():
|
||||
)
|
||||
assert cleanup.default_value == 0.0
|
||||
assert cleanup.subtype == "NONE"
|
||||
stretch_limit = next(
|
||||
split_control = next(
|
||||
item
|
||||
for item in wrapper.interface.items_tree
|
||||
if item.item_type == "SOCKET" and item.name == "Stretch Limit"
|
||||
if item.item_type == "SOCKET" and item.name == "Split"
|
||||
)
|
||||
child_stretch_limit = next(
|
||||
child_split = next(
|
||||
item
|
||||
for item in groups["DEPTH_SURFACE"].interface.items_tree
|
||||
if item.item_type == "SOCKET" and item.name == "Stretch Limit"
|
||||
if item.item_type == "SOCKET" and item.name == "Split"
|
||||
)
|
||||
assert stretch_limit.default_value == 0.0
|
||||
assert stretch_limit.min_value == 0.0
|
||||
assert stretch_limit.max_value == 1.0
|
||||
assert stretch_limit.subtype == "FACTOR"
|
||||
assert getattr(stretch_limit.parent, "name", "") == "Options"
|
||||
assert child_stretch_limit.default_value == 0.0
|
||||
assert child_stretch_limit.min_value == 0.0
|
||||
assert child_stretch_limit.max_value == 1.0
|
||||
assert child_stretch_limit.subtype == "FACTOR"
|
||||
assert getattr(child_stretch_limit.parent, "name", "") == "Options"
|
||||
assert split_control.default_value == 0.0
|
||||
assert split_control.min_value == 0.0
|
||||
assert split_control.max_value == 1.0
|
||||
assert split_control.subtype == "FACTOR"
|
||||
assert getattr(split_control.parent, "name", "") == "Options"
|
||||
assert child_split.default_value == 0.0
|
||||
assert child_split.min_value == 0.0
|
||||
assert child_split.max_value == 1.0
|
||||
assert child_split.subtype == "FACTOR"
|
||||
assert getattr(child_split.parent, "name", "") == "Options"
|
||||
depth_surface_nodes = groups["DEPTH_SURFACE"].nodes
|
||||
captures = [
|
||||
node
|
||||
for node in depth_surface_nodes
|
||||
if node.bl_idname == "GeometryNodeCaptureAttribute"
|
||||
]
|
||||
assert len(captures) == 1
|
||||
assert captures[0].domain == "POINT"
|
||||
assert [item.name for item in captures[0].capture_items] == [
|
||||
"Original Edge Length"
|
||||
]
|
||||
captured_distance = next(
|
||||
link.from_node
|
||||
for link in groups["DEPTH_SURFACE"].links
|
||||
if link.to_node == captures[0]
|
||||
and link.to_socket.name == "Original Edge Length"
|
||||
)
|
||||
assert captured_distance.bl_idname == "ShaderNodeVectorMath"
|
||||
assert captured_distance.operation == "DISTANCE"
|
||||
ratio = next(
|
||||
link.to_node
|
||||
for link in groups["DEPTH_SURFACE"].links
|
||||
if link.from_node == captured_distance
|
||||
and link.to_node.bl_idname == "ShaderNodeMath"
|
||||
and link.to_node.operation == "DIVIDE"
|
||||
)
|
||||
assert any(
|
||||
link.from_node == captures[0]
|
||||
and link.to_node == ratio
|
||||
and link.to_socket == ratio.inputs[0]
|
||||
for link in groups["DEPTH_SURFACE"].links
|
||||
node.bl_idname == "GeometryNodeSplitEdges" for node in depth_surface_nodes
|
||||
)
|
||||
assert any(
|
||||
link.from_node == captured_distance
|
||||
and link.to_node == ratio
|
||||
and link.to_socket == ratio.inputs[1]
|
||||
for link in groups["DEPTH_SURFACE"].links
|
||||
)
|
||||
samples = [
|
||||
node
|
||||
for node in depth_surface_nodes
|
||||
if node.bl_idname == "GeometryNodeSampleIndex"
|
||||
]
|
||||
assert not samples
|
||||
delete_domains = [
|
||||
validate_depth_surface_attributes(groups["DEPTH_SURFACE"])
|
||||
assert [
|
||||
node.domain
|
||||
for node in depth_surface_nodes
|
||||
if node.bl_idname == "GeometryNodeDeleteGeometry"
|
||||
] == ["FACE"]
|
||||
assert "Stretch Limit" not in [
|
||||
item.name for item in groups["DEPTH_SURFACE"].interface.items_tree
|
||||
]
|
||||
assert delete_domains.count("EDGE") == 1
|
||||
assert delete_domains.count("POINT") == 2
|
||||
assert all(
|
||||
socket.is_linked or socket.hide
|
||||
for node in depth_surface_nodes
|
||||
for socket in node.outputs
|
||||
)
|
||||
for node in depth_surface_nodes:
|
||||
if node.bl_idname == "GeometryNodeRepeatInput":
|
||||
assert node.paired_output.location.x > node.location.x
|
||||
cleanup_scale = next(
|
||||
link.to_node
|
||||
for link in wrapper.links
|
||||
@@ -342,26 +445,7 @@ def main():
|
||||
)
|
||||
|
||||
for group in (wrapper, *groups.values()):
|
||||
assert all(not node.label for node in group.nodes)
|
||||
assert all(
|
||||
link.from_node.location.x <= link.to_node.location.x for link in group.links
|
||||
)
|
||||
group_nodes = list(group.nodes)
|
||||
assert len({tuple(node.location) for node in group_nodes}) == len(group_nodes)
|
||||
for index, node in enumerate(group_nodes):
|
||||
for other in group_nodes[index + 1 :]:
|
||||
assert not (
|
||||
abs(node.location.x - other.location.x) < 150.0
|
||||
and abs(node.location.y - other.location.y) < 100.0
|
||||
), (group.name, node.name, other.name)
|
||||
for group_input in (
|
||||
node for node in group_nodes if node.bl_idname == "NodeGroupInput"
|
||||
):
|
||||
assert all(
|
||||
output.is_linked or output.hide
|
||||
for output in group_input.outputs
|
||||
if output.name
|
||||
)
|
||||
validate_node_layout(group)
|
||||
for item in group.interface.items_tree:
|
||||
if item.item_type == "SOCKET" and item.in_out == "INPUT":
|
||||
assert item.structure_type == "SINGLE"
|
||||
@@ -525,14 +609,14 @@ def main():
|
||||
|
||||
set_modifier_input(
|
||||
modifier,
|
||||
modifier_input_identifier(modifier.node_group, "Stretch Limit"),
|
||||
modifier_input_identifier(modifier.node_group, "Split"),
|
||||
0.6,
|
||||
)
|
||||
depth_surface.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
evaluated, result = evaluated_mesh(depth_surface)
|
||||
try:
|
||||
assert 0 < len(result.polygons) < 8
|
||||
assert len(result.polygons) == 8
|
||||
face_edges = {
|
||||
tuple(sorted((start, end)))
|
||||
for polygon in result.polygons
|
||||
@@ -543,9 +627,7 @@ def main():
|
||||
}
|
||||
assert all(tuple(sorted(edge.vertices)) in face_edges for edge in result.edges)
|
||||
face_vertices = {
|
||||
vertex
|
||||
for polygon in result.polygons
|
||||
for vertex in polygon.vertices
|
||||
vertex for polygon in result.polygons for vertex in polygon.vertices
|
||||
}
|
||||
assert face_vertices == set(range(len(result.vertices)))
|
||||
finally:
|
||||
@@ -565,6 +647,25 @@ def main():
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
set_modifier_input(
|
||||
modifier, modifier_input_identifier(modifier.node_group, "Depth Scale"), 1.0
|
||||
)
|
||||
set_modifier_input(
|
||||
modifier, modifier_input_identifier(modifier.node_group, "Thickness"), 0.2
|
||||
)
|
||||
depth_surface.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
evaluated, result = evaluated_mesh(depth_surface)
|
||||
try:
|
||||
edge_uses = Counter(
|
||||
tuple(sorted((a, b)))
|
||||
for face in result.polygons
|
||||
for a, b in zip(face.vertices, (*face.vertices[1:], face.vertices[0]))
|
||||
)
|
||||
assert edge_uses and set(edge_uses.values()) == {2}
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
print(
|
||||
f"Validated Blender {bpy.app.version_string}: "
|
||||
f"{wrapper.name} wrapping "
|
||||
|
||||
Reference in New Issue
Block a user