"""
Math and Vector Math Node bindings for Blender.
Also hosts general-purpose value/vector utilities (mix, map_range,
combine/separate xyz, curves, constant) that work
across shader and geometry trees - they live here rather than in shader.py
or func.py to avoid implying they are specific to those contexts.
"""
from typing import Literal, NamedTuple, TypeVar
import numpy as np
from procfunc import types as pt
from procfunc.nodes import types as nt
from procfunc.nodes.util.bindings_util import ContextualNode, RuntimeResolveDataType
from procfunc.nodes.util.bpy_node_info import NodeDataType
[docs]
def clamp(
value: nt.SocketOrVal[float],
min: nt.SocketOrVal[float] = 0.0,
max: nt.SocketOrVal[float] = 1.0,
clamp_type: Literal["MINMAX", "RANGE"] = "MINMAX",
) -> nt.ProcNode[float]:
"""
Uses a Clamp Shader Node.
See: https://docs.blender.org/manual/en/4.2/render/shader_nodes/converter/clamp.html
"""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeClamp",
inputs={"Value": value, "Min": min, "Max": max},
attrs={"clamp_type": clamp_type},
)
# Math Nodes
def _math(
*operands: nt.SocketOrVal[float],
operation: str = "ADD",
) -> nt.ProcNode[float]:
"""
Uses a Math Shader Node.
Procfunc does NOT support the inline clamp option - use pf.nodes.math.clamp() on the output instead.
See: https://docs.blender.org/manual/en/4.2/render/shader_nodes/converter/math.html
"""
# only mention the sockets this operation uses; an explicit None operand
# still propagates and is rejected by the strict-None policy
inputs = {("Value", i): v for i, v in enumerate(operands)}
return nt.ProcNode.from_nodetype(
node_type=ContextualNode.MATH.value,
inputs=inputs,
attrs={
"operation": operation,
"use_clamp": False, # not supported by procfunc
},
)
# Basic Math Operations
[docs]
def add(a: nt.SocketOrVal[float], b: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='ADD'."""
return _math(a, b, operation="ADD")
[docs]
def subtract(a: nt.SocketOrVal[float], b: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='SUBTRACT'."""
return _math(a, b, operation="SUBTRACT")
[docs]
def multiply(a: nt.SocketOrVal[float], b: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='MULTIPLY'."""
return _math(a, b, operation="MULTIPLY")
[docs]
def multiply_add(
a: nt.SocketOrVal[float],
b: nt.SocketOrVal[float],
addend: nt.SocketOrVal[float],
) -> nt.ProcNode[float]:
"""Uses a Math node with operation='MULTIPLY_ADD'."""
return _math(a, b, addend, operation="MULTIPLY_ADD")
[docs]
def divide(
numerator: nt.SocketOrVal[float], denominator: nt.SocketOrVal[float]
) -> nt.ProcNode[float]:
"""Uses a Math node with operation='DIVIDE'."""
return _math(numerator, denominator, operation="DIVIDE")
[docs]
def power(
base: nt.SocketOrVal[float], exponent: nt.SocketOrVal[float]
) -> nt.ProcNode[float]:
"""Uses a Math node with operation='POWER'."""
return _math(base, exponent, operation="POWER")
[docs]
def logarithm(
value: nt.SocketOrVal[float], base: nt.SocketOrVal[float]
) -> nt.ProcNode[float]:
"""Uses a Math node with operation='LOGARITHM'."""
return _math(value, base, operation="LOGARITHM")
[docs]
def sqrt(value: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='SQRT'."""
return _math(value, operation="SQRT")
[docs]
def inverse_sqrt(value: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='INVERSE_SQRT'."""
return _math(value, operation="INVERSE_SQRT")
[docs]
def absolute(value: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='ABSOLUTE'."""
return _math(value, operation="ABSOLUTE")
[docs]
def exponent(value: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='EXPONENT'."""
return _math(value, operation="EXPONENT")
# Comparison Operations
[docs]
def minimum(a: nt.SocketOrVal[float], b: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='MINIMUM'."""
return _math(a, b, operation="MINIMUM")
[docs]
def maximum(a: nt.SocketOrVal[float], b: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='MAXIMUM'."""
return _math(a, b, operation="MAXIMUM")
[docs]
def less_than(a: nt.SocketOrVal[float], b: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='LESS_THAN'."""
return _math(a, b, operation="LESS_THAN")
[docs]
def greater_than(
a: nt.SocketOrVal[float], b: nt.SocketOrVal[float]
) -> nt.ProcNode[float]:
"""Uses a Math node with operation='GREATER_THAN'."""
return _math(a, b, operation="GREATER_THAN")
[docs]
def sign(value: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='SIGN'."""
return _math(value, operation="SIGN")
[docs]
def compare(
a: nt.SocketOrVal[float],
b: nt.SocketOrVal[float],
epsilon: nt.SocketOrVal[float] = 0.001,
) -> nt.ProcNode[float]:
"""Uses a Math node with operation='COMPARE'."""
return _math(a, b, epsilon, operation="COMPARE")
[docs]
def smooth_minimum(
a: nt.SocketOrVal[float],
b: nt.SocketOrVal[float],
distance: nt.SocketOrVal[float] = 0.0,
) -> nt.ProcNode[float]:
"""Uses a Math node with operation='SMOOTH_MIN'."""
return _math(a, b, distance, operation="SMOOTH_MIN")
[docs]
def smooth_maximum(
a: nt.SocketOrVal[float],
b: nt.SocketOrVal[float],
distance: nt.SocketOrVal[float] = 0.0,
) -> nt.ProcNode[float]:
"""Uses a Math node with operation='SMOOTH_MAX'."""
return _math(a, b, distance, operation="SMOOTH_MAX")
[docs]
def round(value: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='ROUND'."""
return _math(value, operation="ROUND")
[docs]
def floor(value: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='FLOOR'."""
return _math(value, operation="FLOOR")
[docs]
def ceil(value: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='CEIL'."""
return _math(value, operation="CEIL")
[docs]
def truncate(value: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='TRUNC'."""
return _math(value, operation="TRUNC")
[docs]
def fraction(value: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='FRACT'."""
return _math(value, operation="FRACT")
[docs]
def modulo(a: nt.SocketOrVal[float], b: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='MODULO'."""
return _math(a, b, operation="MODULO")
[docs]
def floor_mod(a: nt.SocketOrVal[float], b: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='FLOORED_MODULO'."""
return _math(a, b, operation="FLOORED_MODULO")
[docs]
def wrap(
value: nt.SocketOrVal[float],
max_val: nt.SocketOrVal[float] = 1.0,
min_val: nt.SocketOrVal[float] = 0.0,
) -> nt.ProcNode[float]:
"""Uses a Math node with operation='WRAP'."""
return _math(value, max_val, min_val, operation="WRAP")
[docs]
def snap(
value: nt.SocketOrVal[float], increment: nt.SocketOrVal[float] = 1.0
) -> nt.ProcNode[float]:
"""Uses a Math node with operation='SNAP'."""
return _math(value, increment, operation="SNAP")
[docs]
def pingpong(
value: nt.SocketOrVal[float], scale: nt.SocketOrVal[float] = 1.0
) -> nt.ProcNode[float]:
"""Uses a Math node with operation='PINGPONG'."""
return _math(value, scale, operation="PINGPONG")
# Trigonometric Operations
[docs]
def sin(value: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='SINE'."""
return _math(value, operation="SINE")
[docs]
def cos(value: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='COSINE'."""
return _math(value, operation="COSINE")
[docs]
def tan(value: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='TANGENT'."""
return _math(value, operation="TANGENT")
[docs]
def asin(value: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='ARCSINE'."""
return _math(value, operation="ARCSINE")
[docs]
def acos(value: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='ARCCOSINE'."""
return _math(value, operation="ARCCOSINE")
[docs]
def atan(value: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='ARCTANGENT'."""
return _math(value, operation="ARCTANGENT")
[docs]
def atan2(y: nt.SocketOrVal[float], x: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='ARCTAN2'."""
return _math(y, x, operation="ARCTAN2")
[docs]
def sinh(value: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='SINH'."""
return _math(value, operation="SINH")
[docs]
def cosh(value: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='COSH'."""
return _math(value, operation="COSH")
[docs]
def tanh(value: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='TANH'."""
return _math(value, operation="TANH")
# Conversion Operations
[docs]
def deg_to_rad(value: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='RADIANS'."""
return _math(value, operation="RADIANS")
[docs]
def rad_to_deg(value: nt.SocketOrVal[float]) -> nt.ProcNode[float]:
"""Uses a Math node with operation='DEGREES'."""
return _math(value, operation="DEGREES")
# Vector Math Operations
[docs]
def vector_add(
a: nt.SocketOrVal[pt.Vector],
b: nt.SocketOrVal[pt.Vector],
) -> nt.ProcNode[pt.Vector]:
"""Uses a VectorMath Shader Node with operation='ADD'."""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorMath",
inputs={("Vector", 0): a, ("Vector", 1): b},
attrs={"operation": "ADD"},
)
[docs]
def vector_subtract(
a: nt.SocketOrVal[pt.Vector],
b: nt.SocketOrVal[pt.Vector],
) -> nt.ProcNode[pt.Vector]:
"""Uses a VectorMath Shader Node with operation='SUBTRACT'."""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorMath",
inputs={("Vector", 0): a, ("Vector", 1): b},
attrs={"operation": "SUBTRACT"},
)
[docs]
def vector_multiply(
a: nt.SocketOrVal[pt.Vector],
b: nt.SocketOrVal[pt.Vector],
) -> nt.ProcNode[pt.Vector]:
"""Uses a VectorMath Shader Node with operation='MULTIPLY'."""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorMath",
inputs={("Vector", 0): a, ("Vector", 1): b},
attrs={"operation": "MULTIPLY"},
)
[docs]
def vector_multiply_add(
a: nt.SocketOrVal[pt.Vector],
b: nt.SocketOrVal[pt.Vector],
addend: nt.SocketOrVal[pt.Vector],
) -> nt.ProcNode[pt.Vector]:
"""Uses a VectorMath Shader Node with operation='MULTIPLY_ADD'."""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorMath",
attrs={"operation": "MULTIPLY_ADD"},
inputs={("Vector", 0): a, ("Vector", 1): b, ("Vector", 2): addend},
)
[docs]
def vector_divide(
a: nt.SocketOrVal[pt.Vector],
b: nt.SocketOrVal[pt.Vector],
) -> nt.ProcNode[pt.Vector]:
"""Uses a VectorMath Shader Node with operation='DIVIDE'."""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorMath",
inputs={("Vector", 0): a, ("Vector", 1): b},
attrs={"operation": "DIVIDE"},
)
[docs]
def vector_cross_product(
a: nt.SocketOrVal[pt.Vector],
b: nt.SocketOrVal[pt.Vector],
) -> nt.ProcNode[pt.Vector]:
"""Uses a VectorMath Shader Node with operation='CROSS_PRODUCT'."""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorMath",
inputs={("Vector", 0): a, ("Vector", 1): b},
attrs={"operation": "CROSS_PRODUCT"},
)
[docs]
def vector_project(
vector: nt.SocketOrVal[pt.Vector],
onto: nt.SocketOrVal[pt.Vector],
) -> nt.ProcNode[float]:
"""Uses a VectorMath Shader Node with operation='PROJECT'."""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorMath",
inputs={("Vector", 0): vector, ("Vector", 1): onto},
attrs={"operation": "PROJECT"},
)
[docs]
def vector_reflect(
a: nt.SocketOrVal[pt.Vector],
normal: nt.SocketOrVal[pt.Vector],
) -> nt.ProcNode[pt.Vector]:
"""Uses a VectorMath Shader Node with operation='REFLECT'."""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorMath",
inputs={("Vector", 0): a, ("Vector", 1): normal},
attrs={"operation": "REFLECT"},
)
[docs]
def vector_refract(
incident: nt.SocketOrVal[pt.Vector],
normal: nt.SocketOrVal[pt.Vector],
ior: nt.SocketOrVal[float] = 1.0,
) -> nt.ProcNode[pt.Vector]:
"""Uses a VectorMath Shader Node with operation='REFRACT'."""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorMath",
inputs={("Vector", 0): incident, ("Vector", 1): normal, "Scale": ior},
attrs={"operation": "REFRACT"},
)
[docs]
def vector_faceforward(
vector: nt.SocketOrVal[pt.Vector],
surface: nt.SocketOrVal[pt.Vector],
normal: nt.SocketOrVal[pt.Vector],
) -> nt.ProcNode[pt.Vector]:
"""Uses a VectorMath Shader Node with operation='FACEFORWARD'."""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorMath",
inputs={
("Vector", 0): vector,
("Vector", 1): surface,
("Vector", 2): normal,
},
attrs={"operation": "FACEFORWARD"},
)
[docs]
def vector_dot_product(
a: nt.SocketOrVal[pt.Vector],
b: nt.SocketOrVal[pt.Vector],
) -> nt.ProcNode[pt.Vector]:
"""Uses a VectorMath Shader Node with operation='DOT_PRODUCT'."""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorMath",
inputs={("Vector", 0): a, ("Vector", 1): b},
attrs={"operation": "DOT_PRODUCT"},
)
[docs]
def vector_distance(
a: nt.SocketOrVal[pt.Vector],
b: nt.SocketOrVal[pt.Vector],
) -> nt.ProcNode[pt.Vector]:
"""Uses a VectorMath Shader Node with operation='DISTANCE'."""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorMath",
inputs={("Vector", 0): a, ("Vector", 1): b},
attrs={"operation": "DISTANCE"},
)
[docs]
def vector_length(vector: nt.SocketOrVal[pt.Vector]) -> nt.ProcNode[float]:
"""Uses a VectorMath Shader Node with operation='LENGTH'."""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorMath",
inputs={("Vector", 0): vector},
attrs={"operation": "LENGTH"},
)
[docs]
def vector_scale(
vector: nt.SocketOrVal[pt.Vector], scale: nt.SocketOrVal[float]
) -> nt.ProcNode[pt.Vector]:
"""Uses a VectorMath Shader Node with operation='SCALE'."""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorMath",
inputs={("Vector", 0): vector, ("Scale", 0): scale},
attrs={"operation": "SCALE"},
)
[docs]
def vector_normalize(
vector: nt.SocketOrVal[pt.Vector],
) -> nt.ProcNode[pt.Vector]:
"""Uses a VectorMath Shader Node with operation='NORMALIZE'."""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorMath",
inputs={("Vector", 0): vector},
attrs={"operation": "NORMALIZE"},
)
[docs]
def vector_wrap(
vector: nt.SocketOrVal[pt.Vector],
max_val: nt.SocketOrVal[pt.Vector],
min_val: nt.SocketOrVal[pt.Vector],
) -> nt.ProcNode[pt.Vector]:
"""Uses a VectorMath Shader Node with operation='WRAP'."""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorMath",
inputs={("Vector", 0): vector, ("Vector", 1): max_val, ("Vector", 2): min_val},
attrs={"operation": "WRAP"},
)
[docs]
def vector_snap(
a: nt.SocketOrVal[pt.Vector],
b: nt.SocketOrVal[pt.Vector],
) -> nt.ProcNode[pt.Vector]:
"""Uses a VectorMath Shader Node with operation='SNAP'."""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorMath",
inputs={("Vector", 0): a, ("Vector", 1): b},
attrs={"operation": "SNAP"},
)
[docs]
def vector_floor(
vector: nt.SocketOrVal[pt.Vector],
) -> nt.ProcNode[pt.Vector]:
"""Uses a VectorMath Shader Node with operation='FLOOR'."""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorMath",
inputs={("Vector", 0): vector},
attrs={"operation": "FLOOR"},
)
[docs]
def vector_ceil(
vector: nt.SocketOrVal[pt.Vector],
) -> nt.ProcNode[pt.Vector]:
"""Uses a VectorMath Shader Node with operation='CEIL'."""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorMath",
inputs={("Vector", 0): vector},
attrs={"operation": "CEIL"},
)
[docs]
def vector_modulo(
a: nt.SocketOrVal[pt.Vector],
b: nt.SocketOrVal[pt.Vector],
) -> nt.ProcNode[pt.Vector]:
"""Uses a VectorMath Shader Node with operation='MODULO'."""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorMath",
inputs={("Vector", 0): a, ("Vector", 1): b},
attrs={"operation": "MODULO"},
)
[docs]
def vector_fraction(
vector: nt.SocketOrVal[pt.Vector],
) -> nt.ProcNode[pt.Vector]:
"""Uses a VectorMath Shader Node with operation='FRACTION'."""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorMath",
inputs={("Vector", 0): vector},
attrs={"operation": "FRACTION"},
)
[docs]
def vector_absolute(
vector: nt.SocketOrVal[pt.Vector],
) -> nt.ProcNode[pt.Vector]:
"""Uses a VectorMath Shader Node with operation='ABSOLUTE'."""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorMath",
inputs={("Vector", 0): vector},
attrs={"operation": "ABSOLUTE"},
)
[docs]
def vector_minimum(
a: nt.SocketOrVal[pt.Vector],
b: nt.SocketOrVal[pt.Vector],
) -> nt.ProcNode[pt.Vector]:
"""Uses a VectorMath Shader Node with operation='MINIMUM'."""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorMath",
inputs={("Vector", 0): a, ("Vector", 1): b},
attrs={"operation": "MINIMUM"},
)
[docs]
def vector_maximum(
a: nt.SocketOrVal[pt.Vector],
b: nt.SocketOrVal[pt.Vector],
) -> nt.ProcNode[pt.Vector]:
"""Uses a VectorMath Shader Node with operation='MAXIMUM'."""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorMath",
inputs={("Vector", 0): a, ("Vector", 1): b},
attrs={"operation": "MAXIMUM"},
)
[docs]
def vector_sine(
vector: nt.SocketOrVal[pt.Vector],
) -> nt.ProcNode[pt.Vector]:
"""Uses a VectorMath Shader Node with operation='SINE'."""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorMath",
inputs={("Vector", 0): vector},
attrs={"operation": "SINE"},
)
[docs]
def vector_cosine(
vector: nt.SocketOrVal[pt.Vector],
) -> nt.ProcNode[pt.Vector]:
"""Uses a VectorMath Shader Node with operation='COSINE'."""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorMath",
inputs={("Vector", 0): vector},
attrs={"operation": "COSINE"},
)
[docs]
def vector_tangent(
vector: nt.SocketOrVal[pt.Vector],
) -> nt.ProcNode[pt.Vector]:
"""Uses a VectorMath Shader Node with operation='TANGENT'."""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorMath",
inputs={("Vector", 0): vector},
attrs={"operation": "TANGENT"},
)
[docs]
def vector_rotate_axis_angle(
vector: nt.SocketOrVal[pt.Vector],
center: nt.SocketOrVal[pt.Vector] = (0, 0, 0),
axis: nt.SocketOrVal[pt.Vector] = (0, 0, 1),
angle: nt.SocketOrVal[float] = 0.0,
invert: bool = False,
) -> nt.ProcNode[pt.Vector]:
"""
Uses a VectorRotate Shader Node with rotation_type='AXIS_ANGLE'.
See: https://docs.blender.org/manual/en/4.2/render/shader_nodes/vector/vector_rotate.html
"""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorRotate",
inputs={"Vector": vector, "Center": center, "Axis": axis, "Angle": angle},
attrs={"invert": invert, "rotation_type": "AXIS_ANGLE"},
)
[docs]
def vector_rotate_euler(
vector: nt.SocketOrVal[pt.Vector],
center: nt.SocketOrVal[pt.Vector] = (0, 0, 0),
rotation: nt.SocketOrVal[pt.Vector] = (0, 0, 0),
invert: bool = False,
) -> nt.ProcNode[pt.Vector]:
"""Uses a VectorRotate Shader Node with rotation_type in {'EULER_XYZ', 'X_AXIS', 'Y_AXIS', 'Z_AXIS'}."""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeVectorRotate",
inputs={"Vector": vector, "Center": center, "Rotation": rotation},
attrs={"invert": invert, "rotation_type": "EULER_XYZ"},
)
# NOTE: mode XYZ have been dropped. transpiler specialcases will map these back to vector_rotate_euler calls.
# ---- Constants / inputs ----------------------------------------------------
TConstant = TypeVar("TConstant", int, float, bool, str, pt.Vector, pt.Euler, pt.Color)
_CONSTANT_CONTEXTUAL_BY_TYPE = [
# bool before int: bool is a subclass of int
(bool, ContextualNode.BOOLEAN),
(int, ContextualNode.INT),
(float, ContextualNode.VALUE),
(pt.Euler, ContextualNode.ROTATION),
(pt.Vector, ContextualNode.VECTOR),
(tuple, ContextualNode.VECTOR),
(pt.Color, ContextualNode.RGB),
(str, ContextualNode.STRING),
]
[docs]
def constant(
value: TConstant,
) -> nt.ProcNode[TConstant]:
"""
Replaces all nodes which just store a constant
e.g. ShaderNodeValue, ShaderNodeRGB, FunctionNodeInput*, etc
Dispatches on python type to a contextual node, resolved per tree type at
execution time (e.g. vector -> FunctionNodeInputVector in geometry trees,
a CombineXYZ with component socket defaults elsewhere).
"""
for py_type, contextual in _CONSTANT_CONTEXTUAL_BY_TYPE:
if isinstance(value, py_type):
return nt.ProcNode.from_nodetype(
node_type=contextual.value, inputs={}, attrs={"value": value}
)
raise ValueError(f"Unsupported constant type: {type(value)}")
# ---- Mix -------------------------------------------------------------------
TMix = TypeVar(
"TMix",
nt.SocketOrVal[float],
nt.SocketOrVal[pt.Vector],
nt.SocketOrVal[pt.Color],
)
[docs]
def mix(
a: TMix,
b: TMix,
factor: nt.SocketOrVal[float],
clamp_factor: bool = True,
factor_mode: Literal["UNIFORM", "NON_UNIFORM"] = "UNIFORM",
data_type: NodeDataType | RuntimeResolveDataType | None = None,
) -> nt.ProcNode[TMix]:
"""
Uses MixNode to mix float, vector, or color fields with a plain MIX blend.
For colors, prefer mix_rgb when you need a non-MIX blend mode or clamp_result;
this function hardcodes blend_type="MIX" and clamp_result=False.
"""
if data_type is None:
data_type = RuntimeResolveDataType(
[NodeDataType.RGBA, NodeDataType.FLOAT, NodeDataType.FLOAT_VECTOR],
["A", "B"],
)
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeMix",
inputs={"A": a, "B": b, "Factor": factor},
attrs={
"blend_type": "MIX",
"clamp_factor": clamp_factor,
"clamp_result": False,
"factor_mode": factor_mode,
"data_type": data_type,
},
)
# ---- Curves ----------------------------------------------------------------
[docs]
def float_curve(
factor: nt.SocketOrVal[float],
value: nt.SocketOrVal[float],
curve: np.ndarray | None = None,
handle_type: str = "AUTO",
use_clip: bool = True,
) -> nt.ProcNode[float]:
"""
Uses a FloatCurve Shader Node.
See: https://docs.blender.org/manual/en/4.2/render/shader_nodes/converter/float_curve.html
"""
return nt.ProcNode.from_nodetype(
node_type="ShaderNodeFloatCurve",
inputs={"Factor": factor, "Value": value},
attrs={"mapping": curve, "handle_type": handle_type, "use_clip": use_clip},
)
[docs]
def vector_curve(
vector: nt.SocketOrVal[pt.Vector],
fac: nt.SocketOrVal[float] = 1.0,
curves: list[np.ndarray] | np.ndarray | None = None,
) -> nt.ProcNode[pt.Vector]:
"""
Uses a VectorCurve Shader Node.
`fac` blends between the input and curve-mapped vector; the compositor
variant (CompositorNodeCurveVec) has no Fac socket and always applies the
curve fully, so `fac` is accepted there only at its no-op value 1.0.
See: https://docs.blender.org/manual/en/4.2/render/shader_nodes/vector/curves.html
"""
return nt.ProcNode.from_nodetype(
node_type=ContextualNode.VECTOR_CURVE.value,
inputs={"Fac": fac, "Vector": vector},
attrs={"curves": curves},
)
# ---- Combine / Separate ------------------------------------
[docs]
def combine_xyz(
x: nt.SocketOrVal[float] = 0.0,
y: nt.SocketOrVal[float] = 0.0,
z: nt.SocketOrVal[float] = 0.0,
) -> nt.ProcNode[pt.Vector]:
"""
Uses a CombineXYZ Shader Node.
See: https://docs.blender.org/manual/en/4.2/render/shader_nodes/converter/combine_xyz.html
"""
return nt.ProcNode.from_nodetype(
node_type=ContextualNode.COMBINE_XYZ.value,
inputs={"X": x, "Y": y, "Z": z},
attrs={},
)
[docs]
class SeparateXyzResult(NamedTuple):
x: nt.ProcNode[float]
y: nt.ProcNode[float]
z: nt.ProcNode[float]
[docs]
def separate_xyz(vector: nt.SocketOrVal[pt.Vector]) -> SeparateXyzResult:
"""
Uses a SeparateXYZ Shader Node.
See: https://docs.blender.org/manual/en/4.2/render/shader_nodes/converter/separate_xyz.html
"""
node = nt.ProcNode.from_nodetype(
node_type="ShaderNodeSeparateXYZ",
inputs={"Vector": vector},
attrs={},
)
return SeparateXyzResult(
node._output_socket("x"), node._output_socket("y"), node._output_socket("z")
)
# ---- MapRange --------------------------------------------------------------
TInterpolationType = Literal["LINEAR", "STEPPED_LINEAR", "SMOOTHSTEP", "SMOOTHERSTEP"]
[docs]
def map_range(
value: nt.SocketOrVal[float],
from_max: nt.SocketOrVal[float] = 1.0,
from_min: nt.SocketOrVal[float] = 0.0,
to_max: nt.SocketOrVal[float] = 1.0,
to_min: nt.SocketOrVal[float] = 0.0,
clamp: bool = True,
interpolation_type: TInterpolationType = "LINEAR",
data_type: NodeDataType | RuntimeResolveDataType | None = None,
) -> nt.ProcNode:
"""
Uses a MapRange Shader Node.
See: https://docs.blender.org/manual/en/4.2/render/shader_nodes/converter/map_range.html
"""
if data_type is None:
data_type = RuntimeResolveDataType(
[NodeDataType.FLOAT, NodeDataType.FLOAT_VECTOR],
["From Max", "From Min", "To Max", "To Min", "Value"],
)
# interpolation_type / data_type only exist on ShaderNodeMapRange. The
# wrapper omits interpolation_type at default so a compositor call with
# all defaults doesn't trip _set_node_attribute. RuntimeResolveDataType
# is dropped at construct time when the target lacks the attr; an
# explicit NodeDataType in compositor context will reach setattr and
# raise naturally.
attrs: dict[str, object] = {"clamp": clamp, "data_type": data_type}
if interpolation_type != "LINEAR":
attrs["interpolation_type"] = interpolation_type
return nt.ProcNode.from_nodetype(
node_type=ContextualNode.MAP_RANGE.value,
inputs={
"From Max": from_max,
"From Min": from_min,
"To Max": to_max,
"To Min": to_min,
"Value": value,
},
attrs=attrs,
)