- Introduced a new proposal for depth cutout symmetry, allowing for the reuse of depth replacement and geometry operations. - Added requirements for maintaining default thickness values for cutout nodes and ensuring proper initialization during object creation. - Enhanced symmetry node to handle existing geometry, including alignment, mirroring, and boundary filling. - Implemented fill smoothing to control the geometry of generated walls while preserving original surfaces. - Created tests to validate the functionality of the new symmetry features against baseline geometry. - Updated existing assets and node groups to support the new symmetry capabilities without introducing additional dependencies.
188 lines
9.5 KiB
Python
188 lines
9.5 KiB
Python
"""Mirror completed depth geometry and fill matching boundaries."""
|
|
import bpy
|
|
|
|
from ..common.nodes import (
|
|
interface_socket, group_input, float_input, bool_input, vector_input,
|
|
evaluate_field, store_float_attribute, read_float_attribute, prepare_node_group,
|
|
)
|
|
from ..common.depth_surface import _math
|
|
from ..common.normal_map import ROTATION_ATTRIBUTE, FACE_ATTRIBUTE, AXIS_ATTRIBUTE
|
|
|
|
NODE_GROUP_NAME = "O Image Cutout Symmetry"
|
|
|
|
|
|
def _align_front(group, geometry, controls):
|
|
"""Align existing geometry with the fixed XY symmetry plane."""
|
|
nodes, links = group.nodes, group.links
|
|
align = nodes.new('FunctionNodeAlignEulerToVector')
|
|
align.axis = 'Z'
|
|
links.new(controls['Symmetry Direction'], align.inputs['Vector'])
|
|
position = nodes.new('GeometryNodeInputPosition')
|
|
rotate = nodes.new('ShaderNodeVectorRotate')
|
|
rotate.rotation_type = 'EULER_XYZ'
|
|
rotate.invert = True
|
|
links.new(position.outputs[0], rotate.inputs['Vector'])
|
|
links.new(align.outputs['Rotation'], rotate.inputs['Rotation'])
|
|
place = nodes.new('GeometryNodeSetPosition')
|
|
links.new(geometry, place.inputs['Geometry'])
|
|
links.new(rotate.outputs[0], place.inputs['Position'])
|
|
offset = nodes.new('ShaderNodeCombineXYZ')
|
|
links.new(controls['Depth Offset'], offset.inputs['Z'])
|
|
links.new(offset.outputs[0], place.inputs['Offset'])
|
|
store = nodes.new('GeometryNodeStoreNamedAttribute')
|
|
store.data_type, store.domain = 'FLOAT_VECTOR', 'FACE'
|
|
store.inputs['Name'].default_value = ROTATION_ATTRIBUTE
|
|
links.new(place.outputs[0], store.inputs['Geometry'])
|
|
links.new(align.outputs['Rotation'], store.inputs['Value'])
|
|
return store.outputs['Geometry']
|
|
|
|
|
|
def _delete_negative_faces(group, front, tolerance):
|
|
"""Discard faces touching negative Z 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])
|
|
negative = _math(group, 'LESS_THAN', components.outputs['Z'], tolerance)
|
|
point_negative = evaluate_field(group, negative, 'FLOAT', 'POINT')
|
|
face_negative = evaluate_field(group, point_negative, '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_negative, 0), delete.inputs['Selection'])
|
|
return delete.outputs[0]
|
|
|
|
|
|
def _mirror(group, geometry):
|
|
mirror = group.nodes.new('GeometryNodeTransform')
|
|
mirror.inputs['Scale'].default_value = (1, 1, -1)
|
|
group.links.new(geometry, mirror.inputs['Geometry'])
|
|
flip = group.nodes.new('GeometryNodeFlipFaces')
|
|
group.links.new(mirror.outputs[0], flip.inputs['Mesh'])
|
|
return flip.outputs[0]
|
|
|
|
|
|
def _extrude_boundary(group, geometry, selection, z_factor):
|
|
"""Extrude selected edges to a fraction of their current height."""
|
|
nodes, links = group.nodes, group.links
|
|
extrude = nodes.new('GeometryNodeExtrudeMesh')
|
|
extrude.mode = 'EDGES'
|
|
extrude.inputs['Offset Scale'].default_value = 0
|
|
links.new(geometry, extrude.inputs['Mesh'])
|
|
links.new(selection, extrude.inputs['Selection'])
|
|
position = nodes.new('GeometryNodeInputPosition')
|
|
scale = nodes.new('ShaderNodeVectorMath')
|
|
scale.operation = 'MULTIPLY'
|
|
scale.inputs[1].default_value = (1, 1, z_factor)
|
|
links.new(position.outputs[0], scale.inputs[0])
|
|
place = nodes.new('GeometryNodeSetPosition')
|
|
links.new(extrude.outputs['Mesh'], place.inputs['Geometry'])
|
|
links.new(extrude.outputs['Top'], place.inputs['Selection'])
|
|
links.new(scale.outputs[0], place.inputs['Position'])
|
|
store = nodes.new('GeometryNodeStoreNamedAttribute')
|
|
store.data_type, store.domain = 'FLOAT', 'FACE'
|
|
store.inputs['Name'].default_value = FACE_ATTRIBUTE
|
|
store.inputs['Value'].default_value = 3
|
|
links.new(place.outputs[0], store.inputs['Geometry'])
|
|
links.new(extrude.outputs['Side'], store.inputs['Selection'])
|
|
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, .5, 0)
|
|
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_image_cutout_symmetry_group():
|
|
existing = bpy.data.node_groups.get(NODE_GROUP_NAME)
|
|
if existing is not None:
|
|
return existing
|
|
group = bpy.data.node_groups.new(NODE_GROUP_NAME, 'GeometryNodeTree')
|
|
group.is_modifier, group.use_fake_user = True, True
|
|
group.color_tag = 'GEOMETRY'
|
|
interface_socket(group, 'Geometry', 'INPUT', 'NodeSocketGeometry')
|
|
vector_input(group, 'Symmetry Direction', (0, 0, 1), subtype='DIRECTION').description = 'Align this direction with the local symmetry axis.'
|
|
float_input(group, 'Depth Offset', 0, minimum=-1e10, subtype='DISTANCE').description = 'Offset the aligned geometry along the symmetry axis.'
|
|
bool_input(group, 'Fill Sides', True).description = 'Connect matching front and mirrored boundaries.'
|
|
smooth = interface_socket(group, 'Fill Smooth', 'INPUT', 'NodeSocketInt')
|
|
smooth.default_value, smooth.min_value, smooth.max_value = 2, 0, 20
|
|
smooth.description = 'Smooth only the generated wall interiors. Zero keeps straight walls.'
|
|
interface_socket(group, 'Geometry', 'OUTPUT', 'NodeSocketGeometry')
|
|
nodes, links = group.nodes, group.links
|
|
inputs = group_input(nodes, set()).outputs
|
|
bounds = nodes.new('GeometryNodeBoundBox')
|
|
links.new(inputs['Geometry'], 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_negative_faces(group, _align_front(group, inputs['Geometry'], inputs), 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')
|
|
join = nodes.new('GeometryNodeJoinGeometry')
|
|
for geometry in (front, back, enabled.outputs[0]):
|
|
links.new(geometry, join.inputs[0])
|
|
merge = nodes.new('GeometryNodeMergeByDistance')
|
|
links.new(tolerance, merge.inputs['Distance'])
|
|
links.new(join.outputs[0], merge.inputs['Geometry'])
|
|
geometry = store_float_attribute(group, merge.outputs[0], AXIS_ATTRIBUTE, 0., domain='FACE')
|
|
links.new(geometry, nodes.new('NodeGroupOutput').inputs['Geometry'])
|
|
prepare_node_group(group)
|
|
return group
|