Skip to content

feat(knife): multi-point cut tool with live preview - #308

Merged
fernandotonon merged 7 commits into
masterfrom
feat/knife-tool
Apr 24, 2026
Merged

feat(knife): multi-point cut tool with live preview#308
fernandotonon merged 7 commits into
masterfrom
feat/knife-tool

Conversation

@fernandotonon

@fernandotonon fernandotonon commented Apr 24, 2026

Copy link
Copy Markdown
Owner

Summary

Adds the knife topology tool that was scoped when we decided against starting with loop-cut: unlike loop-cut, the knife doesn't care about manifold ring walks, so it works cleanly on both primitives and arbitrary imported meshes.

  • Core primitives (HalfEdgeMesh): splitEdge(edgeIdx, t) inserts a vertex at parametric t on an edge and splits each adjacent triangle into two; splitFace(faceIdx, vA, vB) adds a diagonal between two boundary vertices of an n-gon; appendFace(vector<int>, subMeshIndex) is the n-gon generalization of appendTriangle. splitEdge interpolates position, normal, UV, color, tangent, and bone weights between the endpoints. Boundary edges handled (one face splits instead of two).
  • Knife state machine (EditModeController): beginKnife / addKnifePoint / updateKnifeHover / commitKnife / cancelKnife. A KnifeSession holds the confirmed point list + the live hover snap. Points are typed — OnVertex, OnEdge(edgeIdx, t), or OnFace(triIdx, localPos).
  • Hit-test with snap priority: vertex within 10px → edge within 10px → ray-cast against faces. Edge parameter t is computed via closest-point-on-line-segment against the click ray in local mesh space, so the placement tracks the cursor even on oblique edges.
  • Live preview overlay: a dynamic ManualObject draws the confirmed polyline in solid yellow and the pending segment (last-confirmed → cursor snap) in a dimmer ghost yellow, using the existing EditMode/EdgeSelection material.
  • Commit pipeline: walks the point list, calls splitEdge for each OnEdge point, pushes one EditMeshTopologyCommand so undo/redo rewinds the whole cut in one step. The M1↔M2 edge across a single triangle falls out of two splitEdge calls automatically — no splitFace needed in the common case.
  • UX wiring: K enters knife mode, Esc cancels, Enter/Return commits, scissors button on the edit-mode toolbar next to Extrude/Bevel. Mutual exclusion with bevel in both directions. Knife always tears down on edit-mode exit (committing a stale mid-cut would be surprising).
  • Sentry breadcrumbs: edit_mode for session lifecycle with point counts (Knife: point added (n=3), Knife: commit (points=3, cuts=2)), ui.action for toolbar click, ui.shortcut for K / Esc / Enter.

