"""Build geometry calculations shared by Cutout assets.""" from .nodes import math_node from anyimage.operators.cutout_tool.shape import BALLOON_ATTRIBUTE_NAME def _island_cleanup(group, geometry, cleanup): nodes = group.nodes links = group.links mesh_island = nodes.new("GeometryNodeInputMeshIsland") face_area = nodes.new("GeometryNodeInputMeshFaceArea") island_area = nodes.new("GeometryNodeAccumulateField") island_area.data_type = "FLOAT" island_area.domain = "FACE" links.new(face_area.outputs["Area"], island_area.inputs["Value"]) links.new(mesh_island.outputs["Island Index"], island_area.inputs["Group ID"]) tiny = math_node(nodes, "LESS_THAN") links.new(island_area.outputs["Total"], tiny.inputs[0]) links.new(cleanup, tiny.inputs[1]) delete = nodes.new("GeometryNodeDeleteGeometry") delete.domain = "FACE" links.new(geometry, delete.inputs["Geometry"]) links.new(tiny.outputs[0], delete.inputs["Selection"]) return delete.outputs["Geometry"] def _profile_field(group, geometry): nodes = group.nodes links = group.links profile = nodes.new("GeometryNodeInputNamedAttribute") profile.data_type = "FLOAT" profile.inputs["Name"].default_value = BALLOON_ATTRIBUTE_NAME profile_max = nodes.new("GeometryNodeAttributeStatistic") profile_max.data_type = "FLOAT" profile_max.domain = "POINT" links.new(geometry, profile_max.inputs["Geometry"]) links.new(profile.outputs["Attribute"], profile_max.inputs["Attribute"]) safe_max = math_node(nodes, "MAXIMUM") safe_max.inputs[1].default_value = 1e-6 links.new(profile_max.outputs["Max"], safe_max.inputs[0]) normalized = math_node(nodes, "DIVIDE") 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 links.new(normalized.outputs[0], smooth.inputs["Value"]) return profile.outputs["Attribute"], smooth.outputs["Result"] def _extrude_layers(group, geometry): node = group.nodes.new("GeometryNodeExtrudeMesh") node.mode = "FACES" node.inputs["Individual"].default_value = False node.inputs["Offset"].default_value = (0.0, 0.0, 1.0) # Keep distinct front and back vertices before overriding their Z positions. node.inputs["Offset Scale"].default_value = 1.0 group.links.new(geometry, node.inputs["Mesh"]) return node def _offset_geometry_z(group, geometry, z_value): nodes = group.nodes links = group.links offset = nodes.new("ShaderNodeCombineXYZ") links.new(z_value, offset.inputs["Z"]) set_position = nodes.new("GeometryNodeSetPosition") links.new(geometry, set_position.inputs["Geometry"]) links.new(offset.outputs["Vector"], set_position.inputs["Offset"]) return set_position.outputs["Geometry"] def _set_layer_z(group, geometry, extrude, front_z, back_z): nodes = group.nodes links = group.links choose_z = nodes.new("GeometryNodeSwitch") choose_z.input_type = "FLOAT" links.new(extrude.outputs["Top"], choose_z.inputs["Switch"]) links.new(front_z, choose_z.inputs["False"]) links.new(back_z, choose_z.inputs["True"]) position = nodes.new("GeometryNodeInputPosition") separate = nodes.new("ShaderNodeSeparateXYZ") links.new(position.outputs["Position"], separate.inputs["Vector"]) combine = nodes.new("ShaderNodeCombineXYZ") links.new(separate.outputs["X"], combine.inputs["X"]) links.new(separate.outputs["Y"], combine.inputs["Y"]) links.new(choose_z.outputs["Output"], combine.inputs["Z"]) set_position = nodes.new("GeometryNodeSetPosition") 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" links.new(set_position.outputs["Geometry"], side.inputs["Geometry"]) links.new(extrude.outputs["Side"], side.inputs["Selection"]) flip_side = nodes.new("GeometryNodeFlipFaces") links.new(side.outputs["Selection"], flip_side.inputs["Mesh"]) front = _offset_geometry_z( group, geometry, front_z ) back = _offset_geometry_z( group, geometry, back_z ) flip_back = nodes.new("GeometryNodeFlipFaces") links.new(back, flip_back.inputs["Mesh"]) join = nodes.new("GeometryNodeJoinGeometry") 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 links.new(join.outputs["Geometry"], merge.inputs["Geometry"]) return merge.outputs["Geometry"] def _remove_zero_thickness( group, zero_geometry, shaped_geometry, thickness, ): nodes = group.nodes links = group.links zero = math_node(nodes, "LESS_THAN") zero.inputs[1].default_value = 1e-6 links.new(thickness, zero.inputs[0]) choose = nodes.new("GeometryNodeSwitch") choose.input_type = "GEOMETRY" 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): nodes = group.nodes links = group.links uv = nodes.new("GeometryNodeInputNamedAttribute") uv.data_type = "FLOAT_VECTOR" uv.inputs["Name"].default_value = "UVMap" position_texture = nodes.new("GeometryNodeImageTexture") position_texture.interpolation = "Linear" position_texture.extension = "EXTEND" links.new(depth_image, position_texture.inputs["Image"]) links.new(uv.outputs["Attribute"], position_texture.inputs["Vector"]) return position_texture.outputs["Color"] def _shade_output(group, geometry): shade = group.nodes.new("GeometryNodeSetShadeSmooth") shade.domain = "FACE" shade.inputs["Shade Smooth"].default_value = True output = group.nodes.new("NodeGroupOutput") group.links.new(geometry, shade.inputs["Geometry"]) group.links.new(shade.outputs["Geometry"], output.inputs["Geometry"])