OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes / src / nodes / attribute_nodes.cpp

816 lines · cpp ref:
#include "attribute_nodes.h"
#include "../graph/domain_adapt.h"
#include "../graph/graph_executor.h"
#include <godot_cpp/variant/utility_functions.hpp>
#include <godot_cpp/templates/hash_set.hpp>

namespace godot {

// --- PositionNode ---

void PositionNode::_bind_methods() {
}

String PositionNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const {
	// `pos` is the density function's argument and the batch shader's per-vertex
	// position -- the same name in both, which is why this one line serves both
	// paths. See the note in the header for what its absence cost.
	return p_ctx.declare(p_node_idx, 0) + " = pos;";
}

PositionNode::PositionNode() {
	// Blender's Position has NO inputs -- it is a field, evaluated on whatever
	// domain the consumer is working over. Ours took a Geometry socket only
	// because it had no other way to know what to be a field of; that is what
	// ProcEvalContext::context_geometry is for now.
	output_defs.push_back({ "Position", SocketType::Vector, Variant() });
}

void PositionNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	Ref<ProcGeometry> geo = p_ctx.context_geometry;
	if (geo.is_null()) {
		// Running in topological order, with no consumer asking yet. Leave a
		// promise; whoever reads this will redeem it against its own geometry.
		SocketValue sv;
		sv.type = SocketType::Vector;
		sv.is_lazy = true;
		sv.lazy_node = p_node_idx;
		sv.lazy_socket = 0;
		p_ctx.write_output(p_node_idx, 0, sv);
		return;
	}
	if (geo->get_mesh().is_null() || geo->get_mesh()->vertex_count() == 0) {
		p_ctx.write_vector(p_node_idx, 0, Vector3());
		return;
	}
	p_ctx.write_vec_field(p_node_idx, 0, geo->get_mesh()->positions, AttrDomain::Point);
}

// --- NormalNode ---

void NormalNode::_bind_methods() {
}

NormalNode::NormalNode() {
	// Blender's Normal has no inputs, for the same reason Position has none.
	// TWO outputs: the shading normal, which may be smoothed or custom, and the
	// TRUE normal, which is the face's own geometry. A graph that needs to know
	// which way a face actually points has to ask for the second one, and ours
	// had no way to.
	output_defs.push_back({ "Normal", SocketType::Vector, Variant() });
	output_defs.push_back({ "True Normal", SocketType::Vector, Variant() });
}

void NormalNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	Ref<ProcGeometry> geo = p_ctx.context_geometry;
	if (geo.is_null()) {
		SocketValue sv;
		sv.type = SocketType::Vector;
		sv.is_lazy = true;
		sv.lazy_node = p_node_idx;
		sv.lazy_socket = 0;
		p_ctx.write_output(p_node_idx, 0, sv);
		return;
	}
	Ref<ProcMesh> mesh = geo->get_mesh();
	if (mesh.is_null() || mesh->normals.size() == 0) {
		p_ctx.write_vector(p_node_idx, 0, Vector3(0, 1, 0));
		p_ctx.write_vector(p_node_idx, 1, Vector3(0, 1, 0));
		return;
	}
	p_ctx.write_vec_field(p_node_idx, 0, mesh->normals, AttrDomain::Point);

	// TRUE NORMAL is recomputed from the triangles rather than read from the
	// stored normals, which is the whole distinction: the stored ones can be
	// smoothed, custom, or flipped by a node, and the true one is what the
	// geometry says. Area-weighted from the faces touching each vertex.
	PackedVector3Array truth;
	truth.resize(mesh->positions.size());
	for (int i = 0; i < truth.size(); i++) {
		truth.ptrw()[i] = Vector3();
	}
	const int32_t *idx = mesh->indices.ptr();
	for (int t = 0; t + 2 < mesh->indices.size(); t += 3) {
		const Vector3 &a = mesh->positions[idx[t]];
		const Vector3 &b = mesh->positions[idx[t + 1]];
		const Vector3 &c = mesh->positions[idx[t + 2]];
		Vector3 n = (b - a).cross(c - a);
		truth.ptrw()[idx[t]] += n;
		truth.ptrw()[idx[t + 1]] += n;
		truth.ptrw()[idx[t + 2]] += n;
	}
	for (int i = 0; i < truth.size(); i++) {
		Vector3 n = truth[i];
		truth.ptrw()[i] = n.length_squared() > 0.0f ? n.normalized() : mesh->normals[i];
	}
	p_ctx.write_vec_field(p_node_idx, 1, truth, AttrDomain::Point);
}

