OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes / src / nodes / mesh_ops_nodes.cpp

1040 lines · cpp ref:
#include "mesh_ops_nodes.h"
#include <godot_cpp/templates/hash_set.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
#include "../graph/graph_executor.h"
#include <cmath>

namespace godot {

// --- FlipFacesNode ---

void FlipFacesNode::_bind_methods() {
}

FlipFacesNode::FlipFacesNode() {
	input_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
	// Blender's Selection, which decides WHICH faces flip. Without it the node
	// flipped every face while advertising no choice about it.
	input_defs.push_back({ "Selection", SocketType::Bool, true });
	output_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
}

void FlipFacesNode::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()) {
		p_ctx.write_geometry(p_node_idx, 0, geo);
		return;
	}

	// SELECTION IS PER FACE, and it is a field. A selection arriving on the
	// POINT domain is interpolated the way Blender interpolates a boolean onto
	// faces: the face is selected only if EVERY one of its corners is.
	SocketValue sel = p_ctx.read_input(p_node_idx, 1);

	geo = geo->duplicate_geometry();
	Ref<ProcMesh> mesh = geo->get_mesh();

	const bool by_point = sel.is_field && sel.domain == AttrDomain::Point;
	int32_t *idx = mesh->indices.ptrw();
	const int tcount = mesh->indices.size() / 3;
	Vector<bool> flipped_vert;
	flipped_vert.resize(mesh->normals.size());
	for (int i = 0; i < flipped_vert.size(); i++) {
		flipped_vert.set(i, false);
	}
	for (int t = 0; t < tcount; t++) {
		int32_t *tri = idx + t * 3;
		bool selected = by_point
				? (sel.bool_at(tri[0]) && sel.bool_at(tri[1]) && sel.bool_at(tri[2]))
				: sel.bool_at(t);
		if (!selected) {
			continue;
		}
		int32_t tmp = tri[1];
		tri[1] = tri[2];
		tri[2] = tmp;
		// Only the normals of vertices belonging to a flipped face turn, and
		// each turns once however many flipped faces share it.
		for (int k = 0; k < 3; k++) {
			if (tri[k] < flipped_vert.size()) {
				flipped_vert.set(tri[k], true);
			}
		}
	}
	Vector3 *nor = mesh->normals.ptrw();
	for (int i = 0; i < mesh->normals.size(); i++) {
		if (flipped_vert[i]) {
			nor[i] = -nor[i];
		}
	}

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

// --- SetShadeSmoothNode ---

void SetShadeSmoothNode::_bind_methods() {
}

SetShadeSmoothNode::SetShadeSmoothNode() {
	// Blender's order: Geometry, Selection, Shade Smooth. Ours had the flag
	// where the SELECTION goes, so a graph wiring a selection set the flag and
	// a graph wiring the flag set nothing.
	input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
	input_defs.push_back({ "Selection", SocketType::Bool, true });
	input_defs.push_back({ "Shade Smooth", SocketType::Bool, true });
	output_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
}

void SetShadeSmoothNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
	// SOCKET 2. Selection took socket 1 to match Blender's order, and reading
	// the old index here would have made a wired selection set the flag.
	bool smooth = p_ctx.read_bool(p_node_idx, 2);

	if (geo.is_null() || geo->get_mesh().is_null()) {
		p_ctx.write_geometry(p_node_idx, 0, geo);
		return;
	}

	geo = geo->duplicate_geometry();
	Ref<ProcMesh> mesh = geo->get_mesh();

	if (smooth) {
		// Smooth: recalculate vertex normals (area-weighted average)
		mesh->recalculate_normals();
	} else {
		// Flat: split vertices so each triangle has its own normals
		int tri_count = mesh->indices.size() / 3;
		PackedVector3Array new_pos;
		PackedVector3Array new_nor;
		PackedVector2Array new_uv;
		PackedInt32Array new_idx;

		new_pos.resize(tri_count * 3);
		new_nor.resize(tri_count * 3);
		new_uv.resize(tri_count * 3);
		new_idx.resize(tri_count * 3);

		const Vector3 *old_pos = mesh->positions.ptr();
		const int32_t *old_idx = mesh->indices.ptr();
		const Vector2 *old_uv = mesh->uvs.size() > 0 ? mesh->uvs.ptr() : nullptr;

		Vector3 *np = new_pos.ptrw();
		Vector3 *nn = new_nor.ptrw();
		Vector2 *nu = new_uv.ptrw();
		int32_t *ni = new_idx.ptrw();

		for (int t = 0; t < tri_count; t++) {
			int i0 = old_idx[t * 3];
			int i1 = old_idx[t * 3 + 1];
			int i2 = old_idx[t * 3 + 2];

			Vector3 p0 = old_pos[i0], p1 = old_pos[i1], p2 = old_pos[i2];
			Vector3 fn = -(p1 - p0).cross(p2 - p0).normalized();

			int base = t * 3;
			np[base] = p0; np[base + 1] = p1; np[base + 2] = p2;
			nn[base] = fn; nn[base + 1] = fn; nn[base + 2] = fn;

			if (old_uv) {
				nu[base] = old_uv[i0];
				nu[base + 1] = old_uv[i1];
				nu[base + 2] = old_uv[i2];
			}

			ni[base] = base; ni[base + 1] = base + 1; ni[base + 2] = base + 2;
		}

		mesh->positions = new_pos;
		mesh->normals = new_nor;
		mesh->uvs = new_uv;
		mesh->indices = new_idx;
	}

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

