Skip to content

quads chunk 1: EditableFace foundation - #327

Merged
fernandotonon merged 1 commit into
feat/quadsfrom
feat/quads-1-foundation
Apr 28, 2026
Merged

quads chunk 1: EditableFace foundation#327
fernandotonon merged 1 commit into
feat/quadsfrom
feat/quads-1-foundation

Conversation

@fernandotonon

Copy link
Copy Markdown
Owner

Summary

First chunk of the quad migration tracked by #326. Adds the n-gon data model (EditableFace) and the round-trip plumbing through HalfEdgeMesh. No user-visible behavior change — every existing path still uses triangles.

Changes

  • EditableFace struct (n-vertex polygon) with isValid() guard.
  • EditableSubMesh gains a faces field with a clear canonical-vs-mirror invariant: when faces is non-empty it is canonical and triangles is the fan-triangulation; when empty, triangles is canonical (legacy mode).
  • Helpers triangulateFaces() and promoteTrianglesToFaces() that maintain the invariant.
  • HalfEdgeMesh::buildFromEditableMesh prefers faces when non-empty, falls back to triangles; toEditableMesh writes both representations and only populates faces if the HE mesh actually contains n-gons (so legacy callers see no diff).
  • validate() relaxed from "exactly 3 half-edges per face" to "at least 3".

Tests

+11 HalfEdgeMesh standalone tests covering:

  • Quad EditableFace produces a single 4-valence HE face.
  • Round-trip preserves quad in faces + emits 2 fan triangles in triangles.
  • Triangle-only meshes leave faces empty.
  • Mixed tri+quad submesh handling.
  • isValid() guard cases.
  • Out-of-range index silently skipped.
  • triangulateFaces / promoteTrianglesToFaces correctness.

Test plan

  • 173 HalfEdgeMesh tests green (was 162; +11 new).
  • App builds clean.
  • Linux CI runs the full suite + sonar.

Targeting feat/quads

Per the rollout plan in #326, chunks land on the long-lived feat/quads branch and only merge to master once the whole migration is working. This PR is chunk 1 of 7.

Closes nothing yet; tracks #326.

🤖 Generated with Claude Code

@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: 3b66297c-b8be-4712-9eef-d6a749af6b13

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-1-foundation

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.

@fernandotonon
fernandotonon force-pushed the feat/quads-1-foundation branch from 76b13a3 to ff5cc8f Compare April 28, 2026 06:40

@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: 76b13a3514

ℹ️ 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
Comment on lines +108 to +109
if (!sub.faces.empty()) {
for (const auto& face : sub.faces) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fall back to triangles when canonical faces are stale

This branch unconditionally ignores sub.triangles whenever sub.faces is non-empty, but existing mesh mutators like EditableMesh::weldByPosition and collapseToSingleSubmeshAndWeld still rewrite only triangles. After a quad submesh goes through one of those paths, faces can contain pre-edit indices while triangles contain post-edit indices; rebuilding from this code then replays stale geometry (or skips faces as out-of-range), silently dropping the edit. A guarded fallback to triangles when a face stream is invalid/stale would avoid this data loss.

Useful? React with 👍 / 👎.

Comment thread src/EditableMesh.cpp
Comment on lines +52 to +56
for (size_t i = 1; i + 1 < idx.size(); ++i) {
EditableTriangle t;
t.indices[0] = idx[0];
t.indices[1] = idx[i];
t.indices[2] = idx[i + 1];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject out-of-range face indices during triangulation

triangulateFaces emits triangles directly from face.indices without checking that each index is < sub.vertices.size(). If a caller passes a face with an out-of-range index, this function mirrors invalid indices into sub.triangles (the legacy path used by buffer upload), which can produce invalid index buffers and undefined rendering behavior. buildFromEditableMesh already treats these faces as invalid, so this helper should enforce the same bound check before emitting triangles.

Useful? React with 👍 / 👎.

Introduces the n-gon (quad-aware) data model that subsequent chunks
will wire through GPU upload, importers, topology ops, and exporters.
No behavior change for triangle-only meshes — every existing test
still passes.

Data model:
  - New EditableFace struct (n-vertex polygon) with isValid() guard.
  - EditableSubMesh gains a `faces` field alongside the existing
    `triangles`. Invariant: when `faces` is non-empty it is the
    canonical face storage and `triangles` mirrors it as a
    fan-triangulation; when `faces` is empty, `triangles` is canonical
    (legacy triangle-only mode).
  - Free helpers `triangulateFaces(sub)` and `promoteTrianglesToFaces(sub)`
    keep the two representations in sync. Documented with the same
    fan-triangulation rule HalfEdgeMesh::appendFace uses, so HE
    round-trips don't change face shape.

HalfEdgeMesh:
  - buildFromEditableMesh prefers `faces` when non-empty; falls back to
    `triangles` for legacy submeshes. Out-of-range and duplicate-vertex
    inputs are silently skipped.
  - toEditableMesh writes any HE n-gon face into both `faces` and a
    fan-triangulated `triangles`. Submeshes that turn out all-triangle
    leave `faces` empty so legacy consumers see no diff.
  - validate() relaxed from "exactly 3 half-edges per face" to "at
    least 3"; n-gons are now first-class.

Tests (+11):
  - Quad EditableFace round-trips as a single 4-valence HE face.
  - toEditableMesh preserves quad in `faces` and emits 2 fan triangles
    in `triangles`.
  - Triangle-only meshes leave `faces` empty (legacy invariant).
  - Mixed tri+quad submesh handling.
  - EditableFace::isValid guard cases.
  - Out-of-range index in EditableFace silently skipped.
  - triangulateFaces / promoteTrianglesToFaces unit coverage.

Towards #326.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@fernandotonon
fernandotonon force-pushed the feat/quads-1-foundation branch from ff5cc8f to ba49a76 Compare April 28, 2026 06:48
@sonarqubecloud

Copy link
Copy Markdown

@fernandotonon
fernandotonon merged commit 9959832 into feat/quads Apr 28, 2026
35 checks passed
@fernandotonon
fernandotonon deleted the feat/quads-1-foundation branch April 28, 2026 14:29
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