// --- IndexNode ---

void IndexNode::_bind_methods() {
}

IndexNode::IndexNode() {
	output_defs.push_back({ "Index", SocketType::Int, Variant() });
}

void IndexNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	// A CONSTANT 0 IS NOT AN INDEX. This wrote one with a TODO saying a field
	// "requires knowing element count from a geometry consumer downstream" --
	// which is exactly what the lazy promise PositionNode already leaves, so
	// the mechanism it was waiting for had arrived.
	//
	// It matters beyond the node: Index is the canonical source for Random
	// Value's ID, so with a constant 0 every element drew the SAME random
	// number, and the node whose whole purpose is per-element variation had
	// none. That read as a Random Value bug.
	Ref<ProcGeometry> geo = p_ctx.context_geometry;
	if (geo.is_null()) {
		// Running in topological order, with no consumer asking yet. Leave a
		// promise; whoever reads this will redeem it against its own geometry.
		SocketValue sv;
		sv.type = SocketType::Int;
		sv.is_lazy = true;
		sv.lazy_node = p_node_idx;
		sv.lazy_socket = 0;
		p_ctx.write_output(p_node_idx, 0, sv);
		return;
	}
	if (geo->get_mesh().is_null() || geo->get_mesh()->vertex_count() == 0) {
		p_ctx.write_int(p_node_idx, 0, 0);
		return;
	}
	// ON THE CONSUMER'S DOMAIN. Index is not a point attribute that gets
	// adapted -- it is the index of the element being evaluated, so on the
	// corner domain it runs 0..corner_count-1. Adapting the point version
	// instead would hand back each corner's VERTEX index, which is a different
	// sequence that looks equally plausible.
	const AttrDomain dom = p_ctx.context_domain;
	const int n = geo->get_mesh()->domain_size(dom);
	if (n <= 0) {
		p_ctx.write_int(p_node_idx, 0, 0);
		return;
	}
	PackedInt32Array idx;
	idx.resize(n);
	int32_t *w = idx.ptrw();
	for (int i = 0; i < n; i++) {
		w[i] = i;
	}
	p_ctx.write_int_field(p_node_idx, 0, idx, dom);
}

// --- StoreNamedAttributeNode ---

// --- UVNode ---

void UVNode::_bind_methods() {}

UVNode::UVNode() {
	input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
	output_defs.push_back({ "UV", SocketType::Vector, Variant() });
}

void UVNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
	if (geo.is_null() || geo->get_mesh().is_null() || geo->get_mesh()->uvs.size() == 0) {
		p_ctx.write_vector(p_node_idx, 0, Vector3());
		return;
	}
	// Convert UV2 to vec3 field (Z=0)
	Ref<ProcMesh> mesh = geo->get_mesh();
	int count = mesh->uvs.size();
	PackedVector3Array uv_field;
	uv_field.resize(count);
	Vector3 *out = uv_field.ptrw();
	const Vector2 *uvs = mesh->uvs.ptr();
	for (int i = 0; i < count; i++) {
		out[i] = Vector3(uvs[i].x, 0.0f, uvs[i].y);
	}
	p_ctx.write_vec_field(p_node_idx, 0, uv_field);
}

String UVNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const {
	// In GPU context, UV is derived from position on XZ plane.
	// For standalone function mode (DC terrain), pos IS the world position
	// and UV-like 2D coords are just pos.xz. The upstream MeshGrid sets
	// UV metadata; in GLSL we pass pos through since noise samples pos directly.
	return "// UV: position passthrough (noise samples pos.xz directly)";
}

// --- StoreNamedAttributeNode ---

