"""Build shared boundary fields and iterative smoothing.""" from .nodes import group_input, sample_field def edge_boundary_field(nodes, links): vertex_neighbors = nodes.new("GeometryNodeInputMeshVertexNeighbors") loose_vertex = nodes.new("FunctionNodeCompare") loose_vertex.data_type = "INT" loose_vertex.operation = "EQUAL" loose_vertex_a = next( socket for socket in loose_vertex.inputs if socket.identifier == "A_INT" ) loose_vertex_b = next( socket for socket in loose_vertex.inputs if socket.identifier == "B_INT" ) loose_vertex_b.default_value = 1 links.new(vertex_neighbors.outputs["Vertex Count"], loose_vertex_a) edge_neighbors = nodes.new("GeometryNodeInputMeshEdgeNeighbors") boundary_edge = nodes.new("FunctionNodeCompare") boundary_edge.data_type = "INT" boundary_edge.operation = "EQUAL" boundary_edge_a = next( socket for socket in boundary_edge.inputs if socket.identifier == "A_INT" ) boundary_edge_b = next( socket for socket in boundary_edge.inputs if socket.identifier == "B_INT" ) boundary_edge_b.default_value = 1 links.new(edge_neighbors.outputs["Face Count"], boundary_edge_a) edge_domain = nodes.new("GeometryNodeFieldOnDomain") edge_domain.data_type = "BOOLEAN" edge_domain.domain = "EDGE" links.new(boundary_edge.outputs["Result"], edge_domain.inputs["Value"]) is_boundary = nodes.new("FunctionNodeBooleanMath") is_boundary.operation = "OR" links.new(edge_domain.outputs["Value"], is_boundary.inputs[0]) links.new(loose_vertex.outputs["Result"], is_boundary.inputs[1]) return is_boundary.outputs["Boolean"] def expand_smooth( group, geometry, iterations_name, weight_name, *, influence=None, pin_sharp=True, ): nodes = group.nodes links = group.links iteration_controls = group_input(nodes, {iterations_name}) weight_controls = group_input(nodes, {weight_name}) repeat_output = nodes.new("GeometryNodeRepeatOutput") repeat_input = nodes.new("GeometryNodeRepeatInput") repeat_input.pair_with_output(repeat_output) links.new( iteration_controls.outputs[iterations_name], repeat_input.inputs["Iterations"], ) boundary_field = edge_boundary_field(nodes, links) boundary_points = nodes.new("GeometryNodeFieldOnDomain") boundary_points.data_type = "BOOLEAN" boundary_points.domain = "POINT" links.new(boundary_field, boundary_points.inputs["Value"]) boundary_geometry = nodes.new("GeometryNodeSeparateGeometry") boundary_geometry.domain = "POINT" links.new(repeat_input.outputs["Geometry"], boundary_geometry.inputs["Geometry"]) links.new(boundary_points.outputs["Value"], boundary_geometry.inputs["Selection"]) links.new(geometry, repeat_input.inputs["Geometry"]) nearest_boundary = nodes.new("GeometryNodeSampleNearest") nearest_boundary.domain = "POINT" if influence is not None: # Both boundary meshes keep the same compact index order: only positions change. source_boundary = nodes.new("GeometryNodeSeparateGeometry") source_boundary.domain = "POINT" links.new(geometry, source_boundary.inputs["Geometry"]) links.new(boundary_points.outputs["Value"], source_boundary.inputs["Selection"]) links.new(source_boundary.outputs["Selection"], nearest_boundary.inputs["Geometry"]) source_position = nodes.new("GeometryNodeInputPosition").outputs["Position"] fixed_position = sample_field(group, geometry, source_position, "FLOAT_VECTOR") links.new(fixed_position, nearest_boundary.inputs["Sample Position"]) else: links.new(boundary_geometry.outputs["Selection"], nearest_boundary.inputs["Geometry"]) position = nodes.new("GeometryNodeInputPosition") smooth_all = nodes.new("GeometryNodeBlurAttribute") smooth_all.data_type = "FLOAT_VECTOR" smooth_all.inputs["Iterations"].default_value = 1 smooth_all.inputs["Weight"].default_value = 1.0 links.new(position.outputs["Position"], smooth_all.inputs["Value"]) boundary_position = nodes.new("GeometryNodeInputPosition") smooth_boundary = nodes.new("GeometryNodeBlurAttribute") smooth_boundary.data_type = "FLOAT_VECTOR" smooth_boundary.inputs["Iterations"].default_value = 1 smooth_boundary.inputs["Weight"].default_value = 0.5 links.new(boundary_position.outputs["Position"], smooth_boundary.inputs["Value"]) sample_boundary = nodes.new("GeometryNodeSampleIndex") sample_boundary.data_type = "FLOAT_VECTOR" sample_boundary.domain = "POINT" sample_boundary.clamp = False links.new(boundary_geometry.outputs["Selection"], sample_boundary.inputs["Geometry"]) links.new(smooth_boundary.outputs["Value"], sample_boundary.inputs["Value"]) links.new(nearest_boundary.outputs["Index"], sample_boundary.inputs["Index"]) choose_boundary = nodes.new("GeometryNodeSwitch") choose_boundary.input_type = "VECTOR" links.new(boundary_points.outputs["Value"], choose_boundary.inputs["Switch"]) links.new(smooth_all.outputs["Value"], choose_boundary.inputs["False"]) links.new(sample_boundary.outputs["Value"], choose_boundary.inputs["True"]) weight = weight_controls.outputs[weight_name] if pin_sharp: normal = nodes.new("GeometryNodeInputNormal") smooth_normal = nodes.new("GeometryNodeBlurAttribute") smooth_normal.data_type = "FLOAT_VECTOR" smooth_normal.inputs["Iterations"].default_value = 10 smooth_normal.inputs["Weight"].default_value = 1.0 links.new(normal.outputs["Normal"], smooth_normal.inputs["Value"]) normal_coherence = nodes.new("ShaderNodeVectorMath") normal_coherence.operation = "LENGTH" links.new(smooth_normal.outputs["Value"], normal_coherence.inputs[0]) smooth_weight = nodes.new("ShaderNodeMath") smooth_weight.operation = "MULTIPLY" links.new(weight_controls.outputs[weight_name], smooth_weight.inputs[0]) links.new(normal_coherence.outputs["Value"], smooth_weight.inputs[1]) weight = smooth_weight.outputs[0] if influence is not None: masked_weight = nodes.new("ShaderNodeMath") masked_weight.operation = "MULTIPLY" links.new(weight, masked_weight.inputs[0]) links.new(influence, masked_weight.inputs[1]) weight = masked_weight.outputs[0] current_position = nodes.new("GeometryNodeInputPosition") mix_position = nodes.new("ShaderNodeMix") mix_position.data_type = "VECTOR" links.new(weight, mix_position.inputs[0]) links.new(current_position.outputs["Position"], mix_position.inputs[4]) links.new(choose_boundary.outputs["Output"], mix_position.inputs[5]) set_position = nodes.new("GeometryNodeSetPosition") links.new(repeat_input.outputs["Geometry"], set_position.inputs["Geometry"]) links.new(mix_position.outputs["Result"], set_position.inputs["Position"]) links.new(set_position.outputs["Geometry"], repeat_output.inputs["Geometry"]) if influence is not None: enabled = nodes.new("GeometryNodeSwitch") enabled.input_type = "GEOMETRY" links.new(iteration_controls.outputs[iterations_name], enabled.inputs["Switch"]) links.new(geometry, enabled.inputs["False"]) links.new(repeat_output.outputs["Geometry"], enabled.inputs["True"]) return enabled.outputs["Output"] return repeat_output.outputs["Geometry"]