- Added a new "Stretch Limit" parameter to the Depth Surface node, allowing for dynamic edge culling based on the ratio of original to projected edge lengths. - Updated the geometry processing to capture original edge lengths before projection and calculate current lengths after projection. - Implemented logic to delete edges and faces that exceed the stretch limit, ensuring cleaner topology. - Enhanced tests to validate the new functionality, including scenarios for edge retention and deletion based on stretch limits. - Updated documentation and node assets to reflect the new parameter and its implications on geometry processing.
1139 lines
44 KiB
Python
1139 lines
44 KiB
Python
"""Build the four Cutout Geometry Nodes groups stored in O_AnyImage.blend."""
|
|
|
|
|
|
import bpy
|
|
from node_graph import expand_smooth
|
|
|
|
from anyimage.operators.cutout_tool.shape import (
|
|
BALLOON_ATTRIBUTE_NAME,
|
|
CUTOUT_AXIS_TO_SOURCE,
|
|
CUTOUT_NODE_GROUP_NAME,
|
|
CUTOUT_NODE_GROUP_NAMES,
|
|
CUTOUT_SHAPE_LABELS,
|
|
)
|
|
|
|
|
|
def _socket(group, name, in_out, socket_type, parent=None):
|
|
socket = group.interface.new_socket(
|
|
name=name,
|
|
in_out=in_out,
|
|
socket_type=socket_type,
|
|
parent=parent,
|
|
)
|
|
if in_out == "INPUT" and hasattr(socket, "structure_type"):
|
|
socket.structure_type = "SINGLE"
|
|
return socket
|
|
|
|
|
|
def _group_input(nodes, location, visible_names):
|
|
node = nodes.new("NodeGroupInput")
|
|
node.location = location
|
|
visible = set(visible_names)
|
|
for output in node.outputs:
|
|
output.hide = output.name not in visible
|
|
return node
|
|
|
|
|
|
def _localize_group_inputs(group, controls):
|
|
"""Place short, socket-specific Group Inputs beside their consumers."""
|
|
links = group.links
|
|
outgoing = [link for link in links if link.from_node == controls]
|
|
by_socket = {}
|
|
for link in outgoing:
|
|
if link.from_socket.name == "Geometry":
|
|
continue
|
|
by_socket.setdefault(link.from_socket.name, []).append(link)
|
|
|
|
for socket_name, socket_links in by_socket.items():
|
|
ordered = sorted(socket_links, key=lambda link: link.to_node.location.x)
|
|
clusters = []
|
|
for link in ordered:
|
|
if (
|
|
not clusters
|
|
or link.to_node.location.x - clusters[-1][-1].to_node.location.x > 900.0
|
|
):
|
|
clusters.append([])
|
|
clusters[-1].append(link)
|
|
for cluster in clusters:
|
|
targets = [link.to_node for link in cluster]
|
|
location = [
|
|
min(node.location.x for node in targets) - 260.0,
|
|
max(node.location.y for node in targets) + 320.0,
|
|
]
|
|
while any(
|
|
abs(node.location.x - location[0]) < 190.0
|
|
and abs(node.location.y - location[1]) < 190.0
|
|
for node in group.nodes
|
|
):
|
|
location[1] += 220.0
|
|
local = _group_input(group.nodes, location, {socket_name})
|
|
for link in cluster:
|
|
target = link.to_socket
|
|
links.remove(link)
|
|
links.new(local.outputs[socket_name], target)
|
|
for output in controls.outputs:
|
|
output.hide = output.name != "Geometry"
|
|
|
|
|
|
def _float_input(
|
|
group,
|
|
name,
|
|
default,
|
|
minimum=0.0,
|
|
maximum=None,
|
|
subtype=None,
|
|
parent=None,
|
|
):
|
|
socket = _socket(group, name, "INPUT", "NodeSocketFloat", parent)
|
|
socket.default_value = default
|
|
socket.min_value = minimum
|
|
if maximum is not None:
|
|
socket.max_value = maximum
|
|
if subtype is not None:
|
|
socket.subtype = subtype
|
|
return socket
|
|
|
|
|
|
def _bool_input(group, name, default=False, parent=None):
|
|
socket = _socket(group, name, "INPUT", "NodeSocketBool", parent)
|
|
socket.default_value = default
|
|
return socket
|
|
|
|
|
|
def _vector_input(group, name, default, subtype=None, parent=None):
|
|
socket = _socket(group, name, "INPUT", "NodeSocketVector", parent)
|
|
socket.default_value = default
|
|
if subtype is not None:
|
|
socket.subtype = subtype
|
|
return socket
|
|
|
|
|
|
def _math(nodes, operation, location):
|
|
node = nodes.new("ShaderNodeMath")
|
|
node.operation = operation
|
|
node.location = location
|
|
return node
|
|
|
|
|
|
def _island_cleanup(group, geometry, cleanup, location):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
x, y = location
|
|
mesh_island = nodes.new("GeometryNodeInputMeshIsland")
|
|
mesh_island.location = (x, y + 250.0)
|
|
face_area = nodes.new("GeometryNodeInputMeshFaceArea")
|
|
face_area.location = (x, y + 100.0)
|
|
island_area = nodes.new("GeometryNodeAccumulateField")
|
|
island_area.data_type = "FLOAT"
|
|
island_area.domain = "FACE"
|
|
island_area.location = (x + 220.0, y + 200.0)
|
|
links.new(face_area.outputs["Area"], island_area.inputs["Value"])
|
|
links.new(mesh_island.outputs["Island Index"], island_area.inputs["Group ID"])
|
|
|
|
tiny = _math(nodes, "LESS_THAN", (x + 450.0, y + 200.0))
|
|
links.new(island_area.outputs["Total"], tiny.inputs[0])
|
|
links.new(cleanup, tiny.inputs[1])
|
|
delete = nodes.new("GeometryNodeDeleteGeometry")
|
|
delete.domain = "FACE"
|
|
delete.location = (x + 700.0, y)
|
|
links.new(geometry, delete.inputs["Geometry"])
|
|
links.new(tiny.outputs[0], delete.inputs["Selection"])
|
|
return delete.outputs["Geometry"]
|
|
|
|
|
|
def _profile_field(group, geometry, location):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
x, y = location
|
|
profile = nodes.new("GeometryNodeInputNamedAttribute")
|
|
profile.data_type = "FLOAT"
|
|
profile.inputs["Name"].default_value = BALLOON_ATTRIBUTE_NAME
|
|
profile.location = (x, y + 250.0)
|
|
profile_max = nodes.new("GeometryNodeAttributeStatistic")
|
|
profile_max.data_type = "FLOAT"
|
|
profile_max.domain = "POINT"
|
|
profile_max.location = (x + 220.0, y)
|
|
links.new(geometry, profile_max.inputs["Geometry"])
|
|
links.new(profile.outputs["Attribute"], profile_max.inputs["Attribute"])
|
|
safe_max = _math(nodes, "MAXIMUM", (x + 450.0, y))
|
|
safe_max.inputs[1].default_value = 1e-6
|
|
links.new(profile_max.outputs["Max"], safe_max.inputs[0])
|
|
normalized = _math(nodes, "DIVIDE", (x + 450.0, y + 250.0))
|
|
links.new(profile.outputs["Attribute"], normalized.inputs[0])
|
|
links.new(safe_max.outputs[0], normalized.inputs[1])
|
|
smooth = nodes.new("ShaderNodeMapRange")
|
|
smooth.data_type = "FLOAT"
|
|
smooth.interpolation_type = "SMOOTHERSTEP"
|
|
smooth.clamp = True
|
|
smooth.inputs["From Min"].default_value = 0.0
|
|
smooth.inputs["From Max"].default_value = 1.0
|
|
smooth.inputs["To Min"].default_value = 0.0
|
|
smooth.inputs["To Max"].default_value = 1.0
|
|
smooth.location = (x + 650.0, y + 250.0)
|
|
links.new(normalized.outputs[0], smooth.inputs["Value"])
|
|
return profile.outputs["Attribute"], smooth.outputs["Result"]
|
|
|
|
|
|
def _extrude_layers(group, geometry, location):
|
|
node = group.nodes.new("GeometryNodeExtrudeMesh")
|
|
node.mode = "FACES"
|
|
node.inputs["Individual"].default_value = False
|
|
node.inputs["Offset"].default_value = (1.0, 0.0, 0.0)
|
|
# Keep distinct front and back vertices before overriding their X positions.
|
|
node.inputs["Offset Scale"].default_value = 1.0
|
|
node.location = location
|
|
group.links.new(geometry, node.inputs["Mesh"])
|
|
return node
|
|
|
|
|
|
def _offset_geometry_x(group, geometry, x_value, location):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
offset = nodes.new("ShaderNodeCombineXYZ")
|
|
offset.location = location
|
|
links.new(x_value, offset.inputs["X"])
|
|
set_position = nodes.new("GeometryNodeSetPosition")
|
|
set_position.location = (location[0] + 200.0, location[1])
|
|
links.new(geometry, set_position.inputs["Geometry"])
|
|
links.new(offset.outputs["Vector"], set_position.inputs["Offset"])
|
|
return set_position.outputs["Geometry"]
|
|
|
|
|
|
def _set_layer_x(group, geometry, extrude, front_x, back_x, location):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
choose_x = nodes.new("GeometryNodeSwitch")
|
|
choose_x.input_type = "FLOAT"
|
|
choose_x.location = (location[0], location[1] - 200.0)
|
|
links.new(extrude.outputs["Top"], choose_x.inputs["Switch"])
|
|
links.new(front_x, choose_x.inputs["False"])
|
|
links.new(back_x, choose_x.inputs["True"])
|
|
position = nodes.new("GeometryNodeInputPosition")
|
|
position.location = (location[0], location[1] + 150.0)
|
|
separate = nodes.new("ShaderNodeSeparateXYZ")
|
|
separate.location = (location[0] + 200.0, location[1] + 150.0)
|
|
links.new(position.outputs["Position"], separate.inputs["Vector"])
|
|
combine = nodes.new("ShaderNodeCombineXYZ")
|
|
combine.location = (location[0] + 400.0, location[1])
|
|
links.new(choose_x.outputs["Output"], combine.inputs["X"])
|
|
links.new(separate.outputs["Y"], combine.inputs["Y"])
|
|
links.new(separate.outputs["Z"], combine.inputs["Z"])
|
|
set_position = nodes.new("GeometryNodeSetPosition")
|
|
set_position.location = (location[0] + 600.0, location[1])
|
|
links.new(extrude.outputs["Mesh"], set_position.inputs["Geometry"])
|
|
links.new(combine.outputs["Vector"], set_position.inputs["Position"])
|
|
|
|
side = nodes.new("GeometryNodeSeparateGeometry")
|
|
side.domain = "FACE"
|
|
side.location = (location[0] + 850.0, location[1] + 200.0)
|
|
links.new(set_position.outputs["Geometry"], side.inputs["Geometry"])
|
|
links.new(extrude.outputs["Side"], side.inputs["Selection"])
|
|
flip_side = nodes.new("GeometryNodeFlipFaces")
|
|
flip_side.location = (location[0] + 850.0, location[1] + 400.0)
|
|
links.new(side.outputs["Selection"], flip_side.inputs["Mesh"])
|
|
front = _offset_geometry_x(
|
|
group, geometry, front_x, (location[0], location[1] - 450.0)
|
|
)
|
|
back = _offset_geometry_x(
|
|
group, geometry, back_x, (location[0], location[1] - 700.0)
|
|
)
|
|
flip_back = nodes.new("GeometryNodeFlipFaces")
|
|
flip_back.location = (location[0] + 500.0, location[1] - 700.0)
|
|
links.new(back, flip_back.inputs["Mesh"])
|
|
|
|
join = nodes.new("GeometryNodeJoinGeometry")
|
|
join.location = (location[0] + 1100.0, location[1])
|
|
links.new(front, join.inputs["Geometry"])
|
|
links.new(flip_back.outputs["Mesh"], join.inputs["Geometry"])
|
|
links.new(flip_side.outputs["Mesh"], join.inputs["Geometry"])
|
|
merge = nodes.new("GeometryNodeMergeByDistance")
|
|
merge.mode = "ALL"
|
|
merge.inputs["Distance"].default_value = 1e-6
|
|
merge.location = (location[0] + 1300.0, location[1])
|
|
links.new(join.outputs["Geometry"], merge.inputs["Geometry"])
|
|
return merge.outputs["Geometry"]
|
|
|
|
|
|
def _set_segmented_layer_x(
|
|
group,
|
|
geometry,
|
|
front_x,
|
|
back_x,
|
|
thickness,
|
|
location,
|
|
):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
x, y = location
|
|
|
|
edge_vertices = nodes.new("GeometryNodeInputMeshEdgeVertices")
|
|
edge_vertices.location = (x, y + 850.0)
|
|
edge_length = nodes.new("ShaderNodeVectorMath")
|
|
edge_length.operation = "DISTANCE"
|
|
edge_length.location = (x + 220.0, y + 850.0)
|
|
links.new(edge_vertices.outputs["Position 1"], edge_length.inputs[0])
|
|
links.new(edge_vertices.outputs["Position 2"], edge_length.inputs[1])
|
|
edge_neighbors = nodes.new("GeometryNodeInputMeshEdgeNeighbors")
|
|
edge_neighbors.location = (x, y + 650.0)
|
|
boundary = _math(nodes, "COMPARE", (x + 220.0, y + 650.0))
|
|
boundary.inputs[1].default_value = 1.0
|
|
boundary.inputs[2].default_value = 0.1
|
|
links.new(edge_neighbors.outputs["Face Count"], boundary.inputs[0])
|
|
statistics = nodes.new("GeometryNodeAttributeStatistic")
|
|
statistics.data_type = "FLOAT"
|
|
statistics.domain = "EDGE"
|
|
statistics.location = (x + 450.0, y + 750.0)
|
|
links.new(geometry, statistics.inputs["Geometry"])
|
|
links.new(boundary.outputs[0], statistics.inputs["Selection"])
|
|
links.new(edge_length.outputs["Value"], statistics.inputs["Attribute"])
|
|
safe_edge = _math(nodes, "MAXIMUM", (x + 680.0, y + 750.0))
|
|
safe_edge.inputs[1].default_value = 1e-6
|
|
links.new(statistics.outputs["Median"], safe_edge.inputs[0])
|
|
segment_ratio = _math(nodes, "DIVIDE", (x + 900.0, y + 750.0))
|
|
links.new(thickness, segment_ratio.inputs[0])
|
|
links.new(safe_edge.outputs[0], segment_ratio.inputs[1])
|
|
segment_count = nodes.new("FunctionNodeFloatToInt")
|
|
segment_count.rounding_mode = "CEILING"
|
|
segment_count.location = (x + 1120.0, y + 750.0)
|
|
links.new(segment_ratio.outputs[0], segment_count.inputs["Float"])
|
|
at_least_one = nodes.new("FunctionNodeIntegerMath")
|
|
at_least_one.operation = "MAXIMUM"
|
|
at_least_one.inputs[1].default_value = 1
|
|
at_least_one.location = (x + 1340.0, y + 750.0)
|
|
links.new(segment_count.outputs["Integer"], at_least_one.inputs[0])
|
|
segment_limit = nodes.new("FunctionNodeIntegerMath")
|
|
segment_limit.operation = "MINIMUM"
|
|
segment_limit.inputs[1].default_value = 128
|
|
segment_limit.location = (x + 1560.0, y + 750.0)
|
|
links.new(at_least_one.outputs[0], segment_limit.inputs[0])
|
|
step = _math(nodes, "DIVIDE", (x + 1560.0, y + 500.0))
|
|
links.new(thickness, step.inputs[0])
|
|
links.new(segment_limit.outputs[0], step.inputs[1])
|
|
|
|
front = _offset_geometry_x(group, geometry, front_x, (x, y + 100.0))
|
|
repeat_output = nodes.new("GeometryNodeRepeatOutput")
|
|
repeat_output.location = (x + 2380.0, y + 100.0)
|
|
repeat_input = nodes.new("GeometryNodeRepeatInput")
|
|
repeat_input.pair_with_output(repeat_output)
|
|
repeat_output.repeat_items.new("GEOMETRY", "Geometry")
|
|
repeat_output.repeat_items.new("GEOMETRY", "Sides")
|
|
repeat_input.location = (x + 1560.0, y + 100.0)
|
|
links.new(segment_limit.outputs[0], repeat_input.inputs["Iterations"])
|
|
links.new(geometry, repeat_input.inputs["Geometry"])
|
|
negative_step = _math(nodes, "MULTIPLY", (x + 1760.0, y - 100.0))
|
|
negative_step.inputs[1].default_value = -1.0
|
|
links.new(step.outputs[0], negative_step.inputs[0])
|
|
extrude = nodes.new("GeometryNodeExtrudeMesh")
|
|
extrude.mode = "FACES"
|
|
extrude.inputs["Individual"].default_value = False
|
|
extrude.inputs["Selection"].default_value = True
|
|
extrude.inputs["Offset"].default_value = (0.0, 0.0, 0.0)
|
|
extrude.location = (x + 1960.0, y + 100.0)
|
|
links.new(negative_step.outputs[0], extrude.inputs["Offset Scale"])
|
|
links.new(repeat_input.outputs["Geometry"], extrude.inputs["Mesh"])
|
|
split_extrude = nodes.new("GeometryNodeSeparateGeometry")
|
|
split_extrude.domain = "FACE"
|
|
split_extrude.location = (x + 2160.0, y + 100.0)
|
|
links.new(extrude.outputs["Mesh"], split_extrude.inputs["Geometry"])
|
|
links.new(extrude.outputs["Side"], split_extrude.inputs["Selection"])
|
|
join_sides = nodes.new("GeometryNodeJoinGeometry")
|
|
join_sides.location = (x + 2160.0, y - 150.0)
|
|
links.new(repeat_input.outputs["Sides"], join_sides.inputs["Geometry"])
|
|
links.new(split_extrude.outputs["Selection"], join_sides.inputs["Geometry"])
|
|
links.new(split_extrude.outputs["Inverted"], repeat_output.inputs["Geometry"])
|
|
links.new(join_sides.outputs["Geometry"], repeat_output.inputs["Sides"])
|
|
|
|
positioned_side = _offset_geometry_x(
|
|
group,
|
|
repeat_output.outputs["Sides"],
|
|
front_x,
|
|
(x + 2450.0, y + 350.0),
|
|
)
|
|
flip_side = nodes.new("GeometryNodeFlipFaces")
|
|
flip_side.location = (x + 2850.0, y + 350.0)
|
|
links.new(positioned_side, flip_side.inputs["Mesh"])
|
|
|
|
back = _offset_geometry_x(group, geometry, back_x, (x + 2450.0, y - 400.0))
|
|
flip_back = nodes.new("GeometryNodeFlipFaces")
|
|
flip_back.location = (x + 2850.0, y - 400.0)
|
|
links.new(back, flip_back.inputs["Mesh"])
|
|
join = nodes.new("GeometryNodeJoinGeometry")
|
|
join.location = (x + 3050.0, y + 50.0)
|
|
links.new(front, join.inputs["Geometry"])
|
|
links.new(flip_back.outputs["Mesh"], join.inputs["Geometry"])
|
|
links.new(flip_side.outputs["Mesh"], join.inputs["Geometry"])
|
|
merge = nodes.new("GeometryNodeMergeByDistance")
|
|
merge.mode = "ALL"
|
|
merge.inputs["Distance"].default_value = 1e-6
|
|
merge.location = (x + 3250.0, y + 50.0)
|
|
links.new(join.outputs["Geometry"], merge.inputs["Geometry"])
|
|
return merge.outputs["Geometry"]
|
|
|
|
|
|
def _set_geometry_x(group, geometry, x_value, location):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
position = nodes.new("GeometryNodeInputPosition")
|
|
position.location = location
|
|
separate = nodes.new("ShaderNodeSeparateXYZ")
|
|
separate.location = (location[0] + 200.0, location[1])
|
|
links.new(position.outputs["Position"], separate.inputs["Vector"])
|
|
combine = nodes.new("ShaderNodeCombineXYZ")
|
|
combine.location = (location[0] + 400.0, location[1])
|
|
links.new(x_value, combine.inputs["X"])
|
|
links.new(separate.outputs["Y"], combine.inputs["Y"])
|
|
links.new(separate.outputs["Z"], combine.inputs["Z"])
|
|
set_position = nodes.new("GeometryNodeSetPosition")
|
|
set_position.location = (location[0] + 600.0, location[1])
|
|
links.new(geometry, set_position.inputs["Geometry"])
|
|
links.new(combine.outputs["Vector"], set_position.inputs["Position"])
|
|
return set_position.outputs["Geometry"]
|
|
|
|
|
|
def _remove_zero_thickness(
|
|
group,
|
|
zero_geometry,
|
|
shaped_geometry,
|
|
thickness,
|
|
location,
|
|
):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
zero = _math(nodes, "LESS_THAN", (location[0], location[1] - 200.0))
|
|
zero.inputs[1].default_value = 1e-6
|
|
links.new(thickness, zero.inputs[0])
|
|
choose = nodes.new("GeometryNodeSwitch")
|
|
choose.input_type = "GEOMETRY"
|
|
choose.location = location
|
|
links.new(zero.outputs[0], choose.inputs["Switch"])
|
|
links.new(shaped_geometry, choose.inputs["False"])
|
|
links.new(zero_geometry, choose.inputs["True"])
|
|
return choose.outputs["Output"]
|
|
|
|
|
|
def _position_field(group, depth_image, location):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
uv = nodes.new("GeometryNodeInputNamedAttribute")
|
|
uv.data_type = "FLOAT_VECTOR"
|
|
uv.inputs["Name"].default_value = "UVMap"
|
|
uv.location = location
|
|
position_texture = nodes.new("GeometryNodeImageTexture")
|
|
position_texture.interpolation = "Linear"
|
|
position_texture.extension = "EXTEND"
|
|
position_texture.location = (location[0] + 220.0, location[1])
|
|
links.new(depth_image, position_texture.inputs["Image"])
|
|
links.new(uv.outputs["Attribute"], position_texture.inputs["Vector"])
|
|
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
|
|
normal = nodes.new("ShaderNodeVectorMath")
|
|
normal.operation = "NORMALIZE"
|
|
normal.location = location
|
|
links.new(direction, normal.inputs[0])
|
|
normal_components = nodes.new("ShaderNodeSeparateXYZ")
|
|
normal_components.location = (location[0] + 220.0, location[1])
|
|
links.new(normal.outputs["Vector"], normal_components.inputs["Vector"])
|
|
position = nodes.new("GeometryNodeInputPosition")
|
|
position.location = (location[0], location[1] + 450.0)
|
|
position_components = nodes.new("ShaderNodeSeparateXYZ")
|
|
position_components.location = (location[0] + 220.0, location[1] + 450.0)
|
|
links.new(position.outputs["Position"], position_components.inputs["Vector"])
|
|
direction_x = _math(
|
|
nodes,
|
|
"MULTIPLY",
|
|
(location[0] + 440.0, location[1] + 850.0),
|
|
)
|
|
direction_x.inputs[1].default_value = -1.0
|
|
links.new(normal_components.outputs["X"], direction_x.inputs[0])
|
|
y_term = _math(nodes, "MULTIPLY", (location[0] + 440.0, location[1] + 500.0))
|
|
links.new(position_components.outputs["Y"], y_term.inputs[0])
|
|
links.new(direction_x.outputs[0], y_term.inputs[1])
|
|
z_term = _math(nodes, "MULTIPLY", (location[0] + 440.0, location[1] + 250.0))
|
|
links.new(position_components.outputs["Z"], z_term.inputs[0])
|
|
links.new(normal_components.outputs["Y"], z_term.inputs[1])
|
|
point_dot = _math(nodes, "ADD", (location[0] + 660.0, location[1] + 400.0))
|
|
links.new(y_term.outputs[0], point_dot.inputs[0])
|
|
links.new(z_term.outputs[0], point_dot.inputs[1])
|
|
local_x = _math(nodes, "MULTIPLY", (location[0] + 660.0, location[1]))
|
|
local_x.inputs[1].default_value = -1.0
|
|
links.new(normal_components.outputs["Z"], local_x.inputs[0])
|
|
divide = _math(nodes, "DIVIDE", (location[0] + 880.0, location[1] + 200.0))
|
|
links.new(point_dot.outputs[0], divide.inputs[0])
|
|
links.new(local_x.outputs[0], divide.inputs[1])
|
|
plane = _math(nodes, "SUBTRACT", (location[0] + 1100.0, location[1] + 200.0))
|
|
links.new(reference_depth, plane.inputs[0])
|
|
links.new(divide.outputs[0], plane.inputs[1])
|
|
return plane.outputs[0]
|
|
|
|
|
|
def _shade_output(group, geometry, location):
|
|
shade = group.nodes.new("GeometryNodeSetShadeSmooth")
|
|
shade.domain = "FACE"
|
|
shade.inputs["Shade Smooth"].default_value = True
|
|
shade.location = location
|
|
output = group.nodes.new("NodeGroupOutput")
|
|
output.location = (location[0] + 250.0, location[1])
|
|
group.links.new(geometry, shade.inputs["Geometry"])
|
|
group.links.new(shade.outputs["Geometry"], output.inputs["Geometry"])
|
|
|
|
|
|
def _build_cutout(group, geometry, thickness):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
half = _math(nodes, "MULTIPLY", (-200.0, 50.0))
|
|
half.inputs[1].default_value = 0.5
|
|
links.new(thickness, half.inputs[0])
|
|
front = _math(nodes, "MULTIPLY", (20.0, 0.0))
|
|
front.inputs[1].default_value = -1.0
|
|
links.new(half.outputs[0], front.inputs[0])
|
|
shaped = _set_segmented_layer_x(
|
|
group,
|
|
geometry,
|
|
front.outputs[0],
|
|
half.outputs[0],
|
|
thickness,
|
|
(250.0, 300.0),
|
|
)
|
|
return _remove_zero_thickness(group, geometry, shaped, thickness, (3750.0, 300.0))
|
|
|
|
|
|
def _build_balloon(group, geometry, thickness):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
profile, _profile_factor = _profile_field(group, geometry, (-200.0, 0.0))
|
|
front = _math(nodes, "MULTIPLY", (2400.0, 500.0))
|
|
front.inputs[1].default_value = -1.0
|
|
links.new(profile, front.inputs[0])
|
|
front_scale = _math(nodes, "MULTIPLY", (2600.0, 500.0))
|
|
links.new(front.outputs[0], front_scale.inputs[0])
|
|
links.new(thickness, front_scale.inputs[1])
|
|
back = _math(nodes, "MULTIPLY", (2600.0, 300.0))
|
|
links.new(profile, back.inputs[0])
|
|
links.new(thickness, back.inputs[1])
|
|
extrude = _extrude_layers(group, geometry, (2400.0, 750.0))
|
|
shaped = _set_layer_x(
|
|
group,
|
|
geometry,
|
|
extrude,
|
|
front_scale.outputs[0],
|
|
back.outputs[0],
|
|
(2850.0, 650.0),
|
|
)
|
|
return _remove_zero_thickness(group, geometry, shaped, thickness, (4400.0, 650.0))
|
|
|
|
|
|
def _build_depth_balloon(
|
|
group,
|
|
geometry,
|
|
thickness,
|
|
depth_scale,
|
|
double_sided,
|
|
uniform_scale,
|
|
depth_direction,
|
|
depth_image,
|
|
depth_reference,
|
|
):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
profile, profile_factor = _profile_field(group, geometry, (-200.0, 250.0))
|
|
camera_position = _position_field(group, depth_image, (200.0, -250.0))
|
|
separate_camera = nodes.new("ShaderNodeSeparateXYZ")
|
|
separate_camera.location = (650.0, -250.0)
|
|
links.new(camera_position, separate_camera.inputs["Vector"])
|
|
metric_depth = _math(nodes, "MULTIPLY", (900.0, -250.0))
|
|
links.new(separate_camera.outputs["Z"], metric_depth.inputs[0])
|
|
links.new(uniform_scale, metric_depth.inputs[1])
|
|
plane_depth = _plane_depth(
|
|
group,
|
|
depth_direction,
|
|
depth_reference,
|
|
(600.0, -850.0),
|
|
)
|
|
corrected_depth = _math(nodes, "SUBTRACT", (1750.0, -250.0))
|
|
links.new(plane_depth, corrected_depth.inputs[0])
|
|
links.new(metric_depth.outputs[0], corrected_depth.inputs[1])
|
|
nonnegative_depth = _math(nodes, "MAXIMUM", (1950.0, -250.0))
|
|
nonnegative_depth.inputs[1].default_value = 0.0
|
|
links.new(corrected_depth.outputs[0], nonnegative_depth.inputs[0])
|
|
scaled_depth = _math(nodes, "MULTIPLY", (2150.0, -250.0))
|
|
links.new(nonnegative_depth.outputs[0], scaled_depth.inputs[0])
|
|
links.new(depth_scale, scaled_depth.inputs[1])
|
|
front_depth = _math(nodes, "MULTIPLY", (2350.0, -250.0))
|
|
front_depth.inputs[1].default_value = -1.0
|
|
links.new(scaled_depth.outputs[0], front_depth.inputs[0])
|
|
base_front = _math(nodes, "MULTIPLY", (2400.0, 500.0))
|
|
links.new(profile, base_front.inputs[0])
|
|
links.new(thickness, base_front.inputs[1])
|
|
negate_front = _math(nodes, "MULTIPLY", (2600.0, 500.0))
|
|
negate_front.inputs[1].default_value = -1.0
|
|
links.new(base_front.outputs[0], negate_front.inputs[0])
|
|
base_back = _math(nodes, "MULTIPLY", (2600.0, 300.0))
|
|
links.new(profile, base_back.inputs[0])
|
|
links.new(thickness, base_back.inputs[1])
|
|
front_mix = nodes.new("ShaderNodeMix")
|
|
front_mix.data_type = "FLOAT"
|
|
front_mix.clamp_factor = True
|
|
front_mix.location = (2600.0, 50.0)
|
|
links.new(profile_factor, front_mix.inputs["Factor"])
|
|
links.new(negate_front.outputs[0], front_mix.inputs["A"])
|
|
links.new(front_depth.outputs[0], front_mix.inputs["B"])
|
|
mirrored_front = _math(nodes, "MULTIPLY", (2850.0, -150.0))
|
|
mirrored_front.inputs[1].default_value = -1.0
|
|
links.new(front_mix.outputs["Result"], mirrored_front.inputs[0])
|
|
back_choice = nodes.new("GeometryNodeSwitch")
|
|
back_choice.input_type = "FLOAT"
|
|
back_choice.location = (3050.0, 100.0)
|
|
links.new(double_sided, back_choice.inputs["Switch"])
|
|
links.new(base_back.outputs[0], back_choice.inputs["False"])
|
|
links.new(mirrored_front.outputs[0], back_choice.inputs["True"])
|
|
extrude = _extrude_layers(group, geometry, (2400.0, 750.0))
|
|
shaped = _set_layer_x(
|
|
group,
|
|
geometry,
|
|
extrude,
|
|
front_mix.outputs["Result"],
|
|
back_choice.outputs["Output"],
|
|
(3300.0, 650.0),
|
|
)
|
|
front_only = _set_geometry_x(
|
|
group, geometry, front_mix.outputs["Result"], (3500.0, 100.0)
|
|
)
|
|
return _remove_zero_thickness(group, front_only, shaped, thickness, (4850.0, 600.0))
|
|
|
|
|
|
def _build_depth_surface(
|
|
group,
|
|
geometry,
|
|
thickness,
|
|
depth_scale,
|
|
stretch_limit,
|
|
uniform_scale,
|
|
reference_depth,
|
|
depth_image,
|
|
):
|
|
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),
|
|
)
|
|
camera_position = _position_field(group, depth_image, (-200.0, -200.0))
|
|
scaled = nodes.new("ShaderNodeVectorMath")
|
|
scaled.operation = "SCALE"
|
|
scaled.location = (3100.0, -100.0)
|
|
links.new(camera_position, scaled.inputs[0])
|
|
links.new(uniform_scale, scaled.inputs[3])
|
|
separate = nodes.new("ShaderNodeSeparateXYZ")
|
|
separate.location = (3300.0, -100.0)
|
|
links.new(scaled.outputs["Vector"], separate.inputs["Vector"])
|
|
relative_depth = _math(nodes, "SUBTRACT", (3500.0, -100.0))
|
|
links.new(separate.outputs["Z"], relative_depth.inputs[0])
|
|
links.new(reference_depth, relative_depth.inputs[1])
|
|
front = _math(nodes, "MULTIPLY", (3700.0, -100.0))
|
|
links.new(relative_depth.outputs[0], front.inputs[0])
|
|
links.new(depth_scale, front.inputs[1])
|
|
|
|
negative_x = _math(nodes, "MULTIPLY", (3500.0, -350.0))
|
|
negative_x.inputs[1].default_value = -1.0
|
|
links.new(separate.outputs["X"], negative_x.inputs[0])
|
|
negative_y = _math(nodes, "MULTIPLY", (3500.0, -600.0))
|
|
negative_y.inputs[1].default_value = -1.0
|
|
links.new(separate.outputs["Y"], negative_y.inputs[0])
|
|
projected = nodes.new("ShaderNodeCombineXYZ")
|
|
projected.location = (3700.0, -450.0)
|
|
links.new(negative_x.outputs[0], projected.inputs["Y"])
|
|
links.new(negative_y.outputs[0], projected.inputs["Z"])
|
|
current_position = nodes.new("GeometryNodeInputPosition")
|
|
current_position.location = (3500.0, -850.0)
|
|
blend = nodes.new("ShaderNodeMix")
|
|
blend.data_type = "VECTOR"
|
|
blend.clamp_factor = False
|
|
blend.location = (3900.0, -450.0)
|
|
links.new(depth_scale, blend.inputs[0])
|
|
links.new(current_position.outputs["Position"], blend.inputs[4])
|
|
links.new(projected.outputs["Vector"], blend.inputs[5])
|
|
blended_axes = nodes.new("ShaderNodeSeparateXYZ")
|
|
blended_axes.location = (4100.0, -500.0)
|
|
links.new(blend.outputs["Result"], blended_axes.inputs["Vector"])
|
|
surface_position = nodes.new("ShaderNodeCombineXYZ")
|
|
surface_position.location = (4100.0, -200.0)
|
|
links.new(front.outputs[0], surface_position.inputs["X"])
|
|
links.new(blended_axes.outputs["Y"], surface_position.inputs["Y"])
|
|
links.new(blended_axes.outputs["Z"], surface_position.inputs["Z"])
|
|
project_geometry = nodes.new("GeometryNodeSetPosition")
|
|
project_geometry.location = (4400.0, 400.0)
|
|
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_x = nodes.new("ShaderNodeValue")
|
|
zero_x.location = (7100.0, 100.0)
|
|
zero_x.outputs["Value"].default_value = 0.0
|
|
geometry = _set_geometry_x(
|
|
group,
|
|
geometry,
|
|
zero_x.outputs["Value"],
|
|
(7200.0, 400.0),
|
|
)
|
|
|
|
back = _math(nodes, "ADD", (3900.0, -100.0))
|
|
links.new(front.outputs[0], back.inputs[0])
|
|
links.new(thickness, back.inputs[1])
|
|
shaped = _set_segmented_layer_x(
|
|
group,
|
|
geometry,
|
|
front.outputs[0],
|
|
back.outputs[0],
|
|
thickness,
|
|
(8300.0, 400.0),
|
|
)
|
|
front_only = _set_geometry_x(
|
|
group,
|
|
geometry,
|
|
front.outputs[0],
|
|
(8500.0, -200.0),
|
|
)
|
|
geometry = _remove_zero_thickness(
|
|
group, front_only, shaped, thickness, (11800.0, 400.0)
|
|
)
|
|
return geometry
|
|
|
|
|
|
def build_node_group(shape):
|
|
group_name = CUTOUT_NODE_GROUP_NAMES[shape]
|
|
group = bpy.data.node_groups.get(group_name)
|
|
if group is not None:
|
|
return group
|
|
group = bpy.data.node_groups.new(group_name, "GeometryNodeTree")
|
|
group.is_modifier = False
|
|
_socket(group, "Geometry", "INPUT", "NodeSocketGeometry")
|
|
if shape in {"BALLOON", "DEPTH_BALLOON"}:
|
|
thickness = _float_input(group, "Thickness", 1.0)
|
|
thickness.description = (
|
|
"Multiplier for the injected Poisson Balloon profile; zero keeps "
|
|
"the source sheet"
|
|
)
|
|
else:
|
|
thickness = _float_input(group, "Thickness", 0.0, 0.0, None, subtype="DISTANCE")
|
|
thickness.description = "Final thickness in scene distance"
|
|
|
|
depth_image = None
|
|
stretch_limit = None
|
|
if shape == "DEPTH_BALLOON":
|
|
depth_scale = _float_input(group, "Depth Scale", 1.0)
|
|
depth_scale.description = "Scale the fitted-plane-corrected metric depth"
|
|
double_sided = _bool_input(group, "Double Sided", False)
|
|
double_sided.description = (
|
|
"Mirror the complete front surface across the local YZ plane"
|
|
)
|
|
elif shape == "DEPTH_SURFACE":
|
|
depth_scale = _float_input(group, "Depth Scale", 1.0)
|
|
|
|
if shape == "DEPTH_BALLOON":
|
|
options = group.interface.new_panel(name="Options", default_closed=True)
|
|
reference_depth = _float_input(
|
|
group,
|
|
"Reference Depth",
|
|
1.0,
|
|
0.0,
|
|
None,
|
|
subtype="DISTANCE",
|
|
parent=options,
|
|
)
|
|
reference_depth.description = (
|
|
"Shared upper-percentile metric depth for outward placement"
|
|
)
|
|
depth_direction = _vector_input(
|
|
group,
|
|
"Depth Direction",
|
|
(0.0, 0.0, 1.0),
|
|
subtype="DIRECTION",
|
|
parent=options,
|
|
)
|
|
depth_direction.description = (
|
|
"Normal direction of the fitted metric depth plane"
|
|
)
|
|
elif shape == "DEPTH_SURFACE":
|
|
options = group.interface.new_panel(name="Options", default_closed=True)
|
|
stretch_limit = _float_input(
|
|
group,
|
|
"Stretch Limit",
|
|
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"
|
|
)
|
|
reference_depth = _float_input(
|
|
group,
|
|
"Reference Depth",
|
|
1.0,
|
|
0.0,
|
|
None,
|
|
subtype="DISTANCE",
|
|
parent=options,
|
|
)
|
|
reference_depth.description = (
|
|
"Scaled camera depth placed on the original image sheet"
|
|
)
|
|
|
|
if shape in {"DEPTH_BALLOON", "DEPTH_SURFACE"}:
|
|
data = group.interface.new_panel(name="Data", default_closed=True)
|
|
uniform_scale = _float_input(group, "Uniform Scale", 1.0, 0.0, parent=data)
|
|
uniform_scale.description = (
|
|
"Internal uniform conversion from MoGe meters to local distance"
|
|
)
|
|
uniform_scale.hide_in_modifier = True
|
|
depth_image = _socket(group, "Depth Image", "INPUT", "NodeSocketImage", data)
|
|
depth_image.description = (
|
|
f"Internally generated {CUTOUT_SHAPE_LABELS[shape]} data"
|
|
)
|
|
depth_image.hide_in_modifier = True
|
|
_socket(group, "Geometry", "OUTPUT", "NodeSocketGeometry")
|
|
|
|
controls = _group_input(
|
|
group.nodes,
|
|
(-1600.0, 500.0),
|
|
{
|
|
"Geometry",
|
|
"Thickness",
|
|
"Double Sided",
|
|
"Depth Image",
|
|
"Depth Scale",
|
|
"Uniform Scale",
|
|
"Reference Depth",
|
|
"Depth Direction",
|
|
"Stretch Limit",
|
|
},
|
|
)
|
|
geometry = controls.outputs["Geometry"]
|
|
if shape == "SURFACE":
|
|
geometry = _build_cutout(group, geometry, controls.outputs["Thickness"])
|
|
output_location = (4050.0, 300.0)
|
|
elif shape == "BALLOON":
|
|
geometry = _build_balloon(
|
|
group,
|
|
geometry,
|
|
controls.outputs["Thickness"],
|
|
)
|
|
output_location = (4650.0, 650.0)
|
|
elif shape == "DEPTH_BALLOON":
|
|
geometry = _build_depth_balloon(
|
|
group,
|
|
geometry,
|
|
controls.outputs["Thickness"],
|
|
controls.outputs["Depth Scale"],
|
|
controls.outputs["Double Sided"],
|
|
controls.outputs["Uniform Scale"],
|
|
controls.outputs["Depth Direction"],
|
|
controls.outputs["Depth Image"],
|
|
controls.outputs["Reference Depth"],
|
|
)
|
|
output_location = (5100.0, 600.0)
|
|
else:
|
|
geometry = _build_depth_surface(
|
|
group,
|
|
geometry,
|
|
controls.outputs["Thickness"],
|
|
controls.outputs["Depth Scale"],
|
|
controls.outputs["Stretch Limit"],
|
|
controls.outputs["Uniform Scale"],
|
|
controls.outputs["Reference Depth"],
|
|
controls.outputs["Depth Image"],
|
|
)
|
|
output_location = (12100.0, 400.0)
|
|
_localize_group_inputs(group, controls)
|
|
_shade_output(group, geometry, output_location)
|
|
group.color_tag = "GEOMETRY"
|
|
group.description = {
|
|
"SURFACE": "Symmetrically thicken a Surface sheet.",
|
|
"BALLOON": "Inflate a Surface sheet from its boundary profile.",
|
|
"DEPTH_BALLOON": (
|
|
"Mix Balloon profile and metric image depth around a fitted plane."
|
|
),
|
|
"DEPTH_SURFACE": (
|
|
"Project and thicken a Surface sheet from hidden camera points."
|
|
),
|
|
}[shape]
|
|
group.use_fake_user = True
|
|
return group
|
|
|
|
|
|
def build_cutout_modifier_group(shape_groups):
|
|
group = bpy.data.node_groups.get(CUTOUT_NODE_GROUP_NAME)
|
|
if group is not None:
|
|
return group
|
|
group = bpy.data.node_groups.new(CUTOUT_NODE_GROUP_NAME, "GeometryNodeTree")
|
|
group.is_modifier = True
|
|
_socket(group, "Geometry", "INPUT", "NodeSocketGeometry")
|
|
_socket(group, "Shape", "INPUT", "NodeSocketMenu")
|
|
inward_axis = _socket(group, "Inward Axis", "INPUT", "NodeSocketMenu")
|
|
inward_axis.hide_in_modifier = True
|
|
|
|
thickness = _float_input(group, "Thickness", 0.0, 0.0)
|
|
thickness.description = (
|
|
"Scene thickness for Surface shapes or Balloon profile multiplier"
|
|
)
|
|
depth_scale = _float_input(group, "Depth Scale", 1.0, 0.0)
|
|
depth_scale.description = "Scale metric image depth"
|
|
double_sided = _bool_input(group, "Double Sided", False)
|
|
double_sided.description = (
|
|
"Mirror the complete Depth Balloon front surface across the local YZ plane"
|
|
)
|
|
|
|
iterations = _socket(group, "Smooth Iterations", "INPUT", "NodeSocketInt")
|
|
iterations.default_value = 0
|
|
iterations.min_value = 0
|
|
iterations.max_value = 20
|
|
iterations.description = "Number of smoothing passes"
|
|
options = group.interface.new_panel(name="Options", default_closed=True)
|
|
cleanup = _float_input(
|
|
group,
|
|
"Cleanup",
|
|
0.0,
|
|
0.0,
|
|
None,
|
|
parent=options,
|
|
)
|
|
cleanup.description = "Delete mesh islands below this absolute local-area threshold"
|
|
smooth = _float_input(
|
|
group,
|
|
"Smooth",
|
|
1.0,
|
|
0.0,
|
|
1.0,
|
|
subtype="FACTOR",
|
|
parent=options,
|
|
)
|
|
smooth.description = "Blend each smoothing pass into the selected shape"
|
|
stretch_limit = _float_input(
|
|
group,
|
|
"Stretch Limit",
|
|
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"
|
|
)
|
|
|
|
reference_depth = _float_input(
|
|
group,
|
|
"Reference Depth",
|
|
1.0,
|
|
0.0,
|
|
None,
|
|
subtype="DISTANCE",
|
|
parent=options,
|
|
)
|
|
reference_depth.description = "Depth of the flat base plane for both Depth shapes"
|
|
depth_direction = _vector_input(
|
|
group,
|
|
"Depth Direction",
|
|
(0.0, 0.0, 1.0),
|
|
subtype="DIRECTION",
|
|
parent=options,
|
|
)
|
|
depth_direction.description = "Normal direction of the fitted metric depth plane"
|
|
|
|
data = group.interface.new_panel(name="Data", default_closed=True)
|
|
uniform_scale = _float_input(group, "Uniform Scale", 1.0, 0.0, parent=data)
|
|
uniform_scale.description = (
|
|
"Internal uniform conversion from MoGe meters to local distance"
|
|
)
|
|
uniform_scale.hide_in_modifier = True
|
|
depth_image = _socket(group, "Depth Image", "INPUT", "NodeSocketImage", data)
|
|
depth_image.description = "Internally generated Cutout depth data"
|
|
depth_image.hide_in_modifier = True
|
|
_socket(group, "Geometry", "OUTPUT", "NodeSocketGeometry")
|
|
|
|
base_controls = _group_input(
|
|
group.nodes,
|
|
(-2700.0, 500.0),
|
|
{"Geometry", "Cleanup"},
|
|
)
|
|
cleanup_area = _math(group.nodes, "MULTIPLY", (-2500.0, 750.0))
|
|
cleanup_area.inputs[1].default_value = 0.01
|
|
group.links.new(base_controls.outputs["Cleanup"], cleanup_area.inputs[0])
|
|
geometry = _island_cleanup(
|
|
group,
|
|
base_controls.outputs["Geometry"],
|
|
cleanup_area.outputs[0],
|
|
(-2350.0, 250.0),
|
|
)
|
|
|
|
branch_y = {
|
|
"SURFACE": 1150.0,
|
|
"BALLOON": 550.0,
|
|
"DEPTH_BALLOON": -50.0,
|
|
"DEPTH_SURFACE": -650.0,
|
|
}
|
|
branch_nodes = {}
|
|
for shape, y in branch_y.items():
|
|
node = group.nodes.new("GeometryNodeGroup")
|
|
node.node_tree = shape_groups[shape]
|
|
node.location = (-850.0, y)
|
|
node.width = 220.0
|
|
group.links.new(geometry, node.inputs["Geometry"])
|
|
visible = {
|
|
socket.name
|
|
for socket in node.inputs
|
|
if socket.name and socket.name != "Geometry"
|
|
}
|
|
controls = _group_input(group.nodes, (-1200.0, y + 120.0), visible)
|
|
for socket in node.inputs:
|
|
if not socket.name or socket.name == "Geometry":
|
|
continue
|
|
group.links.new(controls.outputs[socket.name], socket)
|
|
branch_nodes[shape] = node
|
|
|
|
menu_switch = group.nodes.new("GeometryNodeMenuSwitch")
|
|
menu_switch.data_type = "GEOMETRY"
|
|
shapes = list(CUTOUT_NODE_GROUP_NAMES)
|
|
for item, shape in zip(menu_switch.enum_items, shapes[:2]):
|
|
item.name = CUTOUT_SHAPE_LABELS[shape]
|
|
for shape in shapes[2:]:
|
|
menu_switch.enum_items.new(CUTOUT_SHAPE_LABELS[shape])
|
|
menu_switch.location = (-350.0, 250.0)
|
|
|
|
shape_controls = _group_input(
|
|
group.nodes,
|
|
(-700.0, -1200.0),
|
|
{"Shape"},
|
|
)
|
|
group.links.new(shape_controls.outputs["Shape"], menu_switch.inputs["Menu"])
|
|
for shape, node in branch_nodes.items():
|
|
group.links.new(
|
|
node.outputs["Geometry"],
|
|
menu_switch.inputs[CUTOUT_SHAPE_LABELS[shape]],
|
|
)
|
|
|
|
geometry = expand_smooth(
|
|
group,
|
|
menu_switch.outputs["Output"],
|
|
iterations_name="Smooth Iterations",
|
|
)
|
|
from mathutils import Matrix
|
|
|
|
orient = group.nodes.new("GeometryNodeTransform")
|
|
orient.inputs["Rotation"].default_value = Matrix(CUTOUT_AXIS_TO_SOURCE).to_euler()
|
|
orient.location = (3300.0, 350.0)
|
|
group.links.new(geometry, orient.inputs["Geometry"])
|
|
|
|
axis_switch = group.nodes.new("GeometryNodeMenuSwitch")
|
|
axis_switch.data_type = "GEOMETRY"
|
|
axis_switch.enum_items[0].name = "-Z"
|
|
axis_switch.enum_items[1].name = "+X"
|
|
axis_switch.location = (3550.0, 550.0)
|
|
axis_controls = _group_input(
|
|
group.nodes,
|
|
(3300.0, -650.0),
|
|
{"Inward Axis"},
|
|
)
|
|
group.links.new(axis_controls.outputs["Inward Axis"], axis_switch.inputs["Menu"])
|
|
group.links.new(orient.outputs["Geometry"], axis_switch.inputs["-Z"])
|
|
group.links.new(geometry, axis_switch.inputs["+X"])
|
|
_shade_output(group, axis_switch.outputs["Output"], (3850.0, 550.0))
|
|
group.color_tag = "GEOMETRY"
|
|
group.description = (
|
|
"Build Surface, Balloon, Depth Balloon, or Depth Surface from one "
|
|
"menu-controlled Cutout modifier."
|
|
)
|
|
group.use_fake_user = True
|
|
return group
|