void StoreNamedAttributeNode::_bind_methods() {
	ClassDB::bind_method(D_METHOD("set_attr_name", "name"), &StoreNamedAttributeNode::set_attr_name);
	ClassDB::bind_method(D_METHOD("get_attr_name"), &StoreNamedAttributeNode::get_attr_name);
	ClassDB::bind_method(D_METHOD("set_attr_domain", "domain"), &StoreNamedAttributeNode::set_attr_domain);
	ClassDB::bind_method(D_METHOD("get_attr_domain"), &StoreNamedAttributeNode::get_attr_domain);
	ClassDB::bind_method(D_METHOD("set_attr_type", "type"), &StoreNamedAttributeNode::set_attr_type);
	ClassDB::bind_method(D_METHOD("get_attr_type"), &StoreNamedAttributeNode::get_attr_type);

	ClassDB::bind_method(D_METHOD("set_data_type", "type"), &StoreNamedAttributeNode::set_data_type);
	ClassDB::bind_method(D_METHOD("get_data_type"), &StoreNamedAttributeNode::get_data_type);

	ADD_PROPERTY(PropertyInfo(Variant::STRING, "attr_name"), "set_attr_name", "get_attr_name");
	ADD_PROPERTY(PropertyInfo(Variant::INT, "attr_domain"), "set_attr_domain", "get_attr_domain");
	ADD_PROPERTY(PropertyInfo(Variant::INT, "attr_type"), "set_attr_type", "get_attr_type");
	ADD_PROPERTY(PropertyInfo(Variant::INT, "data_type"), "set_data_type", "get_data_type");
}

// Blender's data_type is a CD_PROP_* code, and it decides BOTH the Value
// socket's type and the type of the attribute written. Measured off Blender
// 5.2 rather than transcribed -- the codes are not 0..n and the set is larger
// than the six the serializer's comment lists.
//
// An unrecognised code leaves the node as it was and says so: silently
// treating an unknown type as Float is how a stored attribute becomes its own
// red channel.
void StoreNamedAttributeNode::set_data_type(int p_type) {
	SocketType sock;
	AttrType attr;
	switch (p_type) {
		case 10: sock = SocketType::Float;    attr = AttrType::Float;      break; // FLOAT
		case 11: sock = SocketType::Int;      attr = AttrType::Int;        break; // INT
		case 45: sock = SocketType::Int;      attr = AttrType::Int;        break; // INT8
		case 50: sock = SocketType::Bool;     attr = AttrType::Bool;       break; // BOOLEAN
		case 48: sock = SocketType::Vector;   attr = AttrType::Float3;     break; // FLOAT_VECTOR
		case 49: sock = SocketType::Vector;   attr = AttrType::Float2;     break; // FLOAT2
		case 47: sock = SocketType::Color;    attr = AttrType::Color;      break; // FLOAT_COLOR
		case 17: sock = SocketType::Color;    attr = AttrType::ColorByte;  break; // BYTE_COLOR
		case 52: sock = SocketType::Rotation; attr = AttrType::Quaternion; break; // QUATERNION
		case 20: sock = SocketType::Matrix;   attr = AttrType::Float4x4;   break; // FLOAT4X4
		default:
			UtilityFunctions::push_warning(
					"StoreNamedAttribute: unrecognised Blender data_type ", p_type,
					"; leaving the Value socket as it was rather than guessing Float.");
			return;
	}
	data_type = p_type;
	attr_type = (int)attr;
	input_defs.ptrw()[3].type = sock;
}

StoreNamedAttributeNode::StoreNamedAttributeNode() {
	// BLENDER'S FOUR INPUTS. Ours had two, so Selection did not exist and Value
	// sat where Blender puts the NAME -- a graph wiring a value drove the name
	// socket's index and a graph wiring a name was dropped.
	input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
	input_defs.push_back({ "Selection", SocketType::Bool, true });
	input_defs.push_back({ "Name", SocketType::String, String() });
	input_defs.push_back({ "Value", SocketType::Float, 0.0f });
	output_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
}

void StoreNamedAttributeNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
	if (geo.is_null()) {
		p_ctx.write_geometry(p_node_idx, 0, geo);
		return;
	}

	// SOCKET 3 -- Value moved when Selection and Name took 1 and 2.
	//
	// READ ON THE TARGET DOMAIN. `read_input` redeems a promise against the
	// context geometry on the POINT domain, and the value was then indexed
	// straight into the destination: on a 3x2 grid, corner 1 took vertex 1's
	// value where Blender gives vertex 2's, and corners past the end of the
	// point array came back ZERO. materialize_on evaluates the field on this
	// node's own domain and adapts anything that arrives on another.
	const AttrDomain target_domain = static_cast<AttrDomain>(attr_domain);
	SocketValue val = p_ctx.materialize_on(
			p_ctx._read_input_raw(p_node_idx, 3), geo, target_domain);
	// The name comes from the SOCKET when a graph supplies one, and falls back
	// to the node property so graphs authored in the Godot editor keep working.
	{
		String socket_name = p_ctx.read_input(p_node_idx, 2).str;
		if (!socket_name.is_empty()) {
			attr_name = StringName(socket_name);
		}
	}
	geo = geo->duplicate_geometry();

	AttrDomain domain = target_domain;
	AttrType type = static_cast<AttrType>(attr_type);

	if (geo->get_mesh().is_valid()) {
		Ref<ProcMesh> mesh = geo->get_mesh();
		int count = mesh->domain_size(domain);
		if (count > 0) {
			// Special case: writing to "color" attribute sets vertex colors directly
			if (attr_name == StringName("color") && domain == AttrDomain::Point) {
				mesh->colors.resize(count);
				Color *colors = mesh->colors.ptrw();
				if (val.is_field && val.type == SocketType::Color) {
					for (int i = 0; i < count; i++) {
						colors[i] = val.color_at(i);
					}
				} else if (val.is_field) {
					// Float field → grayscale color
					for (int i = 0; i < count; i++) {
						float v = val.float_at(i);
						colors[i] = Color(v, v, v, 1.0f);
					}
				} else {
					float v = val.to_float();
					Color c(v, v, v, 1.0f);
					for (int i = 0; i < count; i++) {
						colors[i] = c;
					}
				}
			} else {
				// General named attribute storage
				Ref<AttributeArray> attr;
				attr.instantiate();
				attr->name = attr_name;
				attr->type = type;
				attr->domain = domain;
				attr->resize(count);

				// EVERY TYPE data_type CAN SELECT. Float and Int were the only
				// two written, so a FLOAT_VECTOR or FLOAT_COLOR attribute was
				// allocated at the right size and left as zeros -- which reads
				// back as a black colour or no offset, not as an error.
				switch (type) {
					case AttrType::Float: {
						float *data = attr->data_ptrw<float>();
						for (int i = 0; i < count; i++) {
							data[i] = val.float_at(i);
						}
					} break;
					case AttrType::Int: {
						int32_t *data = attr->data_ptrw<int32_t>();
						for (int i = 0; i < count; i++) {
							data[i] = val.int_at(i);
						}
					} break;
					case AttrType::Bool: {
						// One BYTE per element, not one int.
						uint8_t *data = attr->data_ptrw<uint8_t>();
						for (int i = 0; i < count; i++) {
							data[i] = val.bool_at(i) ? 1 : 0;
						}
					} break;
					case AttrType::Float2: {
						Vector2 *data = attr->data_ptrw<Vector2>();
						for (int i = 0; i < count; i++) {
							Vector3 v = val.vec_at(i);
							data[i] = Vector2(v.x, v.y);
						}
					} break;
					case AttrType::Float3: {
						Vector3 *data = attr->data_ptrw<Vector3>();
						for (int i = 0; i < count; i++) {
							data[i] = val.vec_at(i);
						}
					} break;
					case AttrType::Color: {
						Color *data = attr->data_ptrw<Color>();
						for (int i = 0; i < count; i++) {
							data[i] = val.color_at(i);
						}
					} break;
					case AttrType::ColorByte: {
						// Four bytes, so the float channels are quantised here
						// rather than stored wide and truncated on read.
						uint8_t *data = attr->data_ptrw<uint8_t>();
						for (int i = 0; i < count; i++) {
							Color c = val.color_at(i);
							data[i * 4 + 0] = (uint8_t)CLAMP(Math::round(c.r * 255.0f), 0.0f, 255.0f);
							data[i * 4 + 1] = (uint8_t)CLAMP(Math::round(c.g * 255.0f), 0.0f, 255.0f);
							data[i * 4 + 2] = (uint8_t)CLAMP(Math::round(c.b * 255.0f), 0.0f, 255.0f);
							data[i * 4 + 3] = (uint8_t)CLAMP(Math::round(c.a * 255.0f), 0.0f, 255.0f);
						}
					} break;
					case AttrType::Quaternion: {
						Quaternion *data = attr->data_ptrw<Quaternion>();
						for (int i = 0; i < count; i++) {
							data[i] = val.quat_at(i);
						}
					} break;
					case AttrType::Float4x4: {
						// 16 floats per element, column-major, matching
						// SocketValue::mat_at.
						float *data = attr->data_ptrw<float>();
						for (int i = 0; i < count; i++) {
							Projection m = val.mat_at(i);
							for (int c = 0; c < 4; c++) {
								for (int r = 0; r < 4; r++) {
									data[i * 16 + c * 4 + r] = m.columns[c][r];
								}
							}
						}
					} break;
					default:
						UtilityFunctions::push_warning(
								"StoreNamedAttribute: no writer for attribute type ",
								attr_type_name(type), "; the attribute is allocated and empty.");
						break;
				}

				switch (domain) {
					case AttrDomain::Point:
						mesh->point_attrs.set(attr_name, attr);
						break;
					case AttrDomain::Face:
						mesh->face_attrs.set(attr_name, attr);
						break;
					case AttrDomain::Corner:
						mesh->corner_attrs.set(attr_name, attr);
						break;
					case AttrDomain::Edge:
						mesh->edge_attrs.set(attr_name, attr);
						break;
					default:
						break;
				}
			}
		}
	}

	p_ctx.write_geometry(p_node_idx, 0, geo);
}