// --- SubdivideMeshNode ---

void SubdivideMeshNode::_bind_methods() {
}

SubdivideMeshNode::SubdivideMeshNode() {
	input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
	input_defs.push_back({ "Level", SocketType::Int, 1 });
	output_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
}

void SubdivideMeshNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
	int levels = CLAMP(p_ctx.read_int(p_node_idx, 1), 0, 5);

	if (geo.is_null() || geo->get_mesh().is_null() || levels == 0) {
		p_ctx.write_geometry(p_node_idx, 0, geo);
		return;
	}

	geo = geo->duplicate_geometry();
	Ref<ProcMesh> mesh = geo->get_mesh();

	// ⛔ THIS NODE USED TO READ `indices` AS A FLAT TRIANGLE LIST.
	//
	// `indices` is the CORNER array; `face_offsets` groups it into faces. On a
	// triangle mesh those coincide, and for as long as every mesh arrived here
	// triangulated nobody could tell. The moment the bridge started carrying
	// Blender's own topology, the mountain's 10086-QUAD cage was read as 13448
	// triangles cut out of quad corners three at a time -- 1520981 vertices of
	// plausible nonsense against Blender's 2582018.
	//
	// Blender's Subdivide Mesh is SIMPLE (linear) subdivision, and its topology
	// step is Catmull-Clark's: every face becomes N QUADS, one per corner, each
	// spanning corner -> next edge midpoint -> face centre -> previous edge
	// midpoint. A quad becomes 4 quads; a TRIANGLE BECOMES 3 QUADS, not 4
	// triangles, which is what this produced before and is a different mesh.
	for (int level = 0; level < levels; level++) {
		const int face_count = mesh->face_count();
		const int32_t *idx = mesh->indices.ptr();
		const Vector3 *pos = mesh->positions.ptr();
		const bool has_nor = mesh->normals.size() == mesh->positions.size();
		const bool has_uv = mesh->uvs.size() == mesh->positions.size();

		PackedVector3Array new_positions = mesh->positions;
		PackedVector3Array new_normals = has_nor ? mesh->normals : PackedVector3Array();
		PackedVector2Array new_uvs = has_uv ? mesh->uvs : PackedVector2Array();
		PackedInt32Array new_corners;
		PackedInt32Array new_faces;

		HashMap<uint64_t, int> edge_midpoints;
		auto midpoint = [&](int a, int b) -> int {
			const uint64_t key = (a < b) ? ((uint64_t)a << 32 | (uint32_t)b)
										 : ((uint64_t)b << 32 | (uint32_t)a);
			HashMap<uint64_t, int>::ConstIterator it = edge_midpoints.find(key);
			if (it != edge_midpoints.end()) {
				return it->value;
			}
			const int n = new_positions.size();
			new_positions.push_back((pos[a] + pos[b]) * 0.5f);
			if (has_nor) {
				new_normals.push_back(((mesh->normals[a] + mesh->normals[b]) * 0.5f).normalized());
			}
			if (has_uv) {
				new_uvs.push_back((mesh->uvs[a] + mesh->uvs[b]) * 0.5f);
			}
			edge_midpoints[key] = n;
			return n;
		};

		new_faces.push_back(0);
		for (int f = 0; f < face_count; f++) {
			const int begin = mesh->face_corner_begin(f);
			const int n = mesh->face_size(f);
			if (n < 3) {
				continue;
			}
			// The face centre, once per face.
			Vector3 c_pos;
			Vector3 c_nor;
			Vector2 c_uv;
			for (int k = 0; k < n; k++) {
				const int v = idx[begin + k];
				c_pos += pos[v];
				if (has_nor) {
					c_nor += mesh->normals[v];
				}
				if (has_uv) {
					c_uv += mesh->uvs[v];
				}
			}
			const float inv = 1.0f / (float)n;
			const int centre = new_positions.size();
			new_positions.push_back(c_pos * inv);
			if (has_nor) {
				new_normals.push_back((c_nor * inv).normalized());
			}
			if (has_uv) {
				new_uvs.push_back(c_uv * inv);
			}

			for (int k = 0; k < n; k++) {
				const int v = idx[begin + k];
				const int v_next = idx[begin + (k + 1) % n];
				const int v_prev = idx[begin + (k + n - 1) % n];
				const int m_next = midpoint(v, v_next);
				const int m_prev = midpoint(v_prev, v);
				new_corners.push_back(v);
				new_corners.push_back(m_next);
				new_corners.push_back(centre);
				new_corners.push_back(m_prev);
				new_faces.push_back(new_corners.size());
			}
		}

		mesh->positions = new_positions;
		mesh->normals = new_normals;
		mesh->uvs = new_uvs;
		mesh->indices = new_corners;
		mesh->face_offsets = new_faces;
	}

	// Attributes cannot ride through this untouched: every domain changed size.
	// Dropping the ones that no longer fit is not a loss of information -- they
	// were already meaningless at the new size -- and it is said out loud rather
	// than left as an array that still reads as data.
	{
		const AttrDomain doms[4] = { AttrDomain::Point, AttrDomain::Edge,
				AttrDomain::Face, AttrDomain::Corner };
		AttributeStorage *st[4] = { &mesh->point_attrs, &mesh->edge_attrs,
				&mesh->face_attrs, &mesh->corner_attrs };
		for (int d = 0; d < 4; d++) {
			Vector<StringName> stale;
			for (const KeyValue<StringName, Ref<AttributeArray>> &kv : st[d]->attributes) {
				if (kv.value.is_valid() && kv.value->size() != mesh->domain_size(doms[d])) {
					stale.push_back(kv.key);
				}
			}
			for (int i = 0; i < stale.size(); i++) {
				UtilityFunctions::push_warning(
						"proc-nodes: Subdivide Mesh dropped attribute '", String(stale[i]),
						"' on the ", attr_domain_name(doms[d]),
						" domain -- subdivision changes that domain and this node does "
						"not interpolate yet.");
				st[d]->remove(stale[i]);
			}
		}
	}

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