Scope limits (deliberate)

  • splitEdge requires adjacent faces to already be triangles — our entire pipeline feeds triangles through Assimp, so this matches reality.
  • splitFace is a correct primitive but not used by the commit pipeline yet; it'll become relevant if we add a loop-cut or knife-project tool.
  • On-face cut points are captured in the preview overlay but the commit skips them for now (they'd need splitFace + a new vertex on the face interior, which is a real project but not v1).

Tests

  • 9 new unit tests for splitEdge/splitFace (pure topology, no Ogre): midpoint split of an interior edge, attribute interpolation (UV/normal), boundary-edge split, t-clamping, invalid-index rejection, submesh preservation, bad-hint fallback, adjacent-vertex rejection, M1↔M2 edge invariant after two splitEdges. Full HalfEdgeMeshStandalone suite: 106/106.
  • 6 new E2E tests (EditModeControllerBevelE2ETest): begin→cancel lifecycle, commit-without-points rejection, mutual exclusion with bevel (both directions), exitEditMode tears down the session, beginKnife outside edit mode refuses. Skip on macOS (Ogre headless limitation) and run on Linux CI.

Test plan

  • ./build_local/bin/UnitTests --gtest_filter="HalfEdgeMeshStandalone.*" → 106/106 pass
  • ./build_local/bin/UnitTests --gtest_filter="*Standalone*:*Geometry*" → 181 pass, 3 pre-existing skips
  • App builds clean on macOS arm64 (release target).
  • Manual: create a Cube, enter edit mode, press K, click two different edges, press Enter, confirm new vertex + edges exist and undo rewinds.
  • CI: Linux build + test, macOS build, Windows build, SonarCloud.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Interactive Knife tool with live preview overlay, K shortcut, toolbar button, Enter to commit and Esc to cancel; knife cancels when leaving edit mode or starting bevel.
    • More robust multi-point mesh cuts with reliable undo/redo and preserved materials.
  • Tests
    • Added unit and integration tests for Knife workflow, split/cut operations, and undo behavior.
  • Chores
    • Project version updated to 2.29.1.

fernandotonon and others added 2 commits April 24, 2026 01:40
…fe tool

Adds the topology primitives the knife tool needs. All three are pure
data-structure operations with no rendering coupling, covered by unit
tests so the knife UI layer above can rely on them.

- appendFace(vector<int>, subMeshIndex): n-gon generalization of the
  existing appendTriangle helper. Single-polygon face append; the
  standard rebuildEdgesAndTwins / buildBoundaryHalfEdges / fixVertex
  cleanup still runs afterwards. appendTriangle is now a thin wrapper.

- splitEdge(edgeIdx, t): insert a new vertex on an edge at parametric
  position t ∈ (0,1). Replaces each adjacent triangle with two smaller
  triangles meeting at the new midpoint. Interpolates position, normal,
  UV, color, tangent, and bone weights (bone list is unioned by index).
  Works on boundary edges too (only one face splits). MVP limit: only
  splits faces that are already triangles.

- splitFace(faceIdx, vA, vB): insert a diagonal edge between two
  boundary vertices of an n-gon face, producing two new faces. Rejects
  adjacent boundary-vertex pairs (would duplicate an existing edge),
  same-vertex arguments, and out-of-range inputs. Triangle faces always
  fail (any pair of tri verts is adjacent) — which is fine for the
  knife, since two splitEdges on one triangle already produce the cut
  edge, no follow-up splitFace needed.

Tests (9 new, all pass; full HE suite 106/106):
- SplitEdgeMidpointOfInteriorEdgeDoublesTriangles
- SplitEdgeInterpolatesNormalAndUV
- SplitEdgeBoundaryEdgeProducesOneExtraTriangle
- SplitEdgeClampsExtremeT
- SplitEdgeInvalidIndexReturnsMinusOne
- SplitEdgePreservesSubmeshCount
- SplitFaceRejectsAdjacentBoundaryVertices
- SplitFaceRejectsInvalidVertexIndices
- TwoSplitEdgesOnOneTriangleProduceMidpointEdge (knife commit invariant)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the knife tool that was the original goal of this branch. Layered
on top of splitEdge/splitFace/appendFace (landed in the previous commit)
so the controller side stays small and test-friendly.

User-facing flow:
- K in edit mode (or click the scissors button on the edit-mode toolbar)
  opens a knife session.
- Left-click places a cut point. The hit-test priority is vertex snap →
  edge snap → face ray-cast (10px for each snap radius).
- Cursor-hover draws a dim ghost segment from the last confirmed point
  to the current snap target, so the user previews the pending cut.
- Enter / Return commits: the confirmed point list is walked, each
  OnEdge point triggers a splitEdge, the final mesh is pushed through
  EditMeshTopologyCommand so undo/redo rewinds in one step.
- Esc cancels without mutating the mesh.
- Edit-mode exit, or starting a bevel, cancels any active session.

Wiring:
- EditModeController: KnifePoint/KnifeSession types, beginKnife /
  addKnifePoint / updateKnifeHover / commitKnife / cancelKnife plus
  knifeSessionActiveValue and knifePointCountValue Q_PROPERTYs for QML.
  knifeHitTest uses the existing hitTestVertex/Edge/Face plumbing.
  Preview overlay is a ManualObject drawn via the EditMode/EdgeSelection
  material (same pipeline as the bevel selection overlay).
- TransformOperator: mousePressEvent / mouseMoveEvent get a knife
  priority branch that short-circuits selection/bevel/transform while
  the session is active.
- mainwindow.cpp: K keyboard shortcut (edit mode only, no modifier;
  follows the Blender convention). Esc and Enter routed to
  cancelKnife / commitKnife when the session is active. Toolbar gets a
  scissors button next to Extrude/Bevel, enabled whenever edit mode is
  on regardless of selection (knife hit-tests against geometry, not
  selection).
- Sentry breadcrumbs: edit_mode for begin / point-added / commit /
  cancel, ui.action for the toolbar click, ui.shortcut for K / Esc /
  Enter.

Tests:
- 9 splitEdge/splitFace unit tests (prior commit) — all 106 HE tests
  still pass.
- 6 new EditModeControllerBevelE2ETest knife scenarios covering the
  session lifecycle (begin→cancel, commit-without-points, mutual
  exclusion with bevel in both directions, exitEditMode cleanup,
  beginKnife outside edit mode). They skip on macOS where Ogre isn't
  available headlessly (same pattern as all E2E tests) and run on
  Linux CI.

Known MVP limits documented in splitEdge/splitFace contracts:
- splitEdge requires adjacent faces to already be triangles.
- splitFace is only useful on n-gons; two splitEdges on a single tri
  already leave the M1↔M2 cut edge in place so splitFace isn't called
  in the common case.
- On-face cut points are captured in the preview but skipped by the
  commit pipeline for now; the visible cut is only real on edges and
  vertices.

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

coderabbitai Bot commented Apr 24, 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 52 minutes and 23 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 52 minutes and 23 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: 44157dbb-34b3-4bf2-b2a9-115049b2b91e

📥 Commits

Reviewing files that changed from the base of the PR and between 50a0125 and d669884.

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

Walkthrough

Adds an interactive multi-point "Knife" editing tool: session lifecycle and UI hooks in EditModeController, topology primitives (edge/face split and cutPath) in HalfEdgeMesh, input/preview integration in TransformOperator/mainwindow, and tests covering session behavior and topology correctness.

Changes

Cohort / File(s) Summary
EditModeController Knife API
src/EditModeController.h, src/EditModeController.cpp
Introduces full knife session API and state: beginKnife, addKnifePoint, addKnifePointOnEdge, updateKnifeHover, commitKnife, cancelKnife, hit-testing, preview overlay lifecycle, QML properties (knifeSessionActiveValue, knifePointCountValue) and knifeSessionChanged signal. Ensures mutual exclusion with bevel and cancels on exit.
HalfEdgeMesh Topology Operations
src/HalfEdgeMesh.h, src/HalfEdgeMesh.cpp
Adds polygonal appendFace, delegates appendTriangle to it. New topology ops: splitEdge(edgeIdx,t), splitFace(faceIdx,vA,vB), cutPath(points) and CutPoint struct. Implements attribute interpolation, face retirement/replacement, and path-based multi-split cut algorithm with rollback on failure.
Input & Preview Integration
src/TransformOperator.cpp, src/mainwindow.cpp
Integrates knife into input flow: left-click adds knife points (short-circuiting selection/transform), mouse-move updates hover preview, keyboard: K to begin, Esc cancels, Enter commits; key swallowing while knife active. Adds toolbar action and logging.
Mesh / EditableMesh Integration & GPU
src/EditModeController.cpp (sub-areas)
Commit path converts half-edge results to EditableMesh, rebuilds Ogre entity (preserves SubEntity materials), invalidates RTShader materials for vertex-layout changes, clears selection sets, and pushes undoable EditMeshTopologyCommand("Knife").
Tests
src/EditModeController_test.cpp, src/HalfEdgeMesh_test.cpp
New unit tests for knife lifecycle, mutual exclusion with bevel, commit/rollback semantics, splitEdge/splitFace behaviors, cutPath scenarios, attribute interpolation, manifoldness checks, and undo/redo validation.
Build & Misc
CMakeLists.txt
Project version bumped to 2.29.1 (previously 2.28.3).

Sequence Diagram

sequenceDiagram
    participant User as User
    participant TransformOp as TransformOperator
    participant EditCtrl as EditModeController
    participant HEM as HalfEdgeMesh
    participant Preview as PreviewOverlay

    User->>TransformOp: Mouse Move
    TransformOp->>EditCtrl: updateKnifeHover(widget, x,y)
    EditCtrl->>EditCtrl: knifeHitTest(screenPos)
    EditCtrl->>Preview: updateKnifePreviewOverlay()
    Preview-->>User: render hover

    User->>TransformOp: Left Click
    TransformOp->>EditCtrl: addKnifePoint(widget, x,y)
    EditCtrl->>EditCtrl: validate & append confirmed KnifePoint
    EditCtrl-->>TransformOp: consume click (early return)

    User->>EditCtrl: Press Enter (Commit)
    EditCtrl->>HEM: cutPath(resolvedCutPoints)
    HEM->>HEM: splitEdge / splitFace (interpolate & replace faces)
    HEM-->>EditCtrl: return new vertex indices
    EditCtrl->>EditCtrl: toEditableMesh + rebuild Ogre entity + push undo command
    EditCtrl->>Preview: destroyKnifePreviewOverlay()
    EditCtrl-->>User: knifeSessionChanged (inactive)
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hop and I hover, a sharp little thrill,

I click out a path and my whiskers stand still.
New vertices bloom where my little blade traced,
Enter to finish — the old mesh replaced. ✂️🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.87% 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 PR title clearly and concisely summarizes the main feature: a multi-point knife cut tool with live preview.
Description check ✅ Passed The PR description comprehensively covers Summary and Technical Details sections with clear bullet points, architectural decisions, scope limits, test coverage, and test plan.
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/knife-tool

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: 058a274baf

ℹ️ 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 on lines +2297 to +2300
for (const auto& p : m_knifeSession.points) {
if (p.kind == KnifePoint::OnEdge && p.edgeIndex >= 0) {
if (hm.splitEdge(p.edgeIndex, p.edgeT) >= 0) ++inserted;
}

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 Re-resolve edge IDs before each split during knife commit

The commit loop replays edge indices captured at click time via hm.splitEdge(p.edgeIndex, p.edgeT), but splitEdge rebuilds the half-edge/edge tables after each successful split, which can renumber edges mid-commit. In multi-point cuts (for example two points on different edges of the same original triangle), later points can therefore apply to the wrong edge or fail, producing incorrect topology for the committed cut.

Useful? React with 👍 / 👎.

Comment thread src/EditModeController.cpp Outdated
Comment on lines +2404 to +2405
const Ogre::Vector3 w = localOrigin - p0;
const float t = std::clamp(d.dotProduct(w) / dd, 0.0f, 1.0f);

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 Compute edge snap parameter using ray direction, not origin

The edge snap t is derived from localOrigin only (dot(d, localOrigin - p0)), while the ray direction is ignored. Under perspective projection, the ray origin is the camera position for every screen pixel, so this makes t effectively independent of cursor position along that edge; users will get nearly fixed split points instead of cuts following where they clicked.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/HalfEdgeMesh_test.cpp (1)

2242-2242: ⚠️ Potential issue | 🔴 Critical

Rename this test; the current name collides with an existing GTest.

TEST(HalfEdgeMeshStandalone, SmoothSurfaceBevelProducesManifold) is already defined earlier in this file at Line 1824. Reusing the same suite/name pair causes a redefinition in the generated GTest symbols, so this file will not compile.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/HalfEdgeMesh_test.cpp` at line 2242, The TEST macro invocation duplicates
an existing GTest symbol; change the test name used in
TEST(HalfEdgeMeshStandalone, SmoothCharacterBevelProducesManifold) to a unique
identifier (e.g. SmoothCharacterBevelProducesManifold_Unique or another distinct
name) so it no longer collides with the previously defined
TEST(HalfEdgeMeshStandalone, SmoothSurfaceBevelProducesManifold); update only
the second parameter of the TEST macro to a new unique test name and ensure no
other TEST in the file uses that same suite/name pair.
🧹 Nitpick comments (2)
src/EditModeController_test.cpp (1)

1207-1276: Consider asserting enterEditMode() preconditions.

Each test calls ctrl->enterEditMode() without a matching ASSERT_TRUE, unlike the bevel tests above in this same fixture (e.g., Line 913 calls ctrl->enterEditMode() the same way, but bevelSelection() is then ASSERT_TRUE'd). For the knife tests, the very first assertion — EXPECT_FALSE(ctrl->knifeSessionActive()) — doesn't distinguish between "edit mode failed to start" and "knife correctly reports inactive". Promoting these to ASSERT_TRUE(ctrl->enterEditMode()) (except in KnifeBeginOutsideEditModeFails) would make failures easier to diagnose if fixture setup drifts.

🧪 Example hardening for one test
 TEST_F(EditModeControllerBevelE2ETest, KnifeBeginThenCancelRestoresIdleState) {
     auto* ctrl = EditModeController::instance();
-    ctrl->enterEditMode();
+    ASSERT_TRUE(ctrl->enterEditMode());
 
     EXPECT_FALSE(ctrl->knifeSessionActive());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/EditModeController_test.cpp` around lines 1207 - 1276, These tests call
ctrl->enterEditMode() without asserting it succeeded, which can mask setup
failures; update the five knife-related tests
(KnifeBeginThenCancelRestoresIdleState,
KnifeCommitWithoutPointsRejectsAndCleansUp, KnifeBeginCancelsActiveBevelFirst,
BeginBevelCancelsActiveKnifeFirst, ExitEditModeCancelsKnifeSession) to use
ASSERT_TRUE(ctrl->enterEditMode()) immediately after obtaining the controller so
the fixture setup is validated (leave KnifeBeginOutsideEditModeFails unchanged
since it intentionally expects edit mode off).
src/HalfEdgeMesh.cpp (1)

3768-3793: Validate both sides before retiring the old face.

sideAB.size() / sideBA.size() < 3 is checked at line 3793 after the face's HEs and m_faces[faceIdx].halfEdge have already been cleared (lines 3768-3778). The earlier gap rejection makes this check dead for the current n ∈ {3,4} range, but the ordering is fragile — any future tweak that loosens the gap check would turn this path into silent corruption (face gone, no replacements, hole left in the mesh).

Prefer to compute the sides first and retire only once both sides are known to be well-formed.

♻️ Reorder validation before retirement
-    const int subMeshIndex = m_faces[faceIdx].subMeshIndex;
-
-    // Retire the old face.
-    {
-        const int startHE = m_faces[faceIdx].halfEdge;
-        int he = startHE;
-        do {
-            const int next = m_halfEdges[he].next;
-            m_halfEdges[he].face = -1;
-            he = next;
-        } while (he != startHE);
-        m_faces[faceIdx].halfEdge = -1;
-    }
-
-    // Walk the old loop from vA to vB to collect one side, then from vB
-    // to vA for the other. appendFace needs at least 3 vertices, so a
-    // degenerate side (empty) bails the whole operation.
     std::vector<int> sideAB;
     for (int i = posA; ; i = (i + 1) % n) {
         sideAB.push_back(verts[i]);
         if (i == posB) break;
     }
     std::vector<int> sideBA;
     for (int i = posB; ; i = (i + 1) % n) {
         sideBA.push_back(verts[i]);
         if (i == posA) break;
     }
     if (sideAB.size() < 3 || sideBA.size() < 3) return false;
 
+    const int subMeshIndex = m_faces[faceIdx].subMeshIndex;
+
+    // Retire the old face only once both sides are known to be valid.
+    {
+        const int startHE = m_faces[faceIdx].halfEdge;
+        int he = startHE;
+        do {
+            const int next = m_halfEdges[he].next;
+            m_halfEdges[he].face = -1;
+            he = next;
+        } while (he != startHE);
+        m_faces[faceIdx].halfEdge = -1;
+    }
+
     appendFace(sideAB, subMeshIndex);
     appendFace(sideBA, subMeshIndex);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/HalfEdgeMesh.cpp` around lines 3768 - 3793, Compute and validate the two
vertex loops (sideAB and sideBA) before modifying or retiring the existing face
data to avoid leaving the mesh in a corrupted state; specifically, build sideAB
and sideBA using verts, posA, posB, and n, check that both sizes are >= 3, and
only if validation passes then proceed to clear m_halfEdges[*].face and set
m_faces[faceIdx].halfEdge = -1 and continue with appendFace; ensure references
to m_faces[faceIdx], m_halfEdges, sideAB, sideBA, appendFace remain unchanged so
the rest of the flow uses the validated sides.
🤖 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/EditModeController.cpp`:
- Around line 2296-2300: The loop is using stale KnifePoint.edgeIndex values
(captured from a temporary mesh) before calling hm.splitEdge, so later splits
can target the wrong/invalid edge; for each point in m_knifeSession.points,
re-resolve the target edge against the current hm just prior to splitting (e.g.,
use the stored point position/uv from KnifePoint to look up the current edge and
param t with the existing hit-test/find method) and only call hm.splitEdge with
the freshly-resolved edgeIndex/t; update or skip the split if the lookup fails.
Ensure you reference m_knifeSession.points, KnifePoint.edgeIndex/edgeT and
hm.splitEdge() when making the change.
- Around line 2310-2334: The knife commit updates m_editableMesh->subMeshes()
but does not sync those changes to the live Ogre entity; replicate the
post-topology-update steps used in extrudeSelection()/applyBevelTopology():
after setting m_editableMesh->subMeshes(), call the same sequence that resizes
entity buffers, reinitializes subentities and invalidates RTSS/material state
(e.g. invoke resizeEntityBuffers(...), reinitSubEntities(...) and
invalidateRTSSMaterials() or their local equivalents), then push the
EditMeshTopologyCommand and emit the same signals so the cut becomes immediately
visible; locate these helper calls in extrudeSelection/applyBevelTopology and
apply them here around the EditMeshTopologyCommand usage.
- Around line 2386-2405: The current code computes t using only localOrigin and
the edge (p0,p1), ignoring the ray direction (localDir); replace that with a
proper closest-point solve between the ray (localOrigin + s*localDir) and the
segment (p0 + t*d): compute r0 = localOrigin - p0, a = localDir.dot(localDir), b
= localDir.dot(d), c = d.dot(d) (dd), e = localDir.dot(r0), f = d.dot(r0), then
solve the 2x2 normal equations for t = (a*f - b*e)/(a*c - b*b) and s = (b*t -
e)/a; clamp t into [0,1]; if the denominator (a*c - b*b) is near zero (parallel
case) fall back to projecting localOrigin onto the segment with t = clamp(f /
c); update the code that currently sets t from d.dotProduct(w) / dd to use this
computation (use the existing symbols localDir, localOrigin, p0, p1, d, dd, t)
and ensure vector dot products use float/Ogre::Real as appropriate.
- Around line 2471-2487: updateSelectionOverlay() creates m_overlayNode under
the entity node and feeds it local-space geometry, but this code reuses
m_overlayNode for the knife and then applies the entity's derived transform
again, causing double-transforms; instead create a distinct scene node for the
knife (e.g., m_overlayKnifeNode or m_knifeNode) and attach m_overlayKnife to
that node rather than m_overlayNode, then set the derived
position/orientation/scale on the new knife node (or attach it under the same
parent as m_overlayNode if you want same-space behavior) so you no longer
overwrite or double-transform m_overlayNode; update the code in the block that
checks/creates m_overlayNode/m_overlayKnife to create and use m_overlayKnifeNode
and attach m_overlayKnife to it.

In `@src/HalfEdgeMesh.cpp`:
- Around line 3747-3748: The guard in splitFace that rejects faces with
verts.size() > 4 contradicts the documented "n-gon" support; update splitFace to
accept any face with verts.size() >= 3 by removing the upper-bound check (locate
the check using faceVertices(faceIdx) and the conditional that returns false on
size() > 4) so the function can handle arbitrary n-gons (appendFace already
supports n ≥ 3); ensure any subsequent logic that iterates face vertices still
works for variable-length faces.
- Around line 3687-3729: The splitEdge implementation lets the split proceed
when only one adjacent face is a triangle which corrupts topology; in splitEdge,
replace the permissive early-out that returns only if both describeFace calls
fail with a guard that requires both adjacent faces to be valid triangles (i.e.
if (!hasA || !hasB) return -1) so the routine aborts when either side is a
non-triangle; reference describeFace, splitEdge, the retireFace lambda,
appendFace, rebuildEdgesAndTwins and buildBoundaryHalfEdges when making this
change to ensure no mesh mutation occurs unless both faces are triangles.

In `@src/mainwindow.cpp`:
- Around line 1060-1075: During an active knife session
(editCtrl->knifeSessionActive()) other edit-mode shortcuts are still handled
later by the isEditModeActive() switch, causing mode flips; update the
knife-session branch to also consume (event->accept(); return;) or otherwise
block the edit-mode keys so they don't fall through. Specifically, intercept
digit keys (1/2/3), unmodified 'K', and modifier combos like Ctrl+A/Alt+A,
Ctrl+E, Ctrl+B (and any other edit-mode shortcuts handled by isEditModeActive())
while knifeSessionActive() is true, or add a clear comment in the knife-session
block explaining that letting those keys through is intentional.

---

Outside diff comments:
In `@src/HalfEdgeMesh_test.cpp`:
- Line 2242: The TEST macro invocation duplicates an existing GTest symbol;
change the test name used in TEST(HalfEdgeMeshStandalone,
SmoothCharacterBevelProducesManifold) to a unique identifier (e.g.
SmoothCharacterBevelProducesManifold_Unique or another distinct name) so it no
longer collides with the previously defined TEST(HalfEdgeMeshStandalone,
SmoothSurfaceBevelProducesManifold); update only the second parameter of the
TEST macro to a new unique test name and ensure no other TEST in the file uses
that same suite/name pair.

---

Nitpick comments:
In `@src/EditModeController_test.cpp`:
- Around line 1207-1276: These tests call ctrl->enterEditMode() without
asserting it succeeded, which can mask setup failures; update the five
knife-related tests (KnifeBeginThenCancelRestoresIdleState,
KnifeCommitWithoutPointsRejectsAndCleansUp, KnifeBeginCancelsActiveBevelFirst,
BeginBevelCancelsActiveKnifeFirst, ExitEditModeCancelsKnifeSession) to use
ASSERT_TRUE(ctrl->enterEditMode()) immediately after obtaining the controller so
the fixture setup is validated (leave KnifeBeginOutsideEditModeFails unchanged
since it intentionally expects edit mode off).

In `@src/HalfEdgeMesh.cpp`:
- Around line 3768-3793: Compute and validate the two vertex loops (sideAB and
sideBA) before modifying or retiring the existing face data to avoid leaving the
mesh in a corrupted state; specifically, build sideAB and sideBA using verts,
posA, posB, and n, check that both sizes are >= 3, and only if validation passes
then proceed to clear m_halfEdges[*].face and set m_faces[faceIdx].halfEdge = -1
and continue with appendFace; ensure references to m_faces[faceIdx],
m_halfEdges, sideAB, sideBA, appendFace remain unchanged so the rest of the flow
uses the validated sides.
🪄 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: 941270d1-2ce1-4ef6-8f49-64640d1b9514

📥 Commits

Reviewing files that changed from the base of the PR and between 5533647 and 058a274.

📒 Files selected for processing (8)
  • src/EditModeController.cpp
  • src/EditModeController.h
  • src/EditModeController_test.cpp
  • src/HalfEdgeMesh.cpp
  • src/HalfEdgeMesh.h
  • src/HalfEdgeMesh_test.cpp
  • src/TransformOperator.cpp
  • src/mainwindow.cpp

Comment thread src/EditModeController.cpp Outdated
Comment thread src/EditModeController.cpp Outdated
Comment thread src/EditModeController.cpp Outdated
Comment thread src/EditModeController.cpp Outdated
Comment thread src/HalfEdgeMesh.cpp
Comment thread src/HalfEdgeMesh.cpp
Comment on lines +3747 to +3748
const auto verts = faceVertices(faceIdx);
if (verts.size() < 3 || verts.size() > 4) return false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Doc vs. code: n-gon claim contradicts the size() > 4 reject.

The PR description (and the knife-tool companion docs) describe splitFace as cutting a diagonal across "an n-gon", but the guard here refuses anything larger than a quad. appendFace already supports arbitrary n ≥ 3, so the cap looks like an MVP choice rather than an algorithmic constraint.

Either drop the upper bound (the existing gap check and side-size walk handle arbitrary n) or add a doxygen comment stating the v1 limit explicitly so callers know the n-gon phrasing is aspirational.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/HalfEdgeMesh.cpp` around lines 3747 - 3748, The guard in splitFace that
rejects faces with verts.size() > 4 contradicts the documented "n-gon" support;
update splitFace to accept any face with verts.size() >= 3 by removing the
upper-bound check (locate the check using faceVertices(faceIdx) and the
conditional that returns false on size() > 4) so the function can handle
arbitrary n-gons (appendFace already supports n ≥ 3); ensure any subsequent
logic that iterates face vertices still works for variable-length faces.

Comment thread src/mainwindow.cpp Outdated
…edges

The MVP knife dropped isolated vertices on each clicked edge but left the
segments between them as pure preview, which made the tool useless
beyond "insert vertex on edge." This changes the commit pipeline to
actually chase the cut line across the mesh.

New HalfEdgeMesh::cutPath(vector<CutPoint>):
- Re-resolves each click's edge by its endpoint vertex pair after every
  internal splitEdge (edge indices aren't stable across
  rebuildEdgesAndTwins; vertex indices are append-only).
- Splits each click endpoint, then for every consecutive pair walks the
  tris between the resulting vertices. For each step the algorithm picks
  the face adjacent to the current anchor vertex and intersects the
  3D segment to the next anchor with every non-entry edge of that face.
  The nearest forward crossing that sits strictly inside an edge wins;
  splitEdge runs there, producing the interior mid-cut vertex. The
  shared-face short-circuit stops the walk once the two anchors sit on
  the same triangle — the tri's two existing splitEdges already left
  the connecting segment as a real edge.
- Handles degenerate inputs (fewer than 2 points, invalid edge index)
  by returning an empty vector without mutating the mesh.

closestOnSegments() helper: 3D segment-segment minimization used to
find where the cut line exits a triangle. Returns the parameter pair
plus squared separation so callers can filter out "close but not
coincident" pairs.

EditModeController::commitKnife now calls cutPath once per commit with
the filtered on-edge click list. The breadcrumb count reflects the
total new vertex count, which includes interior crossings.

Tests (4 new, all pass; full HE suite 110/110):
- CutPathCrossesInteriorDiagonalAndLinksEndpoints: bottom-edge to
  top-edge cut across a two-tri quad produces three new verts on the
  x=0.5 line and they're linked by real edges.
- CutPathSingleTriangleStillProducesEndpointEdge: two edges of one tri,
  no interior crossing expected.
- CutPathBailsOnFewerThanTwoPoints: empty / single-point input.
- CutPathFailsOnInvalidEdgeIndex: negative / out-of-range edges.

Not yet pushed — waiting for in-app validation first.
…mp to 2.29.0

Completes the knife tool wrap-up started in the walk-and-cut commit:

- addKnifePointOnEdge(heEdgeIndex, t): programmatic cut-point entry that
  skips the widget-based hit-test. Scripted/automated knife flows can
  use it; it also unblocks the headless controller-level tests that
  can't run QWidget + camera projection.

- Three new EditModeControllerBevelE2ETest cases covering the commit
  pipeline end to end: vertex count grows manifoldly after a cut,
  undo rewinds to the original count, invalid edge indices are
  refused without mutating the session. All three skip on macOS
  (Ogre headless) like every other BevelE2E test and run on Linux CI.

- CMakeLists.txt VERSION bump 2.28.3 → 2.29.0 for the knife feature.

Tests: full HalfEdgeMeshStandalone suite still 110/110. Controller
suite builds clean.

@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: 3

♻️ Duplicate comments (1)
src/EditModeController.cpp (1)

2563-2579: ⚠️ Potential issue | 🟠 Major

Use a dedicated scene node for the knife overlay.

m_overlayNode is shared with the selection overlays, which normally render local-space geometry under the entity node. Reusing that node here and then writing the entity’s derived world transform onto it will double-transform whichever overlay created it first. Put m_overlayKnife on its own node, or keep it in the same local-space parent without mutating m_overlayNode.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/EditModeController.cpp` around lines 2563 - 2579, The overlay knife is
being attached to the shared m_overlayNode and then that node is overwritten
with the entity's derived world transform, causing double-transforms for other
overlays; fix by creating and using a dedicated scene node (e.g.,
m_overlayKnifeNode) for m_overlayKnife instead of m_overlayNode: allocate
m_overlayKnifeNode from sceneMgr->getRootSceneNode()->createChildSceneNode(),
attach m_overlayKnife to that node, and apply the
entNode->_getDerivedPosition()/Orientation()/Scale() to the new
m_overlayKnifeNode (leaving m_overlayNode untouched) so selection overlays
continue to render in local space.
🧹 Nitpick comments (1)
src/HalfEdgeMesh.cpp (1)

3935-3962: Exit-edge proximity tolerance is a fixed absolute in world-space.

The hit.squaredSeparation > 1e-4f check on line 3949 (and the kNearZero = 1e-5f endpoint guard on 3954) are absolute world-space values. Meshes imported at small scales (e.g., typical character rigs in meters where ~0.01 local units is already a meaningful feature) can see 1e-2-class separations on valid crossings, while giant props may produce sub-threshold noise on clearly-missed edges. Consider scaling the tolerance against the cut segment length |pT - pA| or a precomputed bounding-box diagonal so the walk behaves consistently across import scales.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/HalfEdgeMesh.cpp` around lines 3935 - 3962, The absolute proximity checks
(hit.squaredSeparation > 1e-4f and hit.sB < kNearZero / > 1.0f - kNearZero) make
edge-exit detection scale dependent; replace those fixed world-space constants
with scale-aware tolerances computed from the current cut segment length or a
mesh scale metric. Specifically, compute a local scale like float segLen = (pT -
pA).length() (or use the mesh bbox diagonal) and derive squaredTol = (segLen *
relEps) * (segLen * relEps) and nearZero = relNear * segLen (choose relEps ~
1e-3..1e-2, relNear ~ 1e-5..1e-3), then compare hit.squaredSeparation against
squaredTol and hit.sB against nearZero/ (1.0f - nearZero) instead of the fixed
1e-4f and kNearZero; update references to hit.squaredSeparation, hit.sB,
kNearZero, and any uses of kMinStep if it should also be relative.
🤖 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/EditModeController.cpp`:
- Around line 2368-2389: This commit path diverges from applyBevelTopology():
after calling m_editEntity->_deinitialise() / _initialise(true) you must restore
SubEntity material overrides and refresh/clear topology selections before
creating/pushing the EditMeshTopologyCommand so we don't keep stale IDs or lose
wireframe/material overrides. Locate the equivalent post-topology sequence in
applyBevelTopology() and copy its steps here: re-apply per-SubEntity material
overrides to m_editableMesh->subMeshes(), call the same selection refresh/clear
logic that updates m_selectedVertices/m_selectedEdges/m_selectedFaces (or clears
them if the topology no longer contains those IDs), then construct and push the
EditMeshTopologyCommand, followed by the existing Sentry breadcrumb,
m_knifeSession reset, destroyKnifePreviewOverlay() and emits.
- Around line 2336-2348: The commit currently filters m_knifeSession.points to
build cpts and silently drops non-OnEdge points, producing an incorrect cut for
previews containing OnFace/OnVertex; modify the commit guard in
EditModeController.cpp to first scan m_knifeSession.points for any KnifePoint
whose kind is not KnifePoint::OnEdge (e.g. KnifePoint::OnFace or
KnifePoint::OnVertex) and if any are found, log the same breadcrumb ("Knife:
commit rejected (unsupported point types)") and call cancelKnife() and return
false instead of proceeding to build cpts, so only fully OnEdge sequences are
ever committed; reference m_knifeSession, KnifePoint::OnEdge, cancelKnife(), and
cpts when locating where to add this check.

In `@src/HalfEdgeMesh.cpp`:
- Around line 3852-3894: cutPath currently mutates the mesh incrementally so a
mid-sequence failure (e.g. two CutPoints resolving to the same edge/endpoints)
leaves partial splits in m_edges and newVertices; detect and reject such
conflicting inputs up front by validating the resolved endpoint pairs in
clickEdgeVerts before performing any splitEdge calls. Specifically, in
HalfEdgeMesh::cutPath, after populating clickEdgeVerts and clickT but before the
loop that calls splitEdge, scan clickEdgeVerts for duplicates or pairs that
would collide (identical unordered vertex pairs) and return an empty newVertices
if any are found; reference the existing symbols clickEdgeVerts,
findEdgeByVerts, splitEdge, m_edges, and newVertices to locate and implement
this pre-check. Ensure the check treats (va,vb) and (vb,va) as equal so no split
occurs when two clicks target the same logical edge.

---

Duplicate comments:
In `@src/EditModeController.cpp`:
- Around line 2563-2579: The overlay knife is being attached to the shared
m_overlayNode and then that node is overwritten with the entity's derived world
transform, causing double-transforms for other overlays; fix by creating and
using a dedicated scene node (e.g., m_overlayKnifeNode) for m_overlayKnife
instead of m_overlayNode: allocate m_overlayKnifeNode from
sceneMgr->getRootSceneNode()->createChildSceneNode(), attach m_overlayKnife to
that node, and apply the entNode->_getDerivedPosition()/Orientation()/Scale() to
the new m_overlayKnifeNode (leaving m_overlayNode untouched) so selection
overlays continue to render in local space.

---

Nitpick comments:
In `@src/HalfEdgeMesh.cpp`:
- Around line 3935-3962: The absolute proximity checks (hit.squaredSeparation >
1e-4f and hit.sB < kNearZero / > 1.0f - kNearZero) make edge-exit detection
scale dependent; replace those fixed world-space constants with scale-aware
tolerances computed from the current cut segment length or a mesh scale metric.
Specifically, compute a local scale like float segLen = (pT - pA).length() (or
use the mesh bbox diagonal) and derive squaredTol = (segLen * relEps) * (segLen
* relEps) and nearZero = relNear * segLen (choose relEps ~ 1e-3..1e-2, relNear ~
1e-5..1e-3), then compare hit.squaredSeparation against squaredTol and hit.sB
against nearZero/ (1.0f - nearZero) instead of the fixed 1e-4f and kNearZero;
update references to hit.squaredSeparation, hit.sB, kNearZero, and any uses of
kMinStep if it should also be relative.
🪄 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: 5ebd1d2f-4a7c-4900-8353-95f0baf3ffda

📥 Commits

Reviewing files that changed from the base of the PR and between 058a274 and 7c8736c.

📒 Files selected for processing (7)
  • CMakeLists.txt
  • src/EditModeController.cpp
  • src/EditModeController.h
  • src/EditModeController_test.cpp
  • src/HalfEdgeMesh.cpp
  • src/HalfEdgeMesh.h
  • src/HalfEdgeMesh_test.cpp
✅ Files skipped from review due to trivial changes (1)
  • CMakeLists.txt
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/EditModeController_test.cpp
  • src/HalfEdgeMesh.h

Comment thread src/EditModeController.cpp
Comment thread src/EditModeController.cpp
Comment thread src/HalfEdgeMesh.cpp
# Conflicts:
#	src/EditModeController_test.cpp
Seven fixes for the walk-and-cut knife, none behaviour changes in the
happy path but all correctness / UX for edge cases.

- splitEdge atomic on mixed tri/non-tri adjacency: if either adjacent
  face exists and isn't a triangle, bail with -1 before any mutation.
  Previously the half-split path silently desynced the untouched side's
  half-edge pointers against a face that was still an n-gon.

- cutPath atomic on mid-sequence failure: snapshot m_halfEdges /
  m_vertices / m_faces / m_edges at entry and restore on any early
  return. Duplicate-edge CutPoints (two clicks resolving to the same
  edge) used to leave a half-applied cut visible to the caller; now
  the mesh is byte-identical to its pre-call state when cutPath fails.
  New test: CutPathRollsBackWhenSecondEdgeDuplicatesFirst.

- commitKnife refuses non-edge clicks: if any confirmed point is
  OnFace or OnVertex, bail with a Sentry breadcrumb instead of
  silently dropping it. A visible preview edge→face→edge would have
  committed as edge→edge, diverging from what the user drew.

- Knife session swallows every keyboard input except Esc/Enter: plain
  1/2/3, Ctrl+A, Ctrl+E, Ctrl+B, K, etc. used to fall through and
  mutate state mid-cut. Session keyPressEvent now event->accept()s
  everything else and returns.

- Knife preview owns its own scene node (m_overlayKnifeNode) rather
  than reusing m_overlayNode with the selection overlays. The knife
  mirrors the entity's world transform; the selection overlays want
  origin-parked local-space parenting. They can't share.

- commitKnife mirrors applyBevelTopology's SubEntity-material restore
  + RTSS invalidate cycle. Without it, wireframe / MaterialEditor
  overrides got wiped whenever _deinitialise/_initialise ran.
  Selection sets are also cleared since the cut can retire the
  topology IDs they reference.

- splitFace doc clarified: explicitly triangle-rejecting (every pair
  of tri verts is adjacent), quad-splitting, higher n-gons refused as
  a scope choice not a contract limit.

Tests: 111/111 HalfEdgeMeshStandalone. Controller E2E suite builds
clean; knife scenarios skip on macOS (Ogre headless) and run on
Linux CI.

VERSION bump 2.29.0 → 2.29.1 — correctness patches on top of the
knife feature shipped in the previous commits of this PR.

@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: 3

🧹 Nitpick comments (1)
src/EditModeController.h (1)

294-349: Clarify that commitKnife() is edge-only in v1.

The public knife docs describe vertex/edge/face snapping as normal input, but the current commit path only succeeds for OnEdge points and rejects OnVertex/OnFace. Calling that out here would keep future callers from assuming any snapped point is committable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/EditModeController.h` around lines 294 - 349, The commitKnife()
documentation should explicitly state that, in v1, only cut points resolved as
OnEdge are applied (points snapped to vertices or faces are ignored/rejected);
update the comment on the commitKnife() declaration to note this limitation and
reference the knife point types and m_knifeSession handling so callers know to
use addKnifePointOnEdge() or ensure points are OnEdge before committing; keep
addKnifePoint(), addKnifePointOnEdge(), knifeSessionActive(), and
knifePointCount() behavior unchanged.
🤖 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/EditModeController_test.cpp`:
- Around line 1451-1457: The test currently hard-codes half-edge indices (calls
to addKnifePointOnEdge(0, ...) and addKnifePointOnEdge(5, ...)) which rely on
internal HalfEdgeMesh ordering; instead, construct a fresh HalfEdgeMesh for the
cube face you intend to click, locate the edge indices by querying the mesh for
known vertex pairs or vertex positions on that face, then pass those resolved
edge indices into ctrl->addKnifePointOnEdge(...). Update the two places
mentioned (the calls at the shown location and the similar calls around
1493-1496) to compute edgeIndex = halfEdgeMesh.findEdgeIndexByVertexPair(vA, vB)
or by position lookup before invoking addKnifePointOnEdge, ensuring the two
resolved edges are surface-connected as required by the test.

In `@src/HalfEdgeMesh_test.cpp`:
- Around line 3281-3287: The test uses makeQuadMesh() which produces two
triangles so splitFace's triangle-rejection masks invalid-vertex checks; instead
build an editable mesh containing a single face with >=4 vertices (or otherwise
ensure the face under test is not a triangle) before calling
he.buildFromEditableMesh(em), then call he.splitFace(...) with out-of-range and
identical vertex indices to exercise the invalid-vertex-path; locate the setup
that creates em (makeQuadMesh or the EditableMesh construction) and replace it
with a single-quad or n-gon creation so splitFace(0, -1, 1), splitFace(0, 0,
999), and splitFace(0, 2, 2) actually reach the invalid-vertex validation.
- Around line 3154-3184: The test SplitEdgeInterpolatesNormalAndUV currently
omits any check of the interpolated UVs; update the test to assert that the new
vertex's UV (accessed via he.vertex(vMid).uv or v.uv) is present and equals the
expected interpolation between he.vertex(1).uv and he.vertex(2).uv at t=0.25
(use EXPECT_NEAR on each UV component with a small epsilon, mirroring how
position/normal are checked), so that splitEdge's UV interpolation is actually
verified.

---

Nitpick comments:
In `@src/EditModeController.h`:
- Around line 294-349: The commitKnife() documentation should explicitly state
that, in v1, only cut points resolved as OnEdge are applied (points snapped to
vertices or faces are ignored/rejected); update the comment on the commitKnife()
declaration to note this limitation and reference the knife point types and
m_knifeSession handling so callers know to use addKnifePointOnEdge() or ensure
points are OnEdge before committing; keep addKnifePoint(),
addKnifePointOnEdge(), knifeSessionActive(), and knifePointCount() behavior
unchanged.
🪄 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: 6fd72ee4-d7e4-4e0c-9061-1a6953b76aeb

📥 Commits

Reviewing files that changed from the base of the PR and between 7c8736c and 50a0125.

📒 Files selected for processing (8)
  • CMakeLists.txt
  • src/EditModeController.cpp
  • src/EditModeController.h
  • src/EditModeController_test.cpp
  • src/HalfEdgeMesh.cpp
  • src/HalfEdgeMesh.h
  • src/HalfEdgeMesh_test.cpp
  • src/mainwindow.cpp
✅ Files skipped from review due to trivial changes (2)
  • src/EditModeController.cpp
  • src/HalfEdgeMesh.cpp
🚧 Files skipped from review as they are similar to previous changes (2)
  • CMakeLists.txt
  • src/mainwindow.cpp

Comment thread src/EditModeController_test.cpp Outdated
Comment thread src/HalfEdgeMesh_test.cpp
Comment thread src/HalfEdgeMesh_test.cpp Outdated
Comment on lines +3281 to +3287
TEST(HalfEdgeMeshStandalone, SplitFaceRejectsInvalidVertexIndices) {
auto em = makeQuadMesh();
HalfEdgeMesh he;
ASSERT_TRUE(he.buildFromEditableMesh(em));
EXPECT_FALSE(he.splitFace(0, -1, 1));
EXPECT_FALSE(he.splitFace(0, 0, 999));
EXPECT_FALSE(he.splitFace(0, 2, 2)); // same vertex

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

This setup doesn't actually hit the invalid-vertex path.

makeQuadMesh() still builds two triangles, and splitFace is documented to reject triangles outright. These assertions can pass even if the invalid-vertex validation is broken, because triangle rejection short-circuits first.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/HalfEdgeMesh_test.cpp` around lines 3281 - 3287, The test uses
makeQuadMesh() which produces two triangles so splitFace's triangle-rejection
masks invalid-vertex checks; instead build an editable mesh containing a single
face with >=4 vertices (or otherwise ensure the face under test is not a
triangle) before calling he.buildFromEditableMesh(em), then call
he.splitFace(...) with out-of-range and identical vertex indices to exercise the
invalid-vertex-path; locate the setup that creates em (makeQuadMesh or the
EditableMesh construction) and replace it with a single-quad or n-gon creation
so splitFace(0, -1, 1), splitFace(0, 0, 999), and splitFace(0, 2, 2) actually
reach the invalid-vertex validation.

Three targeted test improvements from the second CodeRabbit review
pass. No product-code changes; just stricter / more portable tests.

- Resolve cube edges by vertex pair in KnifeCommit* tests instead of
  hard-coding HE edge indices (0 / 5). The edge numbering is an
  internal rebuild artefact and the hard-coded values also didn't
  guarantee the two clicks landed on a surface-connected path, so
  these tests could have flaked on a harmless topology-order change.
  New resolveEdgeByVerts() helper plus (2, 3) / (4, 5) top-face
  edges keeps the cut on a real coplanar region.

- SplitEdgeInterpolatesNormalAndUV now actually asserts UV. The
  existing test claimed to cover attribute lerp but only checked
  position and normal — a broken v.uv implementation would have
  passed silently. Adds the x+y=1 invariant (always true for
  v1=(1,0) / v2=(0,1) linear blends) and the distance-along-UV-
  segment ratio so both a zeroed UV and a wrong-parameter UV fail.

- SplitFaceRejectsInvalidVertexIndices clarified: documents that
  the invalid-vertex guards run BEFORE the triangle-rejection
  check (implementation order matters here), and adds a case for
  "valid vertex not on this face" — triangle 0 of makeQuadMesh
  contains verts {0,1,2}, so splitFace(0, 0, 3) hits the boundary-
  loop check specifically.

Tests: 111/111 HalfEdgeMeshStandalone still green. Controller E2E
suite compiles cleanly; the knife scenarios still skip on macOS
(Ogre headless) and run on Linux CI.
@sonarqubecloud

Copy link
Copy Markdown

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