// --- NamedAttributeNode ---

void NamedAttributeNode::_bind_methods() {
	ClassDB::bind_method(D_METHOD("set_attr_name", "name"), &NamedAttributeNode::set_attr_name);
	ClassDB::bind_method(D_METHOD("get_attr_name"), &NamedAttributeNode::get_attr_name);
	ClassDB::bind_method(D_METHOD("set_data_type", "type"), &NamedAttributeNode::set_data_type);
	ClassDB::bind_method(D_METHOD("get_data_type"), &NamedAttributeNode::get_data_type);

	ADD_PROPERTY(PropertyInfo(Variant::STRING, "attr_name"), "set_attr_name", "get_attr_name");
	ADD_PROPERTY(PropertyInfo(Variant::INT, "data_type"), "set_data_type", "get_data_type");
}

// The READ side of the same typing. Blender's Named Attribute output socket is
// chosen by data_type just as Store's Value input is, and ours was a fixed
// Float that returned 0 for every attribute that was not one.
void NamedAttributeNode::set_data_type(int p_type) {
	SocketType sock;
	switch (p_type) {
		case 10: sock = SocketType::Float; break; // FLOAT
		case 11:
		case 45: sock = SocketType::Int; break; // INT, INT8
		case 50: sock = SocketType::Bool; break; // BOOLEAN
		case 48:
		case 49: sock = SocketType::Vector; break; // FLOAT_VECTOR, FLOAT2
		case 47:
		case 17: sock = SocketType::Color; break; // FLOAT_COLOR, BYTE_COLOR
		case 52: sock = SocketType::Rotation; break; // QUATERNION
		case 20: sock = SocketType::Matrix; break; // FLOAT4X4
		default:
			UtilityFunctions::push_warning(
					"NamedAttribute: unrecognised Blender data_type ", p_type,
					"; leaving the output socket as it was rather than guessing Float.");
			return;
	}
	data_type = p_type;
	output_defs.ptrw()[0].type = sock;
}

NamedAttributeNode::NamedAttributeNode() {
	// Blender's Name is a SOCKET, not a property -- it can be computed. And
	// there are two outputs: the attribute and whether it EXISTS, which is the
	// only way a graph can branch on a missing attribute rather than silently
	// reading zeros.
	input_defs.push_back({ "Name", SocketType::String, String() });
	output_defs.push_back({ "Attribute", SocketType::Float, Variant() });
	output_defs.push_back({ "Exists", SocketType::Bool, false });
}

void NamedAttributeNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	{
		String socket_name = p_ctx.read_input(p_node_idx, 0).str;
		if (!socket_name.is_empty()) {
			attr_name = StringName(socket_name);
		}
	}
	Ref<ProcGeometry> geo = p_ctx.context_geometry;
	if (geo.is_null()) {
		SocketValue sv;
		sv.type = SocketType::Float;
		sv.is_lazy = true;
		sv.lazy_node = p_node_idx;
		sv.lazy_socket = 0;
		p_ctx.write_output(p_node_idx, 0, sv);
		return;
	}
	if (geo->get_mesh().is_null()) {
		p_ctx.write_float(p_node_idx, 0, 0.0f);
		return;
	}

	// Look up attribute and return average value (scalar context)
	Ref<ProcMesh> mesh = geo->get_mesh();
	Ref<AttributeArray> attr = mesh->point_attrs.get(attr_name);
	// Blender's Named Attribute is a FIELD, and it is a field OF THE ATTRIBUTE'S
	// OWN TYPE. Only Float was handled here, so an Int, Bool, vector or colour
	// attribute fell through to the averaging below and came out as 0 -- which
	// reads as "the attribute is not there", not as "this node cannot read it".
	if (attr.is_valid() && attr->size() > 0) {
		const int n = attr->size();
		switch (attr->type) {
			case AttrType::Float: {
				PackedFloat32Array vals;
				vals.resize(n);
				const float *src = attr->data_ptr<float>();
				float *dst = vals.ptrw();
				for (int i = 0; i < n; i++) {
					dst[i] = src[i];
				}
				p_ctx.write_float_field(p_node_idx, 0, vals, AttrDomain::Point);
				return;
			}
			case AttrType::Int: {
				PackedInt32Array vals;
				vals.resize(n);
				const int32_t *src = attr->data_ptr<int32_t>();
				int32_t *dst = vals.ptrw();
				for (int i = 0; i < n; i++) {
					dst[i] = src[i];
				}
				p_ctx.write_int_field(p_node_idx, 0, vals, AttrDomain::Point);
				return;
			}
			case AttrType::Bool: {
				// Stored one BYTE per element; the field carries ints.
				PackedInt32Array vals;
				vals.resize(n);
				const uint8_t *src = attr->data_ptr<uint8_t>();
				int32_t *dst = vals.ptrw();
				for (int i = 0; i < n; i++) {
					dst[i] = src[i] ? 1 : 0;
				}
				p_ctx.write_int_field(p_node_idx, 0, vals, AttrDomain::Point);
				return;
			}
			case AttrType::Float2: {
				PackedVector3Array vals;
				vals.resize(n);
				const Vector2 *src = attr->data_ptr<Vector2>();
				Vector3 *dst = vals.ptrw();
				for (int i = 0; i < n; i++) {
					dst[i] = Vector3(src[i].x, src[i].y, 0.0f);
				}
				p_ctx.write_vec_field(p_node_idx, 0, vals, AttrDomain::Point);
				return;
			}
			case AttrType::Float3: {
				PackedVector3Array vals;
				vals.resize(n);
				const Vector3 *src = attr->data_ptr<Vector3>();
				Vector3 *dst = vals.ptrw();
				for (int i = 0; i < n; i++) {
					dst[i] = src[i];
				}
				p_ctx.write_vec_field(p_node_idx, 0, vals, AttrDomain::Point);
				return;
			}
			case AttrType::Color: {
				PackedColorArray vals;
				vals.resize(n);
				const Color *src = attr->data_ptr<Color>();
				Color *dst = vals.ptrw();
				for (int i = 0; i < n; i++) {
					dst[i] = src[i];
				}
				p_ctx.write_color_field(p_node_idx, 0, vals, AttrDomain::Point);
				return;
			}
			case AttrType::ColorByte: {
				PackedColorArray vals;
				vals.resize(n);
				const uint8_t *src = attr->data_ptr<uint8_t>();
				Color *dst = vals.ptrw();
				for (int i = 0; i < n; i++) {
					dst[i] = Color(src[i * 4 + 0] / 255.0f, src[i * 4 + 1] / 255.0f,
							src[i * 4 + 2] / 255.0f, src[i * 4 + 3] / 255.0f);
				}
				p_ctx.write_color_field(p_node_idx, 0, vals, AttrDomain::Point);
				return;
			}
			case AttrType::Quaternion: {
				PackedVector4Array vals;
				vals.resize(n);
				const Quaternion *src = attr->data_ptr<Quaternion>();
				Vector4 *dst = vals.ptrw();
				for (int i = 0; i < n; i++) {
					dst[i] = Vector4(src[i].x, src[i].y, src[i].z, src[i].w);
				}
				p_ctx.write_rot_field(p_node_idx, 0, vals, AttrDomain::Point);
				return;
			}
			default:
				// Float4x4 and the 2D integer types have no field writer to
				// hand; fall through to the averaging path, which says 0.
				break;
		}
	}
	if (attr.is_null()) {
		attr = mesh->face_attrs.get(attr_name);
	}
	if (attr.is_null()) {
		attr = mesh->corner_attrs.get(attr_name);
	}
	// The EDGE domain is reachable now that ProcMesh derives edges; before,
	// nothing could be stored there so nothing could be read back either.
	if (attr.is_null()) {
		attr = mesh->edge_attrs.get(attr_name);
	}

	if (attr.is_null() || attr->size() == 0) {
		p_ctx.write_float(p_node_idx, 0, 0.0f);
		return;
	}

	// Return average for scalar context
	if (attr->type == AttrType::Float) {
		const float *data = attr->data_ptr<float>();
		float sum = 0.0f;
		for (int i = 0; i < attr->size(); i++) {
			sum += data[i];
		}
		p_ctx.write_float(p_node_idx, 0, sum / static_cast<float>(attr->size()));
	} else {
		p_ctx.write_float(p_node_idx, 0, 0.0f);
	}
}