// --- ScaleElementsNode ---

void ScaleElementsNode::_bind_methods() {
}

ScaleElementsNode::ScaleElementsNode() {
	// BLENDER'S SIX INPUTS, and SCALE IS A FLOAT. Ours had it as a Vector at
	// index 1 -- where Blender puts Selection -- so the type and the index were
	// both wrong: a graph wiring a selection drove the scale, and a graph
	// wiring a scale was a vector into a float socket.
	input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
	input_defs.push_back({ "Selection", SocketType::Bool, true });
	input_defs.push_back({ "Scale", SocketType::Float, 1.0f });
	input_defs.push_back({ "Center", SocketType::Vector, Vector3(0, 0, 0) });
	// Blender 5.x MENU: Uniform or Single Axis.
	input_defs.push_back({ "Scale Mode", SocketType::Int, 0 });
	input_defs.push_back({ "Axis", SocketType::Vector, Vector3(1, 0, 0) });
	output_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
}

void ScaleElementsNode::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()) {
		p_ctx.write_geometry(p_node_idx, 0, geo);
		return;
	}

	SocketValue sel = p_ctx.read_input(p_node_idx, 1);
	SocketValue scale_v = p_ctx.read_input(p_node_idx, 2);
	SocketValue center_v = p_ctx.read_input(p_node_idx, 3);
	const int scale_mode = p_ctx.read_int(p_node_idx, 4);
	SocketValue axis_v = p_ctx.read_input(p_node_idx, 5);

	geo = geo->duplicate_geometry();
	Ref<ProcMesh> mesh = geo->get_mesh();
	Vector3 *pos = mesh->positions.ptrw();

	// Blender scales each selected FACE about its own centre by default, the
	// Center socket carrying an implicit per-face field. proc-nodes has no
	// per-face implicit input, so an UNCONNECTED Center means the face's
	// centroid here -- which is what Blender does -- and a connected one is
	// used as given.
	const bool center_given = p_ctx.is_input_connected(p_node_idx, 3);
	const bool by_point = sel.is_field && sel.domain == AttrDomain::Point;
	const int32_t *idx = mesh->indices.ptr();
	const int tcount = mesh->indices.size() / 3;

	// A vertex shared by several selected faces would otherwise be scaled once
	// per face; Blender moves it once.
	Vector<bool> moved;
	moved.resize(mesh->positions.size());
	for (int i = 0; i < moved.size(); i++) {
		moved.set(i, false);
	}

	for (int t = 0; t < tcount; t++) {
		const int32_t *tri = idx + t * 3;
		bool selected = by_point
				? (sel.bool_at(tri[0]) && sel.bool_at(tri[1]) && sel.bool_at(tri[2]))
				: sel.bool_at(t);
		if (!selected) {
			continue;
		}
		Vector3 center = center_given
				? center_v.vec_at(t)
				: (pos[tri[0]] + pos[tri[1]] + pos[tri[2]]) / 3.0f;
		const float sc = scale_v.float_at(t);
		for (int k = 0; k < 3; k++) {
			const int v = tri[k];
			if (moved[v]) {
				continue;
			}
			moved.set(v, true);
			Vector3 offset = pos[v] - center;
			if (scale_mode == 1) { // SINGLE_AXIS
				Vector3 axis = axis_v.vec_at(t);
				if (axis.length_squared() > 0.0f) {
					axis = axis.normalized();
					const float along = offset.dot(axis);
					offset += axis * (along * (sc - 1.0f));
					pos[v] = center + offset;
					continue;
				}
			}
			pos[v] = center + offset * sc;
		}
	}

	mesh->recalculate_normals();
	p_ctx.write_geometry(p_node_idx, 0, geo);
}

