Skip to content

quads chunk 5a: Catmull-Clark subdivide - #333

Merged
fernandotonon merged 2 commits into
feat/quadsfrom
feat/quads-5a-catmull-clark
Apr 28, 2026
Merged

quads chunk 5a: Catmull-Clark subdivide#333
fernandotonon merged 2 commits into
feat/quadsfrom
feat/quads-5a-catmull-clark

Conversation

@fernandotonon

Copy link
Copy Markdown
Owner

Summary

Chunk 5a of the quad migration (#326). Adds the classic Catmull-Clark subdivision-surface op on the half-edge mesh and wires it through the toolbar as a second Subdivide menu item.

Stacked on chunk 4 (#332). When chunk 4 lands on feat/quads, this PR's base auto-updates.

What it does

  • HalfEdgeMesh::subdivideCatmullClark() — one full CC step on the whole mesh. Always emits quads regardless of input topology (triangle → 3 quads, quad → 4 quads, N-gon → N quads). Geometry is smoothed via the standard rule (face points / edge points / smoothed vertex positions). Cross-submesh edges are treated as boundaries so material seams stay sharp.
  • EditModeController::subdivideCatmullClarkAll() — wraps it with the standard pre-cancel-bevel/knife + undo-command + re-pack pipeline. Selection clears after the op.
  • The Subdivide toolbar button becomes a dropdown: Standard (existing 1-to-4 triangle split, selected faces/edges) and Catmull-Clark (whole mesh, smoothed quads).

Tests (+7 standalone)

  • Single quad → 4 sub-quads.
  • Single triangle → 3 sub-quads.
  • Closed cube → 36 quads, no boundary edges (manifold preserved).
  • 2x2 quad grid → 16 sub-quads.
  • Planar quad face point lands at the arithmetic centre.
  • Empty mesh is a no-op.
  • Round-trip through EditableMesh preserves the all-quad output.

204 standalone tests pass (was 197; +7).

Test plan

  • App builds clean.
  • 204 standalone tests green on macOS.
  • Linux/macOS/Windows CI.
  • Manual smoke: import a quad asset, Tab into Edit Mode, click Subdivide → Catmull-Clark. Mesh should subdivide once with smoothed quads.

Deferred to follow-up chunks

  • Selective Catmull-Clark (subdivide only selected faces, with proper boundary blending into unselected neighbours).
  • Multi-step iteration UI (currently each click is one step).
  • Quad-preserving extrude/bevel/dissolve (chunks 5b–5d in the plan).

🤖 Generated with Claude Code

Adds the classic Catmull-Clark subdivision-surface operation on the
half-edge mesh. Always produces an all-quad output regardless of
input topology — a triangle becomes 3 quads, a quad becomes 4, an
N-gon becomes N. Output geometry approaches a C¹-continuous limit on
closed manifolds via the standard rule:

  - Face point Fp = average of corner positions.
  - Edge point Ep = (a + b + Fp1 + Fp2) / 4 on interior edges,
    (a + b) / 2 on boundary or cross-submesh edges.
  - Smoothed vertex V' = (F + 2R + (n-3) V) / n on interior vertices,
    chord rule on boundary vertices. F = avg of adjacent face points,
    R = avg of adjacent edge midpoints (NOT edge points — spec).

Cross-submesh edges are treated as boundaries so material seams stay
sharp through the subdivision. Bone weights on smoothed vertices keep
the original assignment (the smoothed position is still mostly "near
V"); other attributes (UV, normal, color, tangent) blend with the
same weights as positions.

UI:
  - The Subdivide toolbar button becomes a dropdown:
    1. Standard — the existing 1-to-4 triangle split on selected
       faces / edges. Unchanged behaviour.
    2. Catmull-Clark — whole-mesh subdivide-surface step. Selection
       is cleared after the op (partial-CC with selection
       preservation needs a more sophisticated boundary blend that
       isn't in this MVP).
  - Button is enabled whenever in edit mode; menu items self-gate.

Tests (+7 standalone):
  - 1 quad → 4 sub-quads.
  - 1 triangle → 3 sub-quads.
  - Closed cube stays closed (12 → 36 quads, no boundary edges).
  - 2x2 quad grid → 16 sub-quads.
  - Planar quad face point lands at arithmetic centre.
  - Empty mesh is a no-op.
  - Round-trip through EditableMesh preserves the all-quad output.

204 standalone tests pass (was 197; +7).

Towards #326.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 28, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5a3e9313-bc5b-4078-877d-c0737aeeeefe

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/quads-5a-catmull-clark

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7a61fcd85b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/HalfEdgeMesh.cpp Outdated
Comment on lines +5081 to +5084
for (int e = 0; e < origEdgeCount; ++e) {
if (m_edges[e].halfEdge < 0) continue;
const auto [a, b] = edgeVertices(e);
if ((a == va && b == vb) || (a == vb && b == va)) return e;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Replace quadratic edge lookup in Catmull-Clark face rebuild

findEdgeIdxByVerts does a full linear scan over origEdgeCount every time a face side is processed, and it is called for every side of every face during subdivision. On typical quad meshes this turns the rebuild phase into roughly O(F*E) (effectively O(F²)), which will make whole-mesh Catmull-Clark stall or hang on larger assets. Pre-indexing original edges once (e.g., undirected (min(v0,v1), max(v0,v1)) -> edgeIdx) would keep this step linear in mesh size.

Useful? React with 👍 / 👎.

`findEdgeIdxByVerts` was a linear scan over `m_edges` (size = origEdgeCount),
called for every side of every face during the rebuild step of Catmull-Clark
subdivision. On typical quad meshes this turned the rebuild phase into roughly
O(F·E) (effectively O(F²)) and would stall on larger assets.

Fix: pre-build an undirected (min(va,vb), max(va,vb)) → edgeIdx hash inside
the existing edge-walk loop, then look up by vertex pair in O(1). The hash is
populated only for live edges (`halfEdge >= 0`) with valid vertex indices, so
behaviour matches the previous lambda exactly.

All 7 Catmull-Clark standalone tests still pass.
@sonarqubecloud

Copy link
Copy Markdown

Base automatically changed from feat/quads-4-edit-mode-ngon to feat/quads April 28, 2026 23:34
@fernandotonon
fernandotonon merged commit 0533370 into feat/quads Apr 28, 2026
35 checks passed
@fernandotonon
fernandotonon deleted the feat/quads-5a-catmull-clark branch April 28, 2026 23:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant