Skip to content

feat(bevel): carve concave/convex profile into adjacent faces - #301

Merged
fernandotonon merged 2 commits into
masterfrom
feat/bevel-concave-on-adjacent-faces
Apr 23, 2026
Merged

feat(bevel): carve concave/convex profile into adjacent faces#301
fernandotonon merged 2 commits into
masterfrom
feat/bevel-concave-on-adjacent-faces

Conversation

@fernandotonon

@fernandotonon fernandotonon commented Apr 23, 2026

Copy link
Copy Markdown
Owner

Summary

  • The profile graph now visibly carves into the faces adjacent to the beveled edge — not just the chamfer strip — so concave/convex profiles look right from any angle.
  • Flat chamfer behavior is unchanged; all existing bevel tests still pass.

Details

Previously the chamfer strip showed the profile curve correctly, but the two faces perpendicular to the beveled edge kept a straight diagonal cut. That's because Phase 5 neighbor retriangulation ran before the chain intermediates were built, and its fan-from-vertex triangulation covered the cut region with a straight spoke that hid the arc.

This PR:

  • Moves chain construction ahead of Phase 5 so both retriangulation paths can see the intermediates.
  • Splices intermediates into the neighbor-face polygon in both code paths (group-merge + single-face).
  • Adds a generic ear-clipping helper (triangulatePolygon) so the resulting concave polygons triangulate without artificial diagonals.
  • Matches insertion direction on each side so chain edges pair up opposite to the strip's emissions (manifold).
  • Skips the endpoint corner cap (buildCorner) when the profile is shaped, since the flat cap would otherwise mask the arc.

Tests

4 new unit tests verify:

  • Adjacent faces reference chain intermediates for concave profiles.
  • Same for convex profiles.
  • Original corner vertex is unreferenced when shaped.
  • Flat chamfer remains manifold (regression guard).

Test plan

  • UnitTests --gtest_filter="*Bevel*" — 39 bevel tests pass.
  • Interactive verification: bevel a cube's corner edge, raise segments, drag profile concave → concave arcs visible on both front and back faces.
  • CI: Linux / macOS / Windows builds, unit-tests-linux, SonarCloud, CodeRabbit.

Summary by CodeRabbit

  • Chores

    • Version bump to 2.28.2
  • Bug Fixes

    • Improved triangulation robustness for chamfered concave polygons
    • Fixed chamfer cap behavior for shaped profiles to prevent overlap artifacts
  • Tests

    • Added regression tests for shaped profile bevel operations

Previously the profile curve was only visible on the chamfer strip
itself. The faces perpendicular to the beveled edge (the "adjacent"
faces whose corners the bevel cuts) kept a straight diagonal cut,
hiding the profile.

This commit splices the chain intermediates into the adjacent face's
triangulation so the cut polyline follows the chamfer curve. The
approach:

- Chain building moves before Phase 5 so the neighbor-face retriangu-
  lation can reference the intermediates.
- In the group-merge path of processRingNeighbors, insert the chain
  interior between uRight and the implicit close-back-to-uLeft.
- In processNeighborFace's "both on-edge offsets" branch, do the same
  for single-face groups (the asymmetric v=v2 case on cubes).
- Since the spliced polygon is concave, fan-from-[0] would emit a
  straight diagonal across the arc. Replaced with a generic ear-clipping
  helper (triangulatePolygon) that projects into the face's plane,
  ensures CCW orientation, and runs O(n²) ear-clip. Winding is restored
  to the caller's original orientation.
- Direction of chain insertion on each side is chosen so its chain
  edges are emitted opposite to the strip's — yielding manifold.
- The endpoint corner cap (buildCorner) is skipped when the profile is
  shaped, since the cap's flat triangle would mask the arc. The
  original corner vertex (v1 / v2) becomes unreferenced in this case,
  which is harmless.

Tests: 4 new cases covering adjacent-face referencing of intermediates
(concave + convex), manifold flat-bevel regression guard, and cap-
skip verification for shaped profiles. All 39 bevel tests pass.

Version bump 2.28.1 → 2.28.2.

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

coderabbitai Bot commented Apr 23, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@fernandotonon has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 30 minutes and 38 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 30 minutes and 38 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 23ebebff-f9a8-450c-ad45-294c1236e780

📥 Commits

Reviewing files that changed from the base of the PR and between 6c120cc and 76683d0.

📒 Files selected for processing (2)
  • src/HalfEdgeMesh.cpp
  • src/HalfEdgeMesh_test.cpp
📝 Walkthrough

Walkthrough

The pull request updates the QtMeshEditor project version and implements robust polygon triangulation for mesh beveling operations, replacing fan triangulation with ear-clipping to handle concave polygons. Adds shape detection, chain intermediate generation, and new regression test coverage.

Changes