// --- ExtrudeMeshNode ---

void ExtrudeMeshNode::_bind_methods() {
	ClassDB::bind_method(D_METHOD("set_mode", "m"), &ExtrudeMeshNode::set_mode);
	ClassDB::bind_method(D_METHOD("get_mode"), &ExtrudeMeshNode::get_mode);
	ADD_PROPERTY(PropertyInfo(Variant::INT, "mode"), "set_mode", "get_mode");
}

ExtrudeMeshNode::ExtrudeMeshNode() {
	// BLENDER'S FIVE. Ours was Geometry and a FLOAT "Offset" at index 1 --
	// which is where SELECTION is -- so a bridged selection drove the offset
	// distance, Blender's vector Offset landed nowhere, and Offset Scale,
	// Individual, Top and Side did not exist.
	input_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
	input_defs.push_back({ "Selection", SocketType::Bool, true });
	input_defs.push_back({ "Offset", SocketType::Vector, Vector3() });
	input_defs.push_back({ "Offset Scale", SocketType::Float, 1.0f });
	input_defs.push_back({ "Individual", SocketType::Bool, true });
	output_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
	output_defs.push_back({ "Top", SocketType::Bool, Variant() });
	output_defs.push_back({ "Side", SocketType::Bool, Variant() });
}

void ExtrudeMeshNode::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()) {
		p_ctx.write_geometry(p_node_idx, 0, geo);
		return;
	}

	geo = geo->duplicate_geometry();
	Ref<ProcMesh> mesh = geo->get_mesh();
	const int orig_verts = mesh->vertex_count();
	const int orig_tris = mesh->triangle_count();
	if (orig_verts == 0 || orig_tris == 0) {
		p_ctx.write_geometry(p_node_idx, 0, geo);
		return;
	}

	SocketValue selection = p_ctx.materialize(p_ctx.read_input(p_node_idx, 1), geo);
	// OFFSET IS AN IMPLICIT-FIELD SOCKET: unconnected it is the NORMAL, and its
	// default_value is never read. Measured -- setting the default to (1, 0, 0)
	// changes nothing in Blender, while leaving it unconnected still extrudes
	// along +Z on an XY grid. Same trap as Random Value's ID.
	const bool offset_linked = p_ctx.is_input_connected(p_node_idx, 2);
	SocketValue offset_field;
	if (offset_linked) {
		offset_field = p_ctx.materialize(p_ctx.read_input(p_node_idx, 2), geo);
	}
	const float scale = p_ctx.read_float(p_node_idx, 3);

	const bool have_normals = mesh->normals.size() == orig_verts;
	const Vector3 *pos = mesh->positions.ptr();
	const int32_t *idx = mesh->indices.ptr();

	PackedVector3Array out_pos = mesh->positions;
	PackedInt32Array out_idx;
	PackedInt32Array top_face;  // 1 per output triangle
	PackedInt32Array side_face;

	// Existing faces stay, and are neither top nor side.
	for (int t = 0; t < orig_tris; t++) {
		const bool sel = selection.bool_at(t);
		if (sel) {
			continue; // replaced by its extruded top
		}
		for (int c = 0; c < 3; c++) {
			out_idx.push_back(idx[t * 3 + c]);
		}
		top_face.push_back(0);
		side_face.push_back(0);
	}

	// INDIVIDUAL is Blender's default (true): each face extrudes on its own,
	// so its vertices are not shared with a neighbour's.
	for (int t = 0; t < orig_tris; t++) {
		if (!selection.bool_at(t)) {
			continue;
		}
		const int v0 = idx[t * 3 + 0], v1 = idx[t * 3 + 1], v2 = idx[t * 3 + 2];
		Vector3 face_n = (pos[v1] - pos[v0]).cross(pos[v2] - pos[v0]).normalized();

		int base = out_pos.size();
		for (int c = 0; c < 3; c++) {
			const int v = idx[t * 3 + c];
			Vector3 off = offset_linked
					? offset_field.vec_at(v)
					: (have_normals ? mesh->normals[v] : face_n);
			out_pos.push_back(pos[v] + off * scale);
		}
		// The top face, wound as the original.
		out_idx.push_back(base + 0);
		out_idx.push_back(base + 1);
		out_idx.push_back(base + 2);
		top_face.push_back(1);
		side_face.push_back(0);
		// Three side quads, two triangles each.
		const int bot[3] = { v0, v1, v2 };
		for (int e = 0; e < 3; e++) {
			const int b0 = bot[e], b1 = bot[(e + 1) % 3];
			const int t0 = base + e, t1 = base + ((e + 1) % 3);
			out_idx.push_back(b0);
			out_idx.push_back(b1);
			out_idx.push_back(t1);
			top_face.push_back(0);
			side_face.push_back(1);
			out_idx.push_back(b0);
			out_idx.push_back(t1);
			out_idx.push_back(t0);
			top_face.push_back(0);
			side_face.push_back(1);
		}
	}

	mesh->positions = out_pos;
	mesh->indices = out_idx;
	// EVERY PER-VERTEX ARRAY describes the OLD topology once the vertex count
	// changes, and a stale one is not merely wrong -- to_array_mesh refuses the
	// surface outright ("array.size() != p_vertex_array_len") and the bake
	// produces nothing. Dropping them follows DeleteGeometry's existing rule
	// here: wrong data is harder to notice than absent data.
	mesh->normals = PackedVector3Array();
	mesh->tangents = PackedFloat32Array();
	mesh->uvs = PackedVector2Array();
	mesh->colors = PackedColorArray();
	mesh->face_offsets = PackedInt32Array();

	p_ctx.write_geometry(p_node_idx, 0, geo);
	p_ctx.write_int_field(p_node_idx, 1, top_face, AttrDomain::Face);
	p_ctx.write_int_field(p_node_idx, 2, side_face, AttrDomain::Face);
}

