"""Estimate existing front volume and add continuous normal-directed inflation.""" from cutout_volume import node def _math(group, operation, a, b, location): result = node(group, "ShaderNodeMath", location) result.operation = operation for index, value in enumerate((a, b)): if hasattr(value, "node"): group.links.new(value, result.inputs[index]) else: result.inputs[index].default_value = value return result.outputs[0] def _read(group, name, location, kind="FLOAT"): result = node(group, "GeometryNodeInputNamedAttribute", location) result.data_type = kind result.inputs["Name"].default_value = "o_balloon_surface_" + name return result.outputs["Attribute"] def _store(group, geometry, name, value, location, kind="FLOAT"): result = node(group, "GeometryNodeStoreNamedAttribute", location) result.data_type, result.domain = kind, "POINT" result.inputs["Name"].default_value = "o_balloon_surface_" + name group.links.new(geometry, result.inputs["Geometry"]) group.links.new(value, result.inputs["Value"]) return result.outputs["Geometry"] def build_front_fields(group, geometry): """Fit structural amplitude over neighbouring points before creating the shell.""" links = group.links def math(operation, a, b, x, y): return _math(group, operation, a, b, (4800 + x, -2500 + y)) def blur(value, x, y, weight=None): result = node(group, "GeometryNodeBlurAttribute", (4800 + x, -2500 + y)) result.data_type = "FLOAT" result.inputs["Iterations"].default_value = 256 links.new(value, result.inputs["Value"]) if weight is not None: links.new(weight, result.inputs["Weight"]) return result.outputs[0] profile_node = node(group, "GeometryNodeInputNamedAttribute", (4800, -2500)) profile_node.data_type = "FLOAT" profile_node.inputs["Name"].default_value = "o_balloon" profile = profile_node.outputs["Attribute"] rim = _read(group, "rim", (4800, -3050)) interior = math("SUBTRACT", 1, rim, 240, -550) position = node(group, "GeometryNodeInputPosition", (4800, -1900)) axes = node(group, "ShaderNodeSeparateXYZ", (5040, -1900)) links.new(position.outputs[0], axes.inputs[0]) depth = axes.outputs["Z"] depth_slow = blur(depth, 480, 600, interior) profile_slow = blur(profile, 480, 0, interior) depth_bump = math("SUBTRACT", depth, depth_slow, 720, 600) profile_bump = math("SUBTRACT", profile, profile_slow, 720, 0) numerator = math("MULTIPLY", depth_bump, profile_bump, 960, 600) denominator = math("MULTIPLY", profile_bump, profile_bump, 960, 0) numerator = blur(numerator, 1200, 600) denominator = blur(denominator, 1200, 0) denominator = math("MAXIMUM", denominator, 1e-10, 1440, 0) structure = math("DIVIDE", numerator, denominator, 1680, 600) structure = math("MAXIMUM", structure, 0, 1920, 600) carrier = _store(group, geometry, "structure", structure, (7300, 0)) normal = _read(group, "front_normal", (7100, -600), "FLOAT_VECTOR") controls = node(group, "NodeGroupInput", (6860, -1100)) # Keep the accepted broad front-direction filter at the default of 50; # zero disables both this filter and the initial front/back normal filters. spread = _math(group, "MULTIPLY", controls.outputs["Normal Smooth"], 256 / 50, (7100, -1100)) smooth = node(group, "GeometryNodeBlurAttribute", (7340, -600)) smooth.data_type = "FLOAT_VECTOR" links.new(normal, smooth.inputs["Value"]) links.new(spread, smooth.inputs["Iterations"]) normalize = node(group, "ShaderNodeVectorMath", (7580, -600)) normalize.operation = "NORMALIZE" links.new(smooth.outputs[0], normalize.inputs[0]) carrier = _store(group, carrier, "inflation_normal", normalize.outputs[0], (7770, 0), "FLOAT_VECTOR") return carrier def build_inflation_position(group, position, distance, uniform): """Grow both modes continuously while leaving the rear cap in place.""" previous = set(group.nodes) links = group.links def math(operation, a, b, x, y): return _math(group, operation, a, b, (x, y)) structure = _read(group, "structure", (14000, -4300)) control = node(group, "NodeGroupInput", (13760, -5000)) a = math("MULTIPLY", control.outputs["Thickness"], control.outputs["Front Inflation"], 14240, -5000) # A shifted cubic blend preserves continuous growth and height order. shifted = math("ADD", a, 0.75, 14480, -5000) quadratic = math("MULTIPLY", a, shifted, 14720, -5000) quadratic = math("ADD", quadratic, 0.1875, 14960, -5000) q = math("MULTIPLY", a, quadratic, 15200, -5000) k = math("ADD", structure, 0.25, 14240, -4300) square = math("MULTIPLY", k, k, 14480, -4300) cube = math("MULTIPLY", square, k, 14720, -4300) total = math("ADD", cube, q, 14960, -4300) root = math("POWER", total, 1 / 3, 15200, -4300) root_square = math("MULTIPLY", root, root, 15440, -4000) product = math("MULTIPLY", root, k, 15440, -4300) denominator = math("ADD", root_square, product, 15680, -4300) denominator = math("ADD", denominator, square, 15920, -4300) response = math("DIVIDE", q, denominator, 15920, -4800) profile = node(group, "GeometryNodeInputNamedAttribute", (15920, -3700)) profile.data_type = "FLOAT" profile.inputs["Name"].default_value = "o_balloon" balloon = math("MULTIPLY", profile.outputs["Attribute"], response, 16160, -4250) uniform_control = node(group, "NodeGroupInput", (15920, -5100)) uniform_amount = math("MULTIPLY", uniform_control.outputs["Uniform Thickness"], uniform_control.outputs["Front Inflation"], 16160, -5100) selected = node(group, "GeometryNodeSwitch", (16400, -4550)) selected.input_type = "FLOAT" links.new(uniform, selected.inputs["Switch"]) links.new(balloon, selected.inputs["False"]) links.new(uniform_amount, selected.inputs["True"]) displacement = selected.outputs[0] height = _read(group, "thickness", (14000, -5500)) layer = math("DIVIDE", distance, height, 14240, -5500) layer = math("ADD", 1, layer, 14480, -5500) layer = math("MAXIMUM", layer, 0, 14720, -5500) layer = math("MINIMUM", layer, 1, 14960, -5500) displacement = math("MULTIPLY", displacement, layer, 16640, -4550) normal = _read(group, "inflation_normal", (16640, -4000), "FLOAT_VECTOR") offset = node(group, "ShaderNodeVectorMath", (16880, -4550)) offset.operation = "SCALE" links.new(normal, offset.inputs[0]) links.new(displacement, offset.inputs[3]) result = node(group, "ShaderNodeVectorMath", (17120, -4550)) result.operation = "ADD" links.new(position, result.inputs[0]) links.new(offset.outputs[0], result.inputs[1]) for item in set(group.nodes) - previous: item.location.y = -3550 + (item.location.y + 3700) * 0.7 return result.outputs[0]