Cohort / File(s) Summary
Version Metadata
CMakeLists.txt
Bumped project version from 2.28.1 to 2.28.2.
Beveling Triangulation
src/HalfEdgeMesh.cpp
Implemented ear-clipping triangulatePolygon helper for arbitrary simple polygons; added shape detection and chain intermediates generation; updated neighbor-face retriangulation to splice intermediates and use ear-clipping instead of fan triangulation; adjusted corner cap behavior for shaped profiles; refactored Phase 6 to reuse earlier chain-building logic.
Test Coverage
src/HalfEdgeMesh_test.cpp
Added regression tests validating shaped chamfer behavior on welded cubes, verifying chain intermediate vertices are referenced in triangulation, confirming cap triangles don't reference original endpoint vertices, and asserting flat chamfer remains manifold.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Chamfered edges carved with care,
Concave paths no longer snare,
Ears do clip with graceful art,
Polygons triangulated smart! ✨✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main objective: adding concave/convex profile carving into adjacent faces for beveled edges.
Description check ✅ Passed The description covers the required template sections with comprehensive details: a high-level summary, technical implementation details organized logically, features clearly distinguished, and a test plan with checkboxes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/bevel-concave-on-adjacent-faces

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: 6c120cc9bb

ℹ️ 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 +2374 to +2375
const bool shaped = isV1 ? v1Shaped : v2Shaped;
if (shaped) return;

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 Preserve endpoint cap when shaped profile has no splice cover

This return drops the only fallback cap in the polygon.size() == 2 path, which is specifically reached when no ring-edge offsets were emitted (the smooth/no-crease case noted in the surrounding comments). For segmented shaped bevels (segments > 1), the strip does not contribute the direct innerA-innerB edge, so skipping the cap here can leave the chamfer end unclosed (or force the generic hole-filler to patch it unpredictably), regressing manifoldness on smooth character-style meshes.

Useful? React with 👍 / 👎.

Comment thread src/HalfEdgeMesh.cpp Outdated
Comment on lines +2249 to +2250
const bool shaped = isV1 ? v1Shaped : v2Shaped;
if (shaped) return;

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 Avoid skipping empty-ring cap for shaped bevel endpoints

In the ring.empty() branch there is no neighbor-ring retriangulation step to consume chain edges, so this early return can remove the only endpoint closure triangle for shaped profiles. For interior beveled edges whose endpoint ring fails (e.g., endpoint incident to boundary/non-manifold topology), this creates an uncapped chamfer end and introduces new boundary edges.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/HalfEdgeMesh_test.cpp`:
- Around line 2701-2724: interZpos/interZneg currently collect all new vertices
on the ±Z plane including endpoint offset verts (the v1a/v1b end offsets), which
can make probeFaceAtPlane(refsIntermediate) pass falsely; change the collection
to filter out endpoint-offset vertices so only true chain interior vertices are
included (i.e., when iterating newVerts, skip vertices whose position matches
the known endpoint offset positions or otherwise identify them as the
end-offsets) before calling probeFaceAtPlane for probeFront/probeBack; apply the
identical filtering change in the convex-case code path referenced around lines
2794-2805 so both tests only pass interior chain verts to probeFaceAtPlane.

In `@src/HalfEdgeMesh.cpp`:
- Around line 2245-2250: The early return when ring.empty() and shaped (using
isV1 ? v1Shaped : v2Shaped) is too aggressive: instead of unconditionally
skipping shaped endpoint caps, verify that every segment of the corresponding
vChain (each chain[i]→chain[i+1]) is already emitted/covered before returning;
if any chain edge is not covered, do not return—emit a per-segment cap fan for
each chain segment (rather than relying only on the polygon.size() == 2
innerA-innerB chord check). Update the logic in the ring.empty() branch (and the
analogous block at lines ~2364-2375) to iterate the chain segments and gate the
shaped-skip on all being emitted, otherwise fall back to the topology-safe cap
per-segment emission.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 76d5c988-2728-42f9-81cf-23fd153269ac

📥 Commits

Reviewing files that changed from the base of the PR and between 2d06947 and 6c120cc.

📒 Files selected for processing (3)
  • CMakeLists.txt
  • src/HalfEdgeMesh.cpp
  • src/HalfEdgeMesh_test.cpp

Comment thread src/HalfEdgeMesh_test.cpp Outdated
Comment thread src/HalfEdgeMesh.cpp Outdated
Codex + CodeRabbit flagged that the blanket cap-skip in buildCorner
can leave shaped bevel endpoints open in two cases:

1. ring.empty() — no neighbor-face retriangulation runs, so the chain
   edges have no partner from the face side. For shaped profiles, fan
   v across the full chain polyline so each chain edge gets paired
   with a cap triangle. Flat profile still emits the original 3-vertex
   cap.
2. polygon.size() == 2 (smooth/no-crease case) — before skipping, walk
   the chain and verify every chain[i]↔chain[i+1] is already an
   emitted edge. If yes, the splice + strip covered the end, skip.
   If not, fan v across the chain as in case (1).

Also tighten the CarvesAdjacentFaces tests (CodeRabbit minor): filter
to strict chain-interior intermediates (positions off the two bevel-
boundary cube edges at the endpoint) so the probe can't be satisfied
by the end offsets alone. Convex test uses 0.75 instead of 1.0
because max convex pushes intermediates past the face plane's own
edge, which isn't useful for the probe.

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

Copy link
Copy Markdown

@fernandotonon
fernandotonon merged commit 2dad59b into master Apr 23, 2026
19 checks passed
@fernandotonon
fernandotonon deleted the feat/bevel-concave-on-adjacent-faces branch April 23, 2026 12:45
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