"""Build the three directly editable Cutout modifier assets.""" import bpy from types import SimpleNamespace from mathutils import Matrix from anyimage.operators.cutout_tool.shape import CUTOUT_NODE_GROUP_NAMES, CUTOUT_Z_TO_X, NORMAL_SCALE_ATTRIBUTE_NAME from cutout_shapes import ( _socket, _float_input, _bool_input, _vector_input, _group_input, _math, _island_cleanup, _build_cutout, _build_balloon, _build_depth_balloon, _build_depth_surface, _shade_output, ) from cutout_shape_layouts import LAYOUTS from cutout_depth_surface_layout import POSITIONS, INPUT_REGIONS, layout_geometry_references from cutout_layout import layout_stage, finish_layout from cutout_volume import build_volume_fields, shift_stage from cutout_rounding import build_volume_rounding from cutout_inflation import build_front_fields from cutout_shell import build_front_normals, build_shell_position from node_graph import expand_smooth def _menu(group, control, names, kind, location): selector = next((node for node in group.nodes if node.bl_idname == "GeometryNodeMenuSwitch" and node.inputs["Menu"].is_linked and node.inputs["Menu"].links[0].from_socket.name == control), None) if selector is None: selector = group.nodes.new("GeometryNodeMenuSwitch") selector.data_type = "BOOLEAN" selector.location = (location[0] - 240, location[1] + 450) for item, name in zip(selector.enum_items, names): item.name = name selector.inputs[names[0]].default_value = False selector.inputs[names[1]].default_value = True local = _group_input(group.nodes, (location[0] - 480, location[1] + 450), {control}) group.links.new(local.outputs[control], selector.inputs["Menu"]) node = group.nodes.new("GeometryNodeSwitch") node.input_type = kind node.location = location group.links.new(selector.outputs[0], node.inputs["Switch"]) return SimpleNamespace(inputs={names[0]: node.inputs["False"], names[1]: node.inputs["True"]}, outputs=node.outputs) def _read(group, name, location): node = group.nodes.new("GeometryNodeInputNamedAttribute") node.data_type = "FLOAT" node.inputs["Name"].default_value = name node.location = location return node.outputs["Attribute"] def _store(group, geometry, name, value, location, domain="POINT"): node = group.nodes.new("GeometryNodeStoreNamedAttribute") node.domain, node.data_type = domain, "FLOAT" node.location = location node.inputs["Name"].default_value = name group.links.new(geometry, node.inputs["Geometry"]) if isinstance(value, (int, float)): node.inputs["Value"].default_value = value else: group.links.new(value, node.inputs["Value"]) return node.outputs["Geometry"] def _depth_volume(group, stage): """Insert the volume field ahead of identity-based shell construction.""" links = group.links projection = stage["Set Position"].outputs["Geometry"] shell_input = stage["Store Named Attribute.005"].inputs["Geometry"] for node in group.nodes: if node.location.x >= 4150: node.location.x += 5100 projection = build_front_normals(group, projection) volume = build_volume_fields(group, projection, (4150, 0)) shared = build_front_fields(group, volume.node.inputs["Geometry"].links[0].from_socket) links.new(shared, volume.node.inputs["Geometry"]) uniform_control = _group_input(group.nodes, (8210, -330), {"Uniform Thickness"}) uniform = _store(group, shared, "o_balloon_surface_thickness", uniform_control.outputs["Uniform Thickness"], (8490, -330)) volume.node.location.x = uniform.node.location.x choose = _menu(group, "Mode", ("Balloon", "Uniform"), "GEOMETRY", (9000, 0)) links.new(volume, choose.inputs["Balloon"]) links.new(uniform, choose.inputs["Uniform"]) links.new(choose.outputs[0], shell_input) thickness = _read(group, "o_balloon_surface_thickness", (12000, -1100)) links.new(thickness, stage["Math.018"].inputs[1]) control = _group_input(group.nodes, (13300, 700), {"Thickness", "Uniform Thickness"}) amount = _menu(group, "Mode", ("Balloon", "Uniform"), "FLOAT", (13600, 500)) links.new(control.outputs["Thickness"], amount.inputs["Balloon"]) links.new(control.outputs["Uniform Thickness"], amount.inputs["Uniform"]) links.new(amount.outputs[0], stage["Math.019"].inputs[0]) identity_thickness = _read(group, "o_balloon_surface_thickness", (11100, 1500)) positive = _math(group.nodes, "GREATER_THAN", (11340, 1500)) positive.inputs[1].default_value = 1e-12 links.new(identity_thickness, positive.inputs[0]) layer = _math(group.nodes, "MULTIPLY", (11570, 1200)) links.new(positive.outputs[0], layer.inputs[0]) links.new(stage["Separate XYZ.006"].outputs["Z"], layer.inputs[1]) links.new(layer.outputs[0], stage["Combine XYZ.002"].inputs["Z"]) for node in group.nodes: if node.location.x >= 12070: node.location.x += 400 for name in ("Flip Faces", "Flip Faces.001"): source = stage[name].outputs["Mesh"] outgoing = [link for link in links if link.from_socket == source] targets = [link.to_socket for link in outgoing] for link in outgoing: links.remove(link) masked = _store(group, source, NORMAL_SCALE_ATTRIBUTE_NAME, 0.0, (12100, stage[name].location.y), domain="FACE") for target in targets: links.new(masked, target) for node in group.nodes: if node.location.x >= 8490: node.location.x += 3200 volume.node.location.x = 8290 rounded = build_volume_rounding(group, volume, (8600, 0)) rounded.node.location.x = uniform.node.location.x + 40 links.new(rounded, choose.inputs["Balloon"]) restored = stage["Vector Math.004"].outputs[0] targets = [link.to_socket for link in restored.links] uniform_mode = choose.outputs[0].node.inputs["Switch"].links[0].from_socket oriented = build_shell_position(group, restored, uniform_mode, (14100, -2100)) for target in targets: links.new(oriented, target) def build_cutout_group(shape): name = CUTOUT_NODE_GROUP_NAMES[shape] existing = bpy.data.node_groups.get(name) if existing is not None: return existing group = bpy.data.node_groups.new(name, "GeometryNodeTree") group.is_modifier, group.use_fake_user = True, True group.color_tag = "GEOMETRY" group["anyimage_asset_revision"] = 11 depth = shape in {"DEPTH_SOLID", "DEPTH_BALLOON"} dual = shape != "DEPTH_BALLOON" _socket(group, "Geometry", "INPUT", "NodeSocketGeometry") if dual: _socket(group, "Mode", "INPUT", "NodeSocketMenu") _float_input(group, "Thickness", 1.0).description = "Balloon profile multiplier; zero returns the sheet" if dual: _float_input(group, "Uniform Thickness", 0.0, subtype="DISTANCE").description = "Distance used only by Uniform thickness mode" if depth: _float_input(group, "Depth Scale", 1.0) if shape == "DEPTH_BALLOON": _bool_input(group, "Double Sided", True) iterations = _socket(group, "Smooth", "INPUT", "NodeSocketInt") iterations.default_value = 0 iterations.min_value, iterations.max_value = 0, 20 options = group.interface.new_panel(name="Options", default_closed=True) _float_input(group, "Cleanup Threshold", 0.0, parent=options) if shape == "DEPTH_SOLID": _float_input(group, "Split Threshold", 0.0, maximum=1, subtype="FACTOR", parent=options) if depth: _float_input(group, "Reference Depth", 1.0, subtype="DISTANCE", parent=options) if shape == "DEPTH_SOLID": _float_input(group, "Edge Round", 0.2, maximum=1, subtype="FACTOR", parent=options) _float_input(group, "Front Inflation", 1.0, maximum=32, parent=options) normal_iterations = _socket(group, "Normal Smooth", "INPUT", "NodeSocketInt", options) normal_iterations.default_value = 50 normal_iterations.min_value, normal_iterations.max_value = 0, 50 _float_input(group, "Smooth Weight", 1.0, maximum=1, subtype="FACTOR", parent=options) if shape == "DEPTH_BALLOON": _vector_input(group, "Depth Direction", (0, 0, 1), subtype="DIRECTION", parent=options) data = group.interface.new_panel(name="Data", default_closed=True) _socket(group, "Inward Axis", "INPUT", "NodeSocketMenu", data).hide_in_modifier = True if depth: _float_input(group, "Uniform Scale", 1.0, parent=data).hide_in_modifier = True _socket(group, "Depth Image", "INPUT", "NodeSocketImage", data).hide_in_modifier = True _socket(group, "Geometry", "OUTPUT", "NodeSocketGeometry") controls = _group_input(group.nodes, (-5000, 0), set()) previous = set(group.nodes) if shape == "DEPTH_SOLID": geometry = _build_depth_surface(group, controls.outputs["Geometry"], controls.outputs["Thickness"], controls.outputs["Depth Scale"], controls.outputs["Split Threshold"], controls.outputs["Uniform Scale"], controls.outputs["Reference Depth"], controls.outputs["Depth Image"]) stage = layout_stage(group, previous, POSITIONS, INPUT_REGIONS) _depth_volume(group, stage) end = 18000 elif shape == "DEPTH_BALLOON": geometry = _build_depth_balloon(group, controls.outputs["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"]) layout_stage(group, previous, *LAYOUTS["DEPTH_BALLOON"]) end = 3000 else: balloon = _build_balloon(group, controls.outputs["Geometry"], controls.outputs["Thickness"]) layout_stage(group, previous, *LAYOUTS["BALLOON"]) previous = set(group.nodes) uniform = _build_cutout(group, controls.outputs["Geometry"], controls.outputs["Uniform Thickness"]) layout_stage(group, previous, *LAYOUTS["UNIFORM"], offset=(0, -2400)) balloon.node.location.x = uniform.node.location.x choose = _menu(group, "Mode", ("Balloon", "Uniform"), "GEOMETRY", (3200, 0)) group.links.new(balloon, choose.inputs["Balloon"]) group.links.new(uniform, choose.inputs["Uniform"]) geometry, end = choose.outputs[0], 3700 planar_inputs = [link.to_socket for link in group.links if link.from_node.bl_idname == "NodeGroupInput" and link.from_socket.name == "Geometry"] base = _group_input(group.nodes, (-4800, 0), {"Geometry", "Cleanup Threshold"}) area = _math(group.nodes, "MULTIPLY", (-4520, 500)) area.inputs[1].default_value = 0.01 group.links.new(base.outputs["Cleanup Threshold"], area.inputs[0]) cleaned = _island_cleanup(group, base.outputs["Geometry"], area.outputs[0], (-4520, 0)) if shape == "DEPTH_SOLID": cleaned = _store(group, cleaned, NORMAL_SCALE_ATTRIBUTE_NAME, 1.0, (-3540, 0), domain="FACE") for target in planar_inputs: group.links.new(cleaned, target) previous = set(group.nodes) geometry = expand_smooth(group, geometry, iterations_name="Smooth", weight_name="Smooth Weight") shift_stage(group, previous, (end, -550)) end += 3450 if shape == "DEPTH_SOLID": remove = group.nodes.new("GeometryNodeRemoveAttribute") remove.pattern_mode = "WILDCARD" remove.inputs["Name"].default_value = "o_balloon_surface_*" remove.location = (end, 0) group.links.new(geometry, remove.inputs["Geometry"]) geometry, end = remove.outputs[0], end + 300 else: profile = _read(group, "o_balloon", (end, 800)) scale = profile if dual: amount = _group_input(group.nodes, (end, 1100), {"Thickness"}) enabled = _math(group.nodes, "GREATER_THAN", (end + 250, 1100)) enabled.inputs[1].default_value = 1e-6 group.links.new(amount.outputs["Thickness"], enabled.inputs[0]) inflated = group.nodes.new("GeometryNodeSwitch") inflated.input_type = "FLOAT" inflated.location = (end + 480, 800) inflated.inputs["False"].default_value = 1 group.links.new(enabled.outputs[0], inflated.inputs["Switch"]) group.links.new(profile, inflated.inputs["True"]) mode = _menu(group, "Mode", ("Balloon", "Uniform"), "FLOAT", (end + 750, 800)) mode.inputs["Uniform"].default_value = 1 group.links.new(inflated.outputs[0], mode.inputs["Balloon"]) scale = mode.outputs[0] geometry = _store(group, geometry, NORMAL_SCALE_ATTRIBUTE_NAME, scale, (end + 1050, 0)) end += 1350 orient = group.nodes.new("GeometryNodeTransform") orient.location = (end, -350) orient.inputs["Rotation"].default_value = Matrix(CUTOUT_Z_TO_X).to_euler() group.links.new(geometry, orient.inputs["Geometry"]) axis = _menu(group, "Inward Axis", ("-Z", "+X"), "GEOMETRY", (end + 300, 0)) group.links.new(geometry, axis.inputs["-Z"]) group.links.new(orient.outputs[0], axis.inputs["+X"]) _shade_output(group, axis.outputs[0], (end + 600, 0)) if shape == "DEPTH_SOLID": layout_geometry_references(group, stage) finish_layout(group) for item in group.interface.items_tree: if item.item_type != "SOCKET" or item.in_out != "INPUT": continue if item.name == "Mode": item.default_value = "Balloon" elif item.name == "Inward Axis": item.default_value = "-Z" elif item.name == "Uniform Thickness": item.name = "Thickness" return group