Skip to content

fix: make clip_with_plane idempotent and deduplicate the kernel round trip - #22

Merged
lachlangrose merged 1 commit into
fix/implicit-cut-snap-and-diagnosticsfrom
fix/clip-plane-idempotence-dedup
Sep 28, 2026
Merged

lachlangrose merged 1 commit into
fix/implicit-cut-snap-and-diagnosticsfrom
fix/clip-plane-idempotence-dedup

Conversation

@lachlangrose

@lachlangrose lachlangrose commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

Stack 3 of 3. Based on #25, which is based on #24. Merge order: #24 → #25 → this.
A single commit; the wip: snapshot that used to sit here is gone — that work is now #25.

What & why

Fixes #14 ("BUG - long runtime/seg fault"), both halves.

PMP::clip is not idempotent. Re-clipping a mesh that already lies entirely inside the
halfspace removes no face, but still corefines against the plane and splits every edge
along the existing seam. On a 250-point surface, 60 identical clips grew it from 192 to
1,754,578 vertices — ~1.14x per clip, every new vertex exactly on the seam, with
nothing more than 1.4e-15 past the plane after the first clip. It happens with both
kernels, so it is not the exact round trip. That is the "long runtime" in the title.

That growth is also the generator for the segfault. convert_to_double_mesh rebuilt with
no deduplication, so surplus seam points rounded onto coincident doubles, and PMP::clip
degenerates on the next clip along an edge joining such a pair. convert_to_exact had the
same gap in the other direction: write_to_file/read_from_file bypass export_mesh's
dedup, so a reloaded mesh entered the exact kernel already carrying duplicates.

Three changes: an early-out in clipWithPlane when nothing lies outside the halfspace
(returning the documented 0 = no-op); dedup on exact double equality in both converters;
and the two converters folded into one templated rebuild_unique walk, since they differed
only in how a point is converted (net 42 lines out of meshutils.cpp).

How to verify

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build -j
pytest

76 tests before, 84 after. The 8 new ones in tests/test_clip_idempotence.py: 7 fail
without this change and pass with it. The 8th passes both ways by design — it is the
negative control proving the guard does not suppress legitimate clips.

Against the reported crash, both builds compiled in the same container from the same CGAL:

clip_055 (crashing mesh) clip_054 (control)
before rc=139 x3 rc=0 x2
after rc=0 x3 rc=0 x2

Note it does not reproduce on macOS/arm64 even with the crashing mesh — only the mesh
state is portable. This class of bug has to be checked on Linux/x86-64.

Downstream, replaying captured failing members through the real caller, 36 before/after
pairs: 30 segfaulted before, 0 after.

Risk

  • Kernel paths — covered both. The growth reproduces with use_exact_kernel true and
    false; the new tests are parametrised over both.
  • Geometry — not bit-identical, but geometrically identical. 40 random single clips
    produce byte-identical output. On real downstream data, two of three surfaces are
    byte-identical and the third has the same point count, cell count, unique point set,
    area to the last digit and zero bound shift — only PLY vertex ordering changes, because
    dedup changes insertion order.
  • Degenerate input — this is the fix for it; a sliver sweep is identical down to
    1e-11 thickness.
  • API — two behaviour changes, neither breaking a documented contract:
    1. a clip shaving less than ~1e-12 of the model extent is now declined rather than
      performed. Those calls already removed no faces — they returned a negative "faces
      removed" count, having only subdivided.
    2. clipWithPlane now raises ValueError on a degenerate (a=b=c=0) plane normal
      instead of proceeding.

Checklist

  • A test that fails without this change and passes with it
  • pytest passes locally
  • Both kernels covered where the change touches clipping or conversion

🤖 Generated with Claude Code

@lachlangrose lachlangrose changed the title Fix/clip plane idempotence dedup fix: make clip_with_plane idempotent and deduplicate the kernel round trip Sep 28, 2026
@lachlangrose
lachlangrose changed the base branch from main to fix/mesh-review-followups September 28, 2026 10:26
@lachlangrose
lachlangrose force-pushed the fix/clip-plane-idempotence-dedup branch from 78c1d48 to 661ab61 Compare September 28, 2026 11:20
@lachlangrose
lachlangrose changed the base branch from fix/mesh-review-followups to fix/implicit-cut-snap-and-diagnostics September 28, 2026 11:20
@lachlangrose
lachlangrose added this pull request to stack #26 September 28, 2026 11:58
@lachlangrose
lachlangrose force-pushed the fix/clip-plane-idempotence-dedup branch from 661ab61 to b3428d6 Compare September 28, 2026 11:59
… trip

Fixes #14 ("BUG - long runtime/seg fault"), both halves.

PMP::clip is not idempotent. Re-clipping a mesh that already lies entirely
inside the halfspace removes no face, but still corefines against the plane and
splits every edge along the existing seam. Measured on a 250-point surface, 60
identical clips grew it from 192 to 1,754,578 vertices -- ~1.14x per clip, every
new vertex exactly on the seam, with nothing more than 1.4e-15 past the plane
after the first clip. It happens with both kernels, so it is not the exact round
trip; this is the "long runtime" the issue title names.

That growth is also the generator for the segfault. convert_to_double_mesh
rebuilt with no deduplication, so the surplus seam points rounded onto coincident
doubles, and PMP::clip degenerates on the next clip along an edge joining such a
pair. convert_to_exact had the same gap in the other direction: write_to_file /
read_from_file bypass export_mesh's dedup, so a reloaded mesh entered the exact
kernel already carrying duplicates.

Three changes:

- clipWithPlane early-outs when no vertex lies more than 1e-12 * max|coord|
  outside the halfspace, returning the documented 0 = no-op and skipping the
  round trip entirely. It also now rejects a degenerate (a=b=c=0) normal.
- convert_to_exact and convert_to_double_mesh merge vertices on exact double
  equality -- lossless, because the two vertices are one point -- and drop faces
  that collapse to fewer than three distinct corners as a result.
- the two converters differed only in how a point is converted, so they are now
  one templated rebuild_unique walk (net 42 lines out of meshutils.cpp).

Verified against the reported failure: replaying the campaign's captured
clip_054 -> clip_055 pair, the unpatched build emits 6 coincident vertices then
7, and the patched build 0 then 0. The existing 76 tests pass unchanged, 40
random single clips are byte-identical to unpatched, and a sliver sweep is
identical down to 1e-11 thickness.

Behaviour change: a clip shaving less than ~1e-12 of the model extent is now
declined rather than performed. Those calls were already removing no faces --
they returned a negative "faces removed" count, having only subdivided.

Verified on Linux/x86-64, both commits built in the same container from the same
CGAL: before, the captured clip_055 mesh segfaults 3/3 (rc=139) while clip_054
exits clean 2/2; after, clip_055 exits clean 3/3. PyPI 0.1.8 in that container
reproduces the crash identically. Note it does NOT reproduce on macOS/arm64 even
with the crashing mesh -- only the mesh state is portable, so this class of bug
has to be checked on Linux.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lachlangrose
lachlangrose force-pushed the fix/clip-plane-idempotence-dedup branch from b3428d6 to b3acde1 Compare September 28, 2026 12:11
@lachlangrose
lachlangrose merged commit 6ac4e99 into main Sep 28, 2026
18 checks passed
@lachlangrose
lachlangrose deleted the fix/clip-plane-idempotence-dedup branch September 28, 2026 12:39
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.

BUG - long runtime/seg fault

1 participant