|
|
|
|
@@ -9,6 +9,7 @@ from ..common.nodes import (
|
|
|
|
|
from ..common.depth_surface import _math
|
|
|
|
|
from ..common.normal_map import ROTATION_ATTRIBUTE, FACE_ATTRIBUTE, AXIS_ATTRIBUTE
|
|
|
|
|
from ..common.direction import symmetry_legacy_direction_to_canonical
|
|
|
|
|
from ..common.smoothing import edge_boundary_field
|
|
|
|
|
|
|
|
|
|
NODE_GROUP_NAME = "O Image Cutout Symmetry"
|
|
|
|
|
|
|
|
|
|
@@ -55,20 +56,101 @@ def _align_front(group, geometry, controls):
|
|
|
|
|
return store.outputs['Geometry']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _delete_positive_faces(group, front, tolerance):
|
|
|
|
|
"""Discard faces touching positive Y without leaving wires or isolated vertices."""
|
|
|
|
|
position = group.nodes.new('GeometryNodeInputPosition')
|
|
|
|
|
components = group.nodes.new('ShaderNodeSeparateXYZ')
|
|
|
|
|
group.links.new(position.outputs[0], components.inputs[0])
|
|
|
|
|
positive = _math(group, 'GREATER_THAN', components.outputs['Y'], tolerance)
|
|
|
|
|
point_positive = evaluate_field(group, positive, 'FLOAT', 'POINT')
|
|
|
|
|
face_positive = evaluate_field(group, point_positive, 'FLOAT', 'FACE')
|
|
|
|
|
delete = group.nodes.new('GeometryNodeDeleteGeometry')
|
|
|
|
|
delete.domain = 'FACE'
|
|
|
|
|
delete.mode = 'ALL'
|
|
|
|
|
group.links.new(front, delete.inputs['Geometry'])
|
|
|
|
|
group.links.new(_math(group, 'GREATER_THAN', face_positive, 0), delete.inputs['Selection'])
|
|
|
|
|
return delete.outputs[0]
|
|
|
|
|
def _boundary_edge_field(group):
|
|
|
|
|
"""Report one on edges that carry a single face."""
|
|
|
|
|
neighbors = group.nodes.new('GeometryNodeInputMeshEdgeNeighbors')
|
|
|
|
|
return _math(group, 'COMPARE', neighbors.outputs['Face Count'], 1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _fold_to_plane(group, geometry, tolerance):
|
|
|
|
|
"""Move over-plane points onto the plane and drop the faces that collapse."""
|
|
|
|
|
nodes, links = group.nodes, group.links
|
|
|
|
|
position = nodes.new('GeometryNodeInputPosition')
|
|
|
|
|
components = nodes.new('ShaderNodeSeparateXYZ')
|
|
|
|
|
links.new(position.outputs[0], components.inputs[0])
|
|
|
|
|
beyond = _math(group, 'GREATER_THAN', components.outputs['Y'], tolerance)
|
|
|
|
|
target = nodes.new('ShaderNodeCombineXYZ')
|
|
|
|
|
links.new(components.outputs['X'], target.inputs['X'])
|
|
|
|
|
links.new(components.outputs['Z'], target.inputs['Z'])
|
|
|
|
|
place = nodes.new('GeometryNodeSetPosition')
|
|
|
|
|
links.new(geometry, place.inputs['Geometry'])
|
|
|
|
|
links.new(beyond, place.inputs['Selection'])
|
|
|
|
|
links.new(target.outputs[0], place.inputs['Position'])
|
|
|
|
|
# A face lying on the plane would be mirrored onto itself.
|
|
|
|
|
center = evaluate_field(group, _math(group, 'ABSOLUTE', components.outputs['Y']), 'FLOAT', 'FACE')
|
|
|
|
|
normal = nodes.new('GeometryNodeInputNormal')
|
|
|
|
|
normal_axes = nodes.new('ShaderNodeSeparateXYZ')
|
|
|
|
|
links.new(normal.outputs[0], normal_axes.inputs[0])
|
|
|
|
|
flat = nodes.new('FunctionNodeBooleanMath')
|
|
|
|
|
flat.operation = 'AND'
|
|
|
|
|
links.new(_math(group, 'LESS_THAN', center, tolerance), flat.inputs[0])
|
|
|
|
|
links.new(_math(group, 'GREATER_THAN', _math(group, 'ABSOLUTE', normal_axes.outputs['Y']), 1 - 1e-3), flat.inputs[1])
|
|
|
|
|
delete = nodes.new('GeometryNodeDeleteGeometry')
|
|
|
|
|
delete.domain, delete.mode = 'FACE', 'ALL'
|
|
|
|
|
links.new(place.outputs[0], delete.inputs['Geometry'])
|
|
|
|
|
links.new(flat.outputs[0], delete.inputs['Selection'])
|
|
|
|
|
return _clean_crease(group, delete.outputs[0], tolerance)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _clean_crease(group, geometry, tolerance):
|
|
|
|
|
"""Open interior in-plane edges left by folding a closed surface."""
|
|
|
|
|
nodes, links = group.nodes, group.links
|
|
|
|
|
heights = _edge_abs_y(group)
|
|
|
|
|
edge_plane = nodes.new('FunctionNodeBooleanMath')
|
|
|
|
|
edge_plane.operation = 'AND'
|
|
|
|
|
links.new(_math(group, 'LESS_THAN', heights[0], tolerance), edge_plane.inputs[0])
|
|
|
|
|
links.new(_math(group, 'LESS_THAN', heights[1], tolerance), edge_plane.inputs[1])
|
|
|
|
|
neighbors = nodes.new('GeometryNodeInputMeshEdgeNeighbors')
|
|
|
|
|
interior = _math(group, 'COMPARE', neighbors.outputs['Face Count'], 2)
|
|
|
|
|
fold = nodes.new('FunctionNodeBooleanMath')
|
|
|
|
|
fold.operation = 'AND'
|
|
|
|
|
links.new(edge_plane.outputs[0], fold.inputs[0])
|
|
|
|
|
links.new(interior, fold.inputs[1])
|
|
|
|
|
|
|
|
|
|
face_index = nodes.new('GeometryNodeInputIndex')
|
|
|
|
|
corners = nodes.new('GeometryNodeCornersOfFace')
|
|
|
|
|
links.new(face_index.outputs[0], corners.inputs['Face Index'])
|
|
|
|
|
face_fold = None
|
|
|
|
|
for offset in range(4):
|
|
|
|
|
corner = nodes.new('GeometryNodeOffsetCornerInFace')
|
|
|
|
|
links.new(corners.outputs['Corner Index'], corner.inputs['Corner Index'])
|
|
|
|
|
corner.inputs['Offset'].default_value = offset
|
|
|
|
|
edge_of_corner = nodes.new('GeometryNodeEdgesOfCorner')
|
|
|
|
|
links.new(corner.outputs['Corner Index'], edge_of_corner.inputs['Corner Index'])
|
|
|
|
|
sample = nodes.new('GeometryNodeFieldAtIndex')
|
|
|
|
|
sample.data_type, sample.domain = 'BOOLEAN', 'EDGE'
|
|
|
|
|
links.new(fold.outputs[0], sample.inputs['Value'])
|
|
|
|
|
links.new(edge_of_corner.outputs['Next Edge Index'], sample.inputs['Index'])
|
|
|
|
|
if face_fold is None:
|
|
|
|
|
face_fold = sample.outputs['Value']
|
|
|
|
|
else:
|
|
|
|
|
combined = nodes.new('FunctionNodeBooleanMath')
|
|
|
|
|
combined.operation = 'OR'
|
|
|
|
|
links.new(face_fold, combined.inputs[0])
|
|
|
|
|
links.new(sample.outputs['Value'], combined.inputs[1])
|
|
|
|
|
face_fold = combined.outputs['Boolean']
|
|
|
|
|
cleanup = nodes.new('GeometryNodeDeleteGeometry')
|
|
|
|
|
cleanup.domain, cleanup.mode = 'FACE', 'ALL'
|
|
|
|
|
links.new(geometry, cleanup.inputs['Geometry'])
|
|
|
|
|
links.new(face_fold, cleanup.inputs['Selection'])
|
|
|
|
|
return cleanup.outputs[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _off_plane_edge_field(group, tolerance):
|
|
|
|
|
"""Report one on edges that keep an endpoint off the symmetry plane."""
|
|
|
|
|
heights = _edge_abs_y(group)
|
|
|
|
|
return _math(group, 'GREATER_THAN', _math(group, 'MAXIMUM', heights[0], heights[1]), tolerance)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _edge_abs_y(group):
|
|
|
|
|
"""Return both endpoint heights on the symmetry axis for an edge."""
|
|
|
|
|
edge = group.nodes.new('GeometryNodeInputMeshEdgeVertices')
|
|
|
|
|
heights = []
|
|
|
|
|
for socket in ('Position 1', 'Position 2'):
|
|
|
|
|
axis = group.nodes.new('ShaderNodeSeparateXYZ')
|
|
|
|
|
group.links.new(edge.outputs[socket], axis.inputs[0])
|
|
|
|
|
heights.append(_math(group, 'ABSOLUTE', axis.outputs['Y']))
|
|
|
|
|
return heights
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _mirror(group, geometry):
|
|
|
|
|
@@ -80,6 +162,67 @@ def _mirror(group, geometry):
|
|
|
|
|
return flip.outputs[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _cut_fields(group, tolerance):
|
|
|
|
|
"""Return point-domain seam and outline fields for the cut."""
|
|
|
|
|
nodes, links = group.nodes, group.links
|
|
|
|
|
boundary = evaluate_field(group, edge_boundary_field(nodes, links), 'FLOAT', 'POINT')
|
|
|
|
|
position = nodes.new('GeometryNodeInputPosition')
|
|
|
|
|
components = nodes.new('ShaderNodeSeparateXYZ')
|
|
|
|
|
links.new(position.outputs[0], components.inputs[0])
|
|
|
|
|
absolute = _math(group, 'ABSOLUTE', components.outputs['Y'])
|
|
|
|
|
|
|
|
|
|
def split(side):
|
|
|
|
|
node = nodes.new('FunctionNodeBooleanMath')
|
|
|
|
|
node.operation = 'AND'
|
|
|
|
|
links.new(_math(group, 'GREATER_THAN', boundary, 0), node.inputs[0])
|
|
|
|
|
links.new(side, node.inputs[1])
|
|
|
|
|
return node.outputs[0]
|
|
|
|
|
|
|
|
|
|
return split(_math(group, 'LESS_THAN', absolute, tolerance)), split(
|
|
|
|
|
_math(group, 'GREATER_THAN', absolute, tolerance)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _falloff(group, center, rings=2):
|
|
|
|
|
"""Blur a 0/1 center into a falloff over the requested rings."""
|
|
|
|
|
blur = group.nodes.new('GeometryNodeBlurAttribute')
|
|
|
|
|
blur.data_type = 'FLOAT'
|
|
|
|
|
blur.inputs['Iterations'].default_value = rings
|
|
|
|
|
group.links.new(center, blur.inputs['Value'])
|
|
|
|
|
return _math(group, 'MAXIMUM', center, blur.outputs[0])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _apply_planar_relax(group, geometry, influence, smooth):
|
|
|
|
|
"""Move points inside the plane by their blurred position, gated by influence."""
|
|
|
|
|
nodes, links = group.nodes, group.links
|
|
|
|
|
position = nodes.new('GeometryNodeInputPosition')
|
|
|
|
|
blur = nodes.new('GeometryNodeBlurAttribute')
|
|
|
|
|
blur.data_type = 'FLOAT_VECTOR'
|
|
|
|
|
links.new(position.outputs[0], blur.inputs['Value'])
|
|
|
|
|
links.new(smooth, blur.inputs['Iterations'])
|
|
|
|
|
difference = nodes.new('ShaderNodeVectorMath')
|
|
|
|
|
difference.operation = 'SUBTRACT'
|
|
|
|
|
links.new(blur.outputs[0], difference.inputs[0])
|
|
|
|
|
links.new(position.outputs[0], difference.inputs[1])
|
|
|
|
|
in_plane = nodes.new('ShaderNodeVectorMath')
|
|
|
|
|
in_plane.operation = 'MULTIPLY'
|
|
|
|
|
in_plane.inputs[1].default_value = (.5, 0, .5)
|
|
|
|
|
links.new(difference.outputs[0], in_plane.inputs[0])
|
|
|
|
|
scaled = nodes.new('ShaderNodeVectorMath')
|
|
|
|
|
scaled.operation = 'SCALE'
|
|
|
|
|
links.new(in_plane.outputs[0], scaled.inputs[0])
|
|
|
|
|
links.new(influence, scaled.inputs[3])
|
|
|
|
|
place = nodes.new('GeometryNodeSetPosition')
|
|
|
|
|
links.new(geometry, place.inputs['Geometry'])
|
|
|
|
|
links.new(scaled.outputs[0], place.inputs['Offset'])
|
|
|
|
|
return place.outputs[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _relax_seam(group, geometry, seam_field, smooth):
|
|
|
|
|
"""Relax the cut seam in plane through a two-ring falloff."""
|
|
|
|
|
return _apply_planar_relax(group, geometry, _falloff(group, seam_field), smooth)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extrude_boundary(group, geometry, selection, y_factor):
|
|
|
|
|
"""Extrude selected edges to a fraction of their current height."""
|
|
|
|
|
nodes, links = group.nodes, group.links
|
|
|
|
|
@@ -106,50 +249,11 @@ def _extrude_boundary(group, geometry, selection, y_factor):
|
|
|
|
|
return store.outputs[0], extrude.outputs['Top']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _walls(group, front, boundary, smooth, tolerance, *, curved):
|
|
|
|
|
"""Build straight walls or a smoothed half-strip followed by its reflection."""
|
|
|
|
|
nodes, links = group.nodes, group.links
|
|
|
|
|
geometry, top = _extrude_boundary(group, front, boundary, .5 if curved else -1)
|
|
|
|
|
if curved:
|
|
|
|
|
geometry, _ = _extrude_boundary(group, geometry, top, 0)
|
|
|
|
|
sides = nodes.new('GeometryNodeSeparateGeometry')
|
|
|
|
|
sides.domain = 'FACE'
|
|
|
|
|
links.new(geometry, sides.inputs['Geometry'])
|
|
|
|
|
links.new(_math(group, 'COMPARE', read_float_attribute(group, FACE_ATTRIBUTE), 3), sides.inputs['Selection'])
|
|
|
|
|
geometry = sides.outputs['Selection']
|
|
|
|
|
if curved:
|
|
|
|
|
# Query the retained front to pin the original seam; only new ring XY moves.
|
|
|
|
|
proximity = nodes.new('GeometryNodeProximity')
|
|
|
|
|
proximity.target_element = 'POINTS'
|
|
|
|
|
links.new(front, proximity.inputs['Target'])
|
|
|
|
|
movable = _math(group, 'GREATER_THAN', proximity.outputs['Distance'], tolerance)
|
|
|
|
|
position = nodes.new('GeometryNodeInputPosition')
|
|
|
|
|
blur = nodes.new('GeometryNodeBlurAttribute')
|
|
|
|
|
blur.data_type = 'FLOAT_VECTOR'
|
|
|
|
|
links.new(position.outputs[0], blur.inputs['Value'])
|
|
|
|
|
links.new(smooth, blur.inputs['Iterations'])
|
|
|
|
|
# Keep heights fixed so neither the seam nor the symmetry plane can drift.
|
|
|
|
|
difference = nodes.new('ShaderNodeVectorMath')
|
|
|
|
|
difference.operation = 'SUBTRACT'
|
|
|
|
|
links.new(blur.outputs[0], difference.inputs[0])
|
|
|
|
|
links.new(position.outputs[0], difference.inputs[1])
|
|
|
|
|
mask = nodes.new('ShaderNodeVectorMath')
|
|
|
|
|
mask.operation = 'MULTIPLY'
|
|
|
|
|
mask.inputs[1].default_value = (.5, 0, .5)
|
|
|
|
|
links.new(difference.outputs[0], mask.inputs[0])
|
|
|
|
|
place = nodes.new('GeometryNodeSetPosition')
|
|
|
|
|
links.new(geometry, place.inputs['Geometry'])
|
|
|
|
|
links.new(movable, place.inputs['Selection'])
|
|
|
|
|
links.new(mask.outputs[0], place.inputs['Offset'])
|
|
|
|
|
join = nodes.new('GeometryNodeJoinGeometry')
|
|
|
|
|
links.new(place.outputs[0], join.inputs[0])
|
|
|
|
|
links.new(_mirror(group, place.outputs[0]), join.inputs[0])
|
|
|
|
|
geometry = join.outputs[0]
|
|
|
|
|
shade = nodes.new('GeometryNodeSetShadeSmooth')
|
|
|
|
|
shade.domain = 'FACE'
|
|
|
|
|
shade.inputs['Shade Smooth'].default_value = curved
|
|
|
|
|
links.new(geometry, shade.inputs['Geometry'])
|
|
|
|
|
return shade.outputs[0]
|
|
|
|
|
def _build_wall(group, front, boundary):
|
|
|
|
|
"""Attach the curved half wall to the front and mark its faces."""
|
|
|
|
|
geometry, top = _extrude_boundary(group, front, boundary, .5)
|
|
|
|
|
geometry, _ = _extrude_boundary(group, geometry, top, 0)
|
|
|
|
|
return geometry
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_image_cutout_symmetry_group():
|
|
|
|
|
@@ -167,36 +271,54 @@ def build_image_cutout_symmetry_group():
|
|
|
|
|
bool_input(group, 'Fill Sides', True, parent=options).description = 'Connect matching front and mirrored boundaries.'
|
|
|
|
|
smooth = interface_socket(group, 'Fill Smooth', 'INPUT', 'NodeSocketInt', options)
|
|
|
|
|
smooth.default_value, smooth.min_value, smooth.max_value = 2, 0, 16
|
|
|
|
|
smooth.description = 'Smooth only the generated wall interiors. Zero keeps straight walls.'
|
|
|
|
|
smooth.description = 'Smooth the outline and the wall it builds through a two-ring falloff. Zero keeps the raw outline.'
|
|
|
|
|
seam = interface_socket(group, 'Seam Smooth', 'INPUT', 'NodeSocketInt', options)
|
|
|
|
|
seam.default_value, seam.min_value, seam.max_value = 2, 0, 16
|
|
|
|
|
seam.description = 'Smooth the cut seam inside the symmetry plane. Zero keeps the raw seam.'
|
|
|
|
|
interface_socket(group, 'Geometry', 'OUTPUT', 'NodeSocketGeometry')
|
|
|
|
|
nodes, links = group.nodes, group.links
|
|
|
|
|
inputs = group_input(nodes, set()).outputs
|
|
|
|
|
aligned = _align_front(group, inputs['Geometry'], inputs)
|
|
|
|
|
bounds = nodes.new('GeometryNodeBoundBox')
|
|
|
|
|
links.new(inputs['Geometry'], bounds.inputs['Geometry'])
|
|
|
|
|
links.new(aligned, bounds.inputs['Geometry'])
|
|
|
|
|
size = nodes.new('ShaderNodeVectorMath')
|
|
|
|
|
size.operation = 'DISTANCE'
|
|
|
|
|
links.new(bounds.outputs['Min'], size.inputs[0])
|
|
|
|
|
links.new(bounds.outputs['Max'], size.inputs[1])
|
|
|
|
|
tolerance = _math(group, 'MULTIPLY', _math(group, 'MAXIMUM', size.outputs['Value'], 1e-8), 1e-7)
|
|
|
|
|
front = _delete_positive_faces(group, _align_front(group, inputs['Geometry'], inputs), tolerance)
|
|
|
|
|
is_boundary = _boundary_edge_field(group)
|
|
|
|
|
seam_field, outline_field = _cut_fields(group, tolerance)
|
|
|
|
|
front = _fold_to_plane(group, aligned, tolerance)
|
|
|
|
|
shade = nodes.new('GeometryNodeSetShadeSmooth')
|
|
|
|
|
shade.domain = 'FACE'
|
|
|
|
|
links.new(front, shade.inputs['Geometry'])
|
|
|
|
|
front = store_float_attribute(group, shade.outputs[0], FACE_ATTRIBUTE, 1., domain='FACE')
|
|
|
|
|
count = nodes.new('GeometryNodeInputMeshEdgeNeighbors')
|
|
|
|
|
boundary = _math(group, 'COMPARE', count.outputs['Face Count'], 1)
|
|
|
|
|
walls = nodes.new('GeometryNodeSwitch')
|
|
|
|
|
walls.input_type = 'GEOMETRY'
|
|
|
|
|
links.new(_math(group, 'GREATER_THAN', inputs['Fill Smooth'], 0), walls.inputs['Switch'])
|
|
|
|
|
for curved, socket in ((False, 'False'), (True, 'True')):
|
|
|
|
|
links.new(_walls(group, front, boundary, inputs['Fill Smooth'], tolerance, curved=curved), walls.inputs[socket])
|
|
|
|
|
enabled = nodes.new('GeometryNodeSwitch')
|
|
|
|
|
enabled.input_type = 'GEOMETRY'
|
|
|
|
|
links.new(inputs['Fill Sides'], enabled.inputs['Switch'])
|
|
|
|
|
links.new(walls.outputs[0], enabled.inputs['True'])
|
|
|
|
|
back = store_float_attribute(group, _mirror(group, front), FACE_ATTRIBUTE, 2., domain='FACE')
|
|
|
|
|
front = _relax_seam(group, front, seam_field, inputs['Seam Smooth'])
|
|
|
|
|
boundary = nodes.new('FunctionNodeBooleanMath')
|
|
|
|
|
boundary.operation = 'AND'
|
|
|
|
|
links.new(is_boundary, boundary.inputs[0])
|
|
|
|
|
# The seam sits on the plane and welds through the merge, so only off-plane edges need walls.
|
|
|
|
|
links.new(_off_plane_edge_field(group, tolerance), boundary.inputs[1])
|
|
|
|
|
fill_smooth = _math(group, 'MULTIPLY', inputs['Fill Smooth'], inputs['Fill Sides'])
|
|
|
|
|
front = _apply_planar_relax(group, front, _falloff(group, outline_field), fill_smooth)
|
|
|
|
|
walled = _build_wall(group, front, boundary.outputs[0])
|
|
|
|
|
wall_shade = nodes.new('GeometryNodeSetShadeSmooth')
|
|
|
|
|
wall_shade.domain = 'FACE'
|
|
|
|
|
links.new(_math(group, 'COMPARE', read_float_attribute(group, FACE_ATTRIBUTE), 3), wall_shade.inputs['Selection'])
|
|
|
|
|
links.new(walled, wall_shade.inputs['Geometry'])
|
|
|
|
|
body = nodes.new('GeometryNodeSwitch')
|
|
|
|
|
body.input_type = 'GEOMETRY'
|
|
|
|
|
links.new(inputs['Fill Sides'], body.inputs['Switch'])
|
|
|
|
|
links.new(front, body.inputs['False'])
|
|
|
|
|
links.new(wall_shade.outputs[0], body.inputs['True'])
|
|
|
|
|
mark_back = nodes.new('GeometryNodeStoreNamedAttribute')
|
|
|
|
|
mark_back.data_type, mark_back.domain = 'FLOAT', 'FACE'
|
|
|
|
|
mark_back.inputs['Name'].default_value = FACE_ATTRIBUTE
|
|
|
|
|
mark_back.inputs['Value'].default_value = 2
|
|
|
|
|
links.new(_mirror(group, body.outputs[0]), mark_back.inputs['Geometry'])
|
|
|
|
|
links.new(_math(group, 'COMPARE', read_float_attribute(group, FACE_ATTRIBUTE), 1), mark_back.inputs['Selection'])
|
|
|
|
|
join = nodes.new('GeometryNodeJoinGeometry')
|
|
|
|
|
for geometry in (front, back, enabled.outputs[0]):
|
|
|
|
|
for geometry in (body.outputs[0], mark_back.outputs['Geometry']):
|
|
|
|
|
links.new(geometry, join.inputs[0])
|
|
|
|
|
merge = nodes.new('GeometryNodeMergeByDistance')
|
|
|
|
|
links.new(tolerance, merge.inputs['Distance'])
|
|
|
|
|
|