Add tests and implement boundary smoothing and cutout functionality

- Introduced tests for boundary smoothing in `test_boundary_smoothing.py`, verifying that smoothing reduces stair-step artifacts while preserving boundary integrity.
- Added tests for cutout boundary behavior in `test_cutout_boundary.py`, ensuring proper handling of boundary triangles and normal blending.
- Created `test_temporary_attributes.py` to check the cleanup of temporary attributes across depth modifier branches, ensuring no warnings are raised.
- Implemented point culling functionality in `test_validity.py`, confirming that invalid points are removed without affecting adjacent geometry.
- Developed boundary smoothing logic in `boundary_smoothing.py`, allowing for depth smoothing while maintaining edge integrity.
- Added cutout boundary management in `cutout_boundary.py`, including functions for removing boundary triangles and tapering thickness profiles.
- Implemented validity checks in `validity.py` to ensure proper deletion of invalid vertices and associated wire edges.
This commit is contained in:
2026-09-11 20:37:20 +08:00
parent e8129fe495
commit 31074bd511
85 changed files with 2501 additions and 1451 deletions

View File

@@ -114,13 +114,45 @@ def evaluate_field(group, value, data_type, domain):
return node.outputs["Value"]
def capture_fields(group, geometry, domain, **fields):
"""Keep source fields attached to geometry through later changes."""
node = group.nodes.new("GeometryNodeCaptureAttribute")
node.domain = domain
node.capture_items.clear()
def sample_field(group, geometry, value, data_type, domain="POINT", index=None):
"""Evaluate a field on an explicit source geometry and index domain."""
node = group.nodes.new("GeometryNodeSampleIndex")
node.data_type, node.domain = data_type, domain
node.clamp = False
if index is None:
index = group.nodes.new("GeometryNodeInputIndex").outputs["Index"]
group.links.new(geometry, node.inputs["Geometry"])
for name, (data_type, value) in fields.items():
node.capture_items.new(data_type, name)
group.links.new(value, node.inputs[name])
return node.outputs["Geometry"], {name: node.outputs[name] for name in fields}
group.links.new(value, node.inputs["Value"])
group.links.new(index, node.inputs["Index"])
return node.outputs["Value"]
def store_boolean_attribute(group, geometry, name, value, domain="POINT"):
"""Store a topology marker needed after destructive mesh operations."""
node = group.nodes.new("GeometryNodeStoreNamedAttribute")
node.domain, node.data_type = domain, "BOOLEAN"
node.inputs["Name"].default_value = name
group.links.new(geometry, node.inputs["Geometry"])
group.links.new(value, node.inputs["Value"])
read = group.nodes.new("GeometryNodeInputNamedAttribute")
read.data_type = "BOOLEAN"
read.inputs["Name"].default_value = name
return node.outputs["Geometry"], read.outputs["Attribute"]
def remove_attributes(group, geometry, *names, enabled=None):
"""Remove only the explicitly owned temporary attributes."""
original = geometry
for name in names:
node = group.nodes.new("GeometryNodeRemoveAttribute")
node.inputs["Name"].default_value = name
group.links.new(geometry, node.inputs["Geometry"])
geometry = node.outputs["Geometry"]
if enabled is not None:
choose = group.nodes.new("GeometryNodeSwitch")
choose.input_type = "GEOMETRY"
group.links.new(enabled, choose.inputs["Switch"])
group.links.new(original, choose.inputs["False"])
group.links.new(geometry, choose.inputs["True"])
geometry = choose.outputs["Output"]
return geometry