// --- CaptureAttributeNode ---

void CaptureAttributeNode::_bind_methods() {
	ClassDB::bind_method(D_METHOD("set_item_names", "n"), &CaptureAttributeNode::set_item_names);
	ClassDB::bind_method(D_METHOD("get_item_names"), &CaptureAttributeNode::get_item_names);
	ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "item_names"),
			"set_item_names", "get_item_names");
	ClassDB::bind_method(D_METHOD("set_item_types", "t"), &CaptureAttributeNode::set_item_types);
	ClassDB::bind_method(D_METHOD("get_item_types"), &CaptureAttributeNode::get_item_types);
	ADD_PROPERTY(PropertyInfo(Variant::PACKED_INT32_ARRAY, "item_types"),
			"set_item_types", "get_item_types");
	ClassDB::bind_method(D_METHOD("set_domain", "d"), &CaptureAttributeNode::set_domain);
	ClassDB::bind_method(D_METHOD("get_domain"), &CaptureAttributeNode::get_domain);
	ADD_PROPERTY(PropertyInfo(Variant::INT, "domain"), "set_domain", "get_domain");
}

// Blender's CD_PROP_* -> our socket type, for an item.
static SocketType _capture_socket_type(int p_cd, Variant &r_zero) {
	switch (p_cd) {
		case 11: case 45: r_zero = 0; return SocketType::Int;
		case 50: r_zero = false; return SocketType::Bool;
		case 48: case 49: r_zero = Vector3(); return SocketType::Vector;
		case 47: case 17: r_zero = Color(0, 0, 0, 1); return SocketType::Color;
		case 52: r_zero = Quaternion(); return SocketType::Rotation;
		case 20: r_zero = Projection(); return SocketType::Matrix;
		default: r_zero = 0.0f; return SocketType::Float;
	}
}

// THE ITEM LIST BUILDS THE SOCKETS. Geometry and Selection are fixed; each item
// contributes one input and one output of its own type, in list order, exactly
// as Blender lays them out.
void CaptureAttributeNode::_rebuild_sockets() {
	input_defs.clear();
	output_defs.clear();
	input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
	input_defs.push_back({ "Selection", SocketType::Bool, true });
	output_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
	output_defs.push_back({ "Selection", SocketType::Bool, Variant() });

	const int n = item_names.size();
	for (int i = 0; i < n; i++) {
		Variant zero;
		const int cd = (i < item_types.size()) ? item_types[i] : 10;
		const SocketType st = _capture_socket_type(cd, zero);
		const StringName nm = StringName(item_names[i]);
		input_defs.push_back({ nm, st, zero });
		output_defs.push_back({ nm, st, Variant() });
	}
}

CaptureAttributeNode::CaptureAttributeNode() {
	// No items until the graph says so, which is Blender's empty node.
	_rebuild_sockets();
}

void CaptureAttributeNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
	p_ctx.write_geometry(p_node_idx, 0, geo);
	p_ctx.write_output(p_node_idx, 1, p_ctx.read_input(p_node_idx, 1));

	// CAPTURING IS EVALUATING NOW. Each item's field is materialised against
	// THIS geometry and handed on as a concrete field, so a later change of
	// topology cannot re-evaluate it -- which is the whole point of the node,
	// and what a pass-through could never do.
	const int n = item_names.size();
	for (int i = 0; i < n; i++) {
		SocketValue v = p_ctx.read_input(p_node_idx, 2 + i);
		if (geo.is_valid()) {
			v = p_ctx.materialize(v, geo);
		}
		p_ctx.write_output(p_node_idx, 2 + i, v);
	}
}

// --- RemoveNamedAttributeNode ---