// --- MergeByDistanceNode ---

void MergeByDistanceNode::_bind_methods() {
}

MergeByDistanceNode::MergeByDistanceNode() {
	// Blender's four. Distance sat at index 1, which is Selection -- so a graph
	// wiring a selection drove the merge DISTANCE.
	input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
	input_defs.push_back({ "Selection", SocketType::Bool, true });
	// Blender 5.x MENU: All or Connected. Only All is implemented.
	input_defs.push_back({ "Mode", SocketType::Int, 0 });
	input_defs.push_back({ "Distance", SocketType::Float, 0.001f });
	output_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
}

void MergeByDistanceNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
	// SOCKET 3 -- Distance moved when Selection and Mode took 1 and 2. Reading
	// the old index made the merge distance whatever the selection evaluated
	// to, which is 1.0 for the default `true`.
	float dist = p_ctx.read_float(p_node_idx, 3);
	if (p_ctx.read_int(p_node_idx, 2) != 0) {
		UtilityFunctions::push_warning(
				"MergeByDistance: only the All mode is implemented; Connected "
				"needs an edge domain, which ProcMesh has no array for.");
	}

	if (geo.is_null() || geo->get_mesh().is_null()) {
		p_ctx.write_geometry(p_node_idx, 0, geo);
		return;
	}

	geo = geo->duplicate_geometry();
	Ref<ProcMesh> mesh = geo->get_mesh();
	int count = mesh->vertex_count();
	float dist_sq = dist * dist;

	// Build vertex merge map
	PackedInt32Array merge_map;
	merge_map.resize(count);
	int32_t *mm = merge_map.ptrw();
	for (int i = 0; i < count; i++) mm[i] = i;

	const Vector3 *pos = mesh->positions.ptr();
	// O(n^2) — fine for moderate meshes
	for (int i = 0; i < count; i++) {
		if (mm[i] != i) continue; // already merged
		for (int j = i + 1; j < count; j++) {
			if (mm[j] != j) continue;
			if (pos[i].distance_squared_to(pos[j]) < dist_sq) {
				mm[j] = i;
			}
		}
	}

	// Compact: build new vertex list + remap
	PackedInt32Array new_indices_map;
	new_indices_map.resize(count);
	int32_t *nim = new_indices_map.ptrw();

	PackedVector3Array new_pos;
	PackedVector3Array new_nor;
	// EVERY array parallel to positions, not just the normals. Compacting two
	// of four left uvs and colors at the OLD length, and an ArrayMesh whose
	// arrays disagree produces no surface at all -- a merged mesh came back
	// with zero vertices rather than the one it had merged to.
	PackedVector2Array new_uv;
	PackedColorArray new_col;
	const bool has_uv = mesh->uvs.size() == count;
	const bool has_col = mesh->colors.size() == count;
	int new_count = 0;

	// THE MERGED VERTEX SITS AT THE GROUP'S CENTROID, not at its first member.
	// Measured: a 0.02 grid of 16 points merged with distance 0.05 gives
	// Blender ONE vertex at (0, 0, 0), where keeping the first would put it at
	// the corner (-0.01, -0.01, 0).
	PackedVector3Array sum_pos;
	PackedInt32Array group_size;
	sum_pos.resize(count);
	group_size.resize(count);
	for (int i = 0; i < count; i++) {
		sum_pos.ptrw()[i] = Vector3();
		group_size.ptrw()[i] = 0;
	}
	for (int i = 0; i < count; i++) {
		const int root = mm[i];
		sum_pos.ptrw()[root] += pos[i];
		group_size.ptrw()[root] += 1;
	}

	for (int i = 0; i < count; i++) {
		if (mm[i] == i) {
			nim[i] = new_count;
			new_pos.push_back(sum_pos[i] / (float)MAX(group_size[i], 1));
			if (mesh->normals.size() > i) {
				new_nor.push_back(mesh->normals[i]);
			}
			if (has_uv) {
				new_uv.push_back(mesh->uvs[i]);
			}
			if (has_col) {
				new_col.push_back(mesh->colors[i]);
			}
			new_count++;
		} else {
			nim[i] = -1; // resolved later
		}
	}

	// Resolve merged vertices
	for (int i = 0; i < count; i++) {
		if (mm[i] != i) {
			nim[i] = nim[mm[i]];
		}
	}

	// Remap indices
	int32_t *idx = mesh->indices.ptrw();
	for (int i = 0; i < mesh->indices.size(); i++) {
		idx[i] = nim[idx[i]];
	}

	// Remove degenerate triangles
	PackedInt32Array clean_idx;
	for (int i = 0; i < mesh->indices.size(); i += 3) {
		if (idx[i] != idx[i + 1] && idx[i + 1] != idx[i + 2] && idx[i] != idx[i + 2]) {
			clean_idx.push_back(idx[i]);
			clean_idx.push_back(idx[i + 1]);
			clean_idx.push_back(idx[i + 2]);
		}
	}

	mesh->positions = new_pos;
	mesh->normals = new_nor;
	mesh->uvs = new_uv;
	mesh->colors = new_col;
	mesh->indices = clean_idx;
	// Per-corner and per-face arrays describe a topology that no longer
	// exists, as in Delete Geometry.
	mesh->tangents = PackedFloat32Array();
	mesh->face_offsets = PackedInt32Array();

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

