Skip to content

feat(quads): loop cut on quad meshes - #341

Merged
fernandotonon merged 1 commit into
feat/quadsfrom
feat/loop-cut
Apr 29, 2026
Merged

feat(quads): loop cut on quad meshes#341
fernandotonon merged 1 commit into
feat/quadsfrom
feat/loop-cut

Conversation

@fernandotonon

Copy link
Copy Markdown
Owner

Summary

Implements loop cut — the original target that started the quad-migration epic. Walks the perpendicular ring of quads adjacent to a selected edge and bisects each one with a midpoint chain.

HalfEdgeMesh::loopCut(startEdgeIdx)

  • Starts at the two faces adjacent to startEdgeIdx.
  • In each quad, finds the opposite edge (two positions away in the loop) and crosses through it into the next face.
  • Stops on: closed loop (back to start), boundary edge, or non-quad face.
  • Walks both directions from the start so an interior edge of an open mesh produces cuts on both sides.
  • Materialisation phase: vertex-pair-keyed midpoint cache (shared between consecutive steps), splitFace bisects each step's face.

EditModeController::loopCutSelection()

Edge-mode only; uses first selected edge. Pushes "Loop Cut" undo command.

UI

  • Toolbar button (‖ double vertical line) next to Fill.
  • Ctrl+R shortcut — disambiguates from R=Scale (Unity convention). Falls through to Scale when loop cut returns 0, so Ctrl+R is harmless outside Edit/Edge mode.

Quad-only behaviour

Loop cut is geometrically a quad operation — triangles have no opposite-edge correspondence, so there's no well-defined "loop". MVP returns empty on triangle adjacency, matching Blender's behaviour. A tri-pair fallback (treat coplanar triangle pairs as quads for the walk) could be added later if needed.

Test plan

  • 244 standalone tests pass
  • LoopCutOnQuadStripCutsEachQuadOnce — 3-quad strip → 4 rail midpoints, 6 quads after cut, no fan diagonals.
  • LoopCutClosedRingOnQuadCubeReturnsToStart — quad cube → 4 rail midpoints (closed ring), 10 quads (6 + 4 cuts).
  • LoopCutFailsOnNonQuadAdjacency — triangle pair → empty.
  • LoopCutFailsOnInvalidEdgeIndex — -1 / out-of-range → empty.
  • Smoke: FBX quad asset — Ctrl+R on an interior edge produces a perpendicular cut chain through the quad ring.
  • Smoke: triangle cube — Ctrl+R is a no-op (expected).

Known follow-ups

  • Triangle-pair fallback (treat tri+tri sharing a diagonal as a logical quad).
  • Slide / t parameter to position the cut along the loop instead of always at midpoint.
  • Multi-cut loops (insert N parallel cuts in one op).

Loop cut walks the perpendicular ring of quads adjacent to a
selected edge, bisecting each one with a new midpoint chain. Quad-
only operation — that's the geometric definition: triangles have
no opposite-edge correspondence, so a "loop" through tri-tri pairs
is ambiguous and not implemented in this MVP. Matches Blender's
behaviour (loop cut on a triangulated mesh is also a no-op there).

HalfEdgeMesh::loopCut(startEdgeIdx)
  Walk plan:
    - Start: the two faces adjacent to startEdgeIdx.
    - Each step: in the current quad, find the opposite edge (the
      side two positions away in the loop) and record the rail
      pair (entry edge + opposite). Cross opposite into the next
      face.
    - Stop on: closed loop (back to start edge), boundary edge
      (no second face), non-quad face (no opposite-edge
      correspondence).

  Walk both directions from the start edge so an interior edge of
  an open mesh produces cuts on both sides. visitedFaces guards
  against re-entry once a closed ring closes.

  Materialisation: collect rails up-front (vertex-pair-keyed since
  edge indices shift across splitEdge). Per step, ensure midpoint
  on each rail (shared between consecutive steps via map cache),
  then splitFace bisects the now-hexagonal face along the new
  diagonal. Returns the new midpoint vertex indices in walk order.

EditModeController::loopCutSelection()
  Edge-mode only. Uses the FIRST selected edge as start (multi-
  edge loop cuts aren't in MVP scope — each cut is independent).
  Pushes one undo command labeled "Loop Cut".

UI
  Toolbar button (‖ double vertical line) next to Fill, with
  Ctrl+R shortcut. The Ctrl modifier disambiguates from R = Scale
  mode (Unity convention). Falls through to Scale when the loop
  cut returns 0 (e.g. nothing selected, non-quad neighborhood),
  so Ctrl+R remains harmless outside the loop-cut context.

Tests
  - LoopCutOnQuadStripCutsEachQuadOnce: 3-quad strip → 4 rail
    midpoints, 6 quads after cut.
  - LoopCutClosedRingOnQuadCubeReturnsToStart: quad cube → 4
    rail midpoints (closed ring), 10 quads (6 + 4 cuts).
  - LoopCutFailsOnNonQuadAdjacency: triangle pair → empty.
  - LoopCutFailsOnInvalidEdgeIndex: -1 / out-of-range → empty.
  - 244 standalone tests pass.
@coderabbitai

coderabbitai Bot commented Apr 29, 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: 582cd513-a57b-4d56-a446-31d508aaf8da

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/loop-cut

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: 5292e164ba

ℹ️ 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/mainwindow.cpp
Comment on lines 1379 to 1380
SentryReporter::addBreadcrumb("ui.shortcut", "R — Scale mode");
setTransformState(TransformOperator::TS_SCALE);

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 Prevent Ctrl+R fallback from switching to Scale

When Ctrl+R is pressed in edge edit mode and loopCutSelection() returns 0 (for example on triangle/non-quad adjacency), execution falls through to the default R handler and changes the tool to Scale. This makes a failed loop-cut attempt perform a different action instead of a no-op, which is surprising and can interrupt the current edit workflow; the Ctrl-modified path should be consumed once attempted.

Useful? React with 👍 / 👎.

Comment thread src/HalfEdgeMesh.cpp
Comment on lines +4937 to +4941
const auto [f1, f2] = edgeFaces(startEdgeIdx);
walkDirection(f1, startA, startB);
walkDirection(f2, startA, startB);

if (walk.empty()) return newVertices;

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 Enforce quad adjacency precondition before loop walk

The function documentation says the start edge must have two quad-adjacent faces or return empty, but the implementation walks both sides independently and only aborts when walk is fully empty. That means an edge with one quad side and one non-quad side still produces a partial cut (mutating topology) instead of failing, which violates the stated contract and causes unexpected edits near mixed tri/quad regions.

Useful? React with 👍 / 👎.

@sonarqubecloud

Copy link
Copy Markdown

@fernandotonon
fernandotonon merged commit bf750f3 into feat/quads Apr 29, 2026
19 checks passed
@fernandotonon
fernandotonon deleted the feat/loop-cut branch April 29, 2026 14:49
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