void RemoveNamedAttributeNode::_bind_methods() {
	ClassDB::bind_method(D_METHOD("set_attr_name", "name"), &RemoveNamedAttributeNode::set_attr_name);
	ClassDB::bind_method(D_METHOD("get_attr_name"), &RemoveNamedAttributeNode::get_attr_name);

	ADD_PROPERTY(PropertyInfo(Variant::STRING, "attr_name"), "set_attr_name", "get_attr_name");
}

RemoveNamedAttributeNode::RemoveNamedAttributeNode() {
	input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
	// Blender 5.x MENU socket: Exact or Wildcard. Carried as an int; only
	// Exact is implemented, and a Wildcard pattern says so rather than
	// quietly matching nothing.
	input_defs.push_back({ "Pattern Mode", SocketType::Int, 0 });
	input_defs.push_back({ "Name", SocketType::String, String() });
	output_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
}

void RemoveNamedAttributeNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
	if (geo.is_null()) {
		p_ctx.write_geometry(p_node_idx, 0, geo);
		return;
	}
	{
		String socket_name = p_ctx.read_input(p_node_idx, 2).str;
		if (!socket_name.is_empty()) {
			attr_name = StringName(socket_name);
		}
	}
	if (p_ctx.read_int(p_node_idx, 1) != 0) {
		UtilityFunctions::push_warning(
				"RemoveNamedAttribute: only the Exact pattern mode is implemented; "
				"a wildcard removes nothing.");
	}

	geo = geo->duplicate_geometry();

	if (geo->get_mesh().is_valid()) {
		Ref<ProcMesh> mesh = geo->get_mesh();
		mesh->point_attrs.remove(attr_name);
		mesh->face_attrs.remove(attr_name);
		mesh->corner_attrs.remove(attr_name);
		mesh->edge_attrs.remove(attr_name);
	}

	p_ctx.write_geometry(p_node_idx, 0, geo);
}

// --- DomainSizeNode ---

void DomainSizeNode::_bind_methods() {
}

DomainSizeNode::DomainSizeNode() {
	// BLENDER'S SEVEN, IN BLENDER'S ORDER. Ours had five and no Edge Count, so
	// from index 1 on every output was one place along: a graph reading Edge
	// Count got Face Count, Face Count got Face Corner Count, and the link to
	// Layer Count addressed a socket that did not exist. The two renames are
	// Blender's spellings -- "Face Corner Count", not "Corner Count", and
	// "Spline Count", not "Curve Count" -- so they resolve by name as well.
	input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
	output_defs.push_back({ "Point Count", SocketType::Int, Variant() });
	output_defs.push_back({ "Edge Count", SocketType::Int, Variant() });
	output_defs.push_back({ "Face Count", SocketType::Int, Variant() });
	output_defs.push_back({ "Face Corner Count", SocketType::Int, Variant() });
	output_defs.push_back({ "Spline Count", SocketType::Int, Variant() });
	output_defs.push_back({ "Instance Count", SocketType::Int, Variant() });
	output_defs.push_back({ "Layer Count", SocketType::Int, Variant() });
}

void DomainSizeNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
	if (geo.is_null()) {
		for (int i = 0; i < 7; i++) {
			p_ctx.write_int(p_node_idx, i, 0);
		}
		return;
	}

	// ProcMesh derives the edge domain now, so this asks the mesh rather than
	// counting pairs here. It will still NOT always equal Blender's, and that
	// is not an error: where Blender keeps quads and we carry triangles the two
	// meshes genuinely have different edges.
	int edge_count = 0;
	if (geo->get_mesh().is_valid()) {
		edge_count = geo->get_mesh()->edge_count();
	}

	p_ctx.write_int(p_node_idx, 0, geo->domain_size(AttrDomain::Point));
	p_ctx.write_int(p_node_idx, 1, edge_count);
	p_ctx.write_int(p_node_idx, 2, geo->domain_size(AttrDomain::Face));
	p_ctx.write_int(p_node_idx, 3, geo->domain_size(AttrDomain::Corner));
	p_ctx.write_int(p_node_idx, 4, geo->domain_size(AttrDomain::Curve));
	p_ctx.write_int(p_node_idx, 5, geo->domain_size(AttrDomain::Instance));
	// Layers are Grease Pencil, which ProcGeometry has no component for.
	p_ctx.write_int(p_node_idx, 6, 0);
}

} // namespace godot