// --- DualMeshNode ---

void DualMeshNode::_bind_methods() {
}

DualMeshNode::DualMeshNode() {
	input_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
	// Accepted and not yet acted on: keeping the boundary needs an edge domain
	// to find the boundary WITH, and ProcMesh has no edge array.
	input_defs.push_back({ "Keep Boundaries", SocketType::Bool, false });
	output_defs.push_back({ "Dual Mesh", SocketType::Geometry, Variant() });
}

void DualMeshNode::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()) {
		p_ctx.write_geometry(p_node_idx, 0, geo);
		return;
	}

	geo = geo->duplicate_geometry();
	Ref<ProcMesh> mesh = geo->get_mesh();
	int tri_count = mesh->triangle_count();
	if (tri_count == 0) {
		p_ctx.write_geometry(p_node_idx, 0, geo);
		return;
	}

	// Dual mesh: each triangle becomes a vertex (at centroid)
	// Each original vertex becomes a face connecting its surrounding triangle centroids
	const Vector3 *pos = mesh->positions.ptr();
	const int32_t *idx = mesh->indices.ptr();

	// Compute centroids
	PackedVector3Array centroids;
	centroids.resize(tri_count);
	Vector3 *cw = centroids.ptrw();
	for (int t = 0; t < tri_count; t++) {
		cw[t] = (pos[idx[t * 3]] + pos[idx[t * 3 + 1]] + pos[idx[t * 3 + 2]]) / 3.0f;
	}

	// Build adjacency: for each vertex, which triangles use it
	int vert_count = mesh->vertex_count();
	Vector<Vector<int>> vert_tris;
	vert_tris.resize(vert_count);
	for (int t = 0; t < tri_count; t++) {
		vert_tris.ptrw()[idx[t * 3]].push_back(t);
		vert_tris.ptrw()[idx[t * 3 + 1]].push_back(t);
		vert_tris.ptrw()[idx[t * 3 + 2]].push_back(t);
	}

	// Output: centroids are new vertices, each original vertex becomes fan of triangles
	PackedVector3Array dual_pos = centroids;
	PackedInt32Array dual_idx;

	for (int v = 0; v < vert_count; v++) {
		const Vector<int> &tris = vert_tris[v];
		if (tris.size() < 3) continue;
		// Fan triangulate around the vertex
		for (int i = 1; i < tris.size() - 1; i++) {
			dual_idx.push_back(tris[0]);
			dual_idx.push_back(tris[i]);
			dual_idx.push_back(tris[i + 1]);
		}
	}

	mesh->positions = dual_pos;
	mesh->indices = dual_idx;
	mesh->normals.clear();
	mesh->uvs.clear();
	mesh->recalculate_normals();

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

// --- DuplicateElementsNode ---

void DuplicateElementsNode::_bind_methods() {
}

DuplicateElementsNode::DuplicateElementsNode() {
	// Blender's order: Geometry, SELECTION, Amount -- and a second output.
	// Ours had Amount at index 1, so a graph wiring a selection set the count.
	input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
	input_defs.push_back({ "Selection", SocketType::Bool, true });
	input_defs.push_back({ "Amount", SocketType::Int, 1 });
	output_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
	// Which copy an element came from, which is how a graph tells the copies
	// apart at all.
	output_defs.push_back({ "Duplicate Index", SocketType::Int, Variant() });
}

void DuplicateElementsNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
	// SOCKET 2 -- Amount moved when Selection took socket 1.
	int amount = CLAMP(p_ctx.read_int(p_node_idx, 2), 0, 100);

	if (geo.is_null() || geo->get_mesh().is_null() || amount <= 0) {
		p_ctx.write_geometry(p_node_idx, 0, geo);
		return;
	}

	// Duplicate the mesh N times using the Join pattern (offset indices).
	Ref<ProcGeometry> result = geo->duplicate_geometry();
	Ref<ProcMesh> src_mesh = geo->get_mesh();

	int src_vert_count = src_mesh->vertex_count();
	int src_idx_count = src_mesh->indices.size();

	if (src_vert_count == 0) {
		p_ctx.write_geometry(p_node_idx, 0, result);
		return;
	}

	Ref<ProcMesh> dst_mesh = result->get_mesh();

	// Pre-allocate for total copies (original + amount duplicates)
	int total_copies = amount + 1;
	int total_verts = src_vert_count * total_copies;
	int total_indices = src_idx_count * total_copies;

	PackedVector3Array new_pos;
	PackedVector3Array new_nor;
	PackedVector2Array new_uvs;
	PackedInt32Array new_idx;

	new_pos.resize(total_verts);
	new_idx.resize(total_indices);

	bool has_normals = src_mesh->normals.size() == src_vert_count;
	bool has_uvs = src_mesh->uvs.size() == src_vert_count;
	if (has_normals) new_nor.resize(total_verts);
	if (has_uvs) new_uvs.resize(total_verts);

	const Vector3 *sp = src_mesh->positions.ptr();
	const Vector3 *sn = has_normals ? src_mesh->normals.ptr() : nullptr;
	const Vector2 *su = has_uvs ? src_mesh->uvs.ptr() : nullptr;
	const int32_t *si = src_mesh->indices.ptr();

	Vector3 *dp = new_pos.ptrw();
	Vector3 *dn = has_normals ? new_nor.ptrw() : nullptr;
	Vector2 *du = has_uvs ? new_uvs.ptrw() : nullptr;
	int32_t *di = new_idx.ptrw();

	for (int copy = 0; copy < total_copies; copy++) {
		int vert_offset = copy * src_vert_count;
		int idx_offset = copy * src_idx_count;

		for (int v = 0; v < src_vert_count; v++) {
			dp[vert_offset + v] = sp[v];
			if (dn) dn[vert_offset + v] = sn[v];
			if (du) du[vert_offset + v] = su[v];
		}

		for (int i = 0; i < src_idx_count; i++) {
			di[idx_offset + i] = si[i] + vert_offset;
		}
	}

	dst_mesh->positions = new_pos;
	dst_mesh->normals = new_nor;
	dst_mesh->uvs = new_uvs;
	dst_mesh->indices = new_idx;

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

// --- MeshBooleanNode ---

void MeshBooleanNode::_bind_methods() {
	ClassDB::bind_method(D_METHOD("set_operation", "o"), &MeshBooleanNode::set_operation);
	ClassDB::bind_method(D_METHOD("get_operation"), &MeshBooleanNode::get_operation);
	ADD_PROPERTY(PropertyInfo(Variant::INT, "operation"), "set_operation", "get_operation");
}

MeshBooleanNode::MeshBooleanNode() {
	// Blender's shape: Mesh 1, then a MULTI-INPUT socket taking any number of
	// operands, then the two flags. Ours was "A" and "B", which matched neither
	// by name nor by count -- a third operand had nowhere to go.
	input_defs.push_back({ "Mesh 1", SocketType::Geometry, Variant() });
	SocketDef operands;
	operands.name = "Mesh 2";
	operands.type = SocketType::Geometry;
	operands.multi = true;
	input_defs.push_back(operands);
	input_defs.push_back({ "Self Intersection", SocketType::Bool, false });
	input_defs.push_back({ "Hole Tolerant", SocketType::Bool, false });
	output_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
	output_defs.push_back({ "Intersecting Edges", SocketType::Bool, Variant() });
}

void MeshBooleanNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	// THE BOOLEAN ITSELF IS STILL NOT IMPLEMENTED, and this says so out loud
	// rather than returning something that looks like a result.
	//
	// A real CSG needs exact predicates and a robust intersection pass;
	// Blender's uses one. Joining the operands would LOOK right for disjoint
	// meshes and be silently wrong for the overlapping ones the node exists
	// for, which is the plausible-wrong-answer this project keeps finding. The
	// sockets are Blender's now so a graph crosses intact and nothing is
	// mis-wired; the geometry waits for the algorithm.
	Ref<ProcGeometry> a = p_ctx.read_geometry(p_node_idx, 0);
	const int operands = p_ctx.graph ? p_ctx.graph->get_input_link_count(p_node_idx, 1) : 0;
	if (operands > 0) {
		UtilityFunctions::push_warning(
				"MeshBoolean: no CSG implementation -- returning Mesh 1 unchanged. ",
				operands, " operand(s) ignored, operation ", operation, ".");
	}
	if (a.is_valid()) {
		a = a->duplicate_geometry();
	}
	p_ctx.write_geometry(p_node_idx, 0, a);
	// No intersection is computed, so nothing is an intersecting edge.
	p_ctx.write_bool(p_node_idx, 1, false);
}

// --- SplitEdgesNode ---

void SplitEdgesNode::_bind_methods() {
}

SplitEdgesNode::SplitEdgesNode() {
	input_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
	input_defs.push_back({ "Selection", SocketType::Bool, true });
	output_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
}

void SplitEdgesNode::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()) {
		p_ctx.write_geometry(p_node_idx, 0, geo);
		return;
	}
	geo = geo->duplicate_geometry();
	Ref<ProcMesh> mesh = geo->get_mesh();
	if (mesh->triangle_count() == 0) {
		p_ctx.write_geometry(p_node_idx, 0, geo);
		return;
	}

	// IT WAS A PASS-THROUGH because there was no edge domain to select on.
	// There is one now, so the Selection field is read per EDGE and the node
	// does the work: a split edge stops being shared, so the faces either side
	// get their own copies of its vertices.
	const PackedInt32Array edges = mesh->compute_edges();
	const int n_edges = edges.size() / 2;
	SocketValue selection = p_ctx.materialize(p_ctx.read_input(p_node_idx, 1), geo);

	// Which undirected pairs are split.
	HashSet<int64_t> split;
	for (int e = 0; e < n_edges; e++) {
		if (selection.bool_at(e)) {
			split.insert(((int64_t)edges[e * 2] << 32) | (uint32_t)edges[e * 2 + 1]);
		}
	}
	if (split.is_empty()) {
		p_ctx.write_geometry(p_node_idx, 0, geo);
		return;
	}

	PackedVector3Array out_pos = mesh->positions;
	PackedInt32Array out_idx = mesh->indices;
	const int tris = mesh->triangle_count();
	const int32_t *src = mesh->indices.ptr();
	int32_t *dst = out_idx.ptrw();

	// EACH ORIGINAL VERTEX IS CLAIMED BY THE FIRST FACE THAT SEPARATES AT IT;
	// later faces get copies. Duplicating for every face instead leaves the
	// originals unreferenced -- which is how the first version produced 72
	// vertices where Blender gives 60: 12 orphans nothing pointed at.
	PackedInt32Array claimed;
	claimed.resize(mesh->positions.size());
	for (int i = 0; i < claimed.size(); i++) {
		claimed.set(i, -1);
	}

	auto is_split = [&](int a, int b) {
		const int lo = MIN(a, b), hi = MAX(a, b);
		return split.has(((int64_t)lo << 32) | (uint32_t)hi);
	};

	for (int t = 0; t < tris; t++) {
		for (int k = 0; k < 3; k++) {
			const int v = src[t * 3 + k];
			const int prev = src[t * 3 + (k + 2) % 3];
			const int next = src[t * 3 + (k + 1) % 3];
			// A vertex separates for this face only if an edge of THIS face
			// meeting it is split.
			if (!is_split(v, next) && !is_split(prev, v)) {
				continue;
			}
			if (claimed[v] == -1) {
				claimed.set(v, t); // this face keeps the original
				continue;
			}
			if (claimed[v] == t) {
				continue;
			}
			out_pos.push_back(mesh->positions[v]);
			dst[t * 3 + k] = out_pos.size() - 1;
		}
	}

	mesh->positions = out_pos;
	mesh->indices = out_idx;
	// Per-vertex arrays describe the old vertex count once it grows.
	mesh->normals = PackedVector3Array();
	mesh->tangents = PackedFloat32Array();
	mesh->uvs = PackedVector2Array();
	mesh->colors = PackedColorArray();
	mesh->face_offsets = PackedInt32Array();

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

// --- TriangulateNode ---

void TriangulateNode::_bind_methods() {
}

TriangulateNode::TriangulateNode() {
	// Blender's four. Every proc-nodes mesh is already triangles, so this is a
	// pass-through whatever the methods say -- the sockets exist so a Blender
	// graph's links land instead of being dropped.
	input_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
	input_defs.push_back({ "Selection", SocketType::Bool, true });
	input_defs.push_back({ "Quad Method", SocketType::Int, 0 });
	input_defs.push_back({ "N-gon Method", SocketType::Int, 0 });
	output_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
}

void TriangulateNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
	// All meshes in proc-nodes are already triangulated, so pass through.
	p_ctx.write_geometry(p_node_idx, 0, geo);
}

// --- EdgePathsToCurvesNode ---

void EdgePathsToCurvesNode::_bind_methods() {
}

EdgePathsToCurvesNode::EdgePathsToCurvesNode() {
	// Blender's three. Both field inputs are accepted and not yet acted on:
	// walking an edge path needs an edge domain, and ProcMesh has no edge
	// array -- declared so the links land instead of being dropped.
	input_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
	input_defs.push_back({ "Start Vertices", SocketType::Bool, true });
	input_defs.push_back({ "Next Vertex Index", SocketType::Int, -1 });
	output_defs.push_back({ "Curves", SocketType::Geometry, Variant() });
}

void EdgePathsToCurvesNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
	// Placeholder: pass through.
	// Full implementation would extract boundary/feature edges and convert to ProcCurves.
	p_ctx.write_geometry(p_node_idx, 0, geo);
}

} // namespace godot