Tested in Rhino 7.38, Rhino 8.35 and Rhino 9.0.26251.12303 (Beta), Windows x64. Unless noted, the input meshes are Brep boxes converted with Mesh.CreateFromBrep using MeshingParameters.Coarse (6 quads per box). The attached script runs unmodified in all three versions and judges every result against the geometrically correct answer, never against another Rhino version.
What happens
In Rhino 9 Beta, Mesh.CreateBooleanSplit / _MeshBooleanSplit returns a wrong result whenever the cutter shares a box face with the target. It gives no warning. In the main case (same normal orientation):
- The outer piece keeps the full shared box faces of the target, untrimmed. It is
IsClosed=Truebut has non-manifold edges. - The inner piece consists only of the cutter’s non-shared box faces. It has no mesh faces on the shared box faces, so it is an open shell.
CreateBooleanDifference and CreateBooleanIntersection on the same input are correct in Rhino 9 Beta. The same Split cases (T01–T06) are correct in Rhino 7. With a single shared face, Split is also correct in Rhino 8, so that case is a regression in Rhino 9.
The report focuses on the Split regression in Rhino 9. The extended table below also shows cases where Rhino 7 or Rhino 8 are wrong (T09, T12, T13, T16–T18) and cases where all three versions fail (T17); those are noted for context but are not the subject of this report.
Reproduction
- A = mesh box
(-5,-5,-5)..(5,5,5), B = mesh box(0,0,0)..(5,5,5). B is inside A and shares its +X, +Y and +Z box faces with A. Mesh.CreateBooleanSplit([A], [B]), or_MeshBooleanSplitwith A as the object to split and B as the cutter.- Check both pieces for closedness, naked and non-manifold edges, and the mesh face area on each box face.
The attached script builds all the geometry itself, runs every case below and prints CORRECT / WRONG for Split, Difference and Intersection. It also leaves inputs and results in the document for inspection.
Measurement
Main case in Rhino 9 Beta: mesh face area on A’s +X box face (x = 5). The +Y and +Z box faces give identical results. Outer piece: 54 vertices, 27 mesh faces. Inner piece: 12 vertices, 6 mesh faces, all on B’s box faces x=0, y=0, z=0.
Split outer Split inner Difference Intersection
area on +X @ x=5 100.00 0.00 75.00 25.00
closed True False True True
naked edges 0 6 (len 30) 0 0
non-manifold edges 6 (len 30) 0 0 0
volume (1000) (62.5) 875 125
Expected: two closed pieces, 875 and 125. The six non-manifold edges of the outer piece and the six naked edges of the inner piece are the same line: the inner boundary of the three 5×5 patches, total length 30. The volumes of the Split pieces are shown in brackets because they are not meaningful for a non-manifold or open mesh.
Results across versions (Split / Difference / Intersection, C = correct, W = wrong). Only tests that run in all three versions are listed; the MeshBooleanOptions tests (GT01–GT06) are described separately, because Rhino 7 does not have that API.
case Rhino 7.38 Rhino 8.35 Rhino 9 Beta
T01 B=(0..5)^3, 3 shared faces C C C W W W W C C
T02 B=(0,0,0)..(4,4,5), 1 shared face C C C C C C W C C
T03 B=(0,0,0)..(4,5,5), 2 shared faces C C C W W C W C C
T04 B=(0,0,0)..(5,5,3), 2 shared faces C C C W W W W C C
T05 B=(-2,-2,0)..(2,2,5), patch mid-face C C C C C C W C C
T06 B=(0,0,0)..(5,5,8), passes through z=5 C C C W W W W C C
T07 roles swapped, cutter A (nothing to cut) W W C W W W W C C
T08 B touches A from outside (nothing to cut) W C W W W W W C C
T09 B=(-1..1)^3 strictly inside A, no shared W W W C C C C C C
T10 script-built boxes, double, welded C C C W W W W C C
T11 script-built boxes, double, unwelded C C C W W W W C C
T12 triangulated boxes W W C W W W W C C
T13 subdivided, vertices aligned W W W W W W W C C
T14 subdivided, vertices not aligned C C C W W W W C C
T15 rotated 90 deg (exact) C C C W W W W C C
T16 double precision, rotated 37 deg W W W W W W W C C
T17 single precision (CreateFromBrep), rot 37 W W W W W W W W W
T18 CreateFromBrep + UseDoublePrecision, rot 37 W W W W W W W C C
G01 d = 2e-09 C C C W W W W C C
G02 d = 3e-09 C C C W W W C C C
G03 d = 5e-09 C C C W W W C C C
G04 d = 1e-08 C C C W W W W W W
G05 d = 3e-08 C C C W W W W W W
G06 d = 7e-08 C C C W W W W W W
G07 d = 1e-07 C C C C C C C C C
G08 d = 1e-06 C C C C C C C C C
G09 d = 0.001 C C C C C C C C C
27 tests × 3 operations = 81 results. The Rhino 7 log reports 60 of 81 operations correct.
Rows T07–T08 are the two edge cases that belong to the same family as the main case (nothing to cut). They are wrong in all three versions, with different failure modes in each. Row T09 (control, no shared faces) is wrong only in Rhino 7; it is correct in Rhino 8 and Rhino 9. In rows T12, T13, T16 and T18 the Rhino 7 and Rhino 8 failures are unrelated to this report; the Rhino 9 column shows the same Split bug as the main case. T17 is different: all three operations fail in all three versions; it is discussed under Separately.
In Rhino 9 Beta, Split is wrong in every tested case with a shared face (T01–T08, T10–T18 and G01), and Difference and Intersection are correct in all of them, except the single-precision rotated case described under Separately (T17). This holds for welded and unwelded boxes, single- and double-precision vertices, triangulated and subdivided meshes (aligned and non-aligned vertices), and rotations of 90° and 37°. The results are deterministic (three identical runs). On the main case, _-MeshBooleanSplit gives the same pieces as the API (same vertex and mesh face counts, topology and volumes).
In Rhino 8, Split returns the same pieces as Difference + Intersection in most tested cases — but not in T13 (subdivided, aligned), T14 (subdivided, non-aligned), T16 (rotated 37°), nor in GT02 and GT04 among the tolerance tests. Every case with shared faces except T02 and T05 has at least one wrong operation. In Rhino 9, Difference and Intersection were fixed, but Split was not.
Two related symptoms in Rhino 9 Beta, probably from the same cause (Difference and Intersection are correct in both):
- Target inside the cutter (split B = (0..5)^3 by A, 3 shared faces): nothing should be cut. Split returns B as two open shells of 6 mesh faces each: its 3 box faces lying on A’s boundary, and its 3 interior box faces.
- Opposite orientation (B = (5,0,0)..(10,5,5) touching A’s +X box face from outside): Split returns A correctly, plus a spurious zero-volume piece at x = 5 — V8 F4 in Rhino 9, i.e. two back-to-back 5×5 patches, triangulated.
In Rhino 7 these two cases return the correct geometry plus an empty mesh (V0 F0) appended to the result. The geometry is right; the empty mesh is cosmetic. In Rhino 8 both cases return open, wrong meshes. For the opposite-orientation case (T08), the Rhino 9 behaviour is closest to Rhino 7 — correct geometry, spurious extra piece — but the extra piece now has volume 0 and 4 mesh faces instead of being empty. For the target-inside-cutter case (T07), Rhino 9 returns B split into two open shells, so the geometry is not correct.
Possible lead
- Where the patch goes. A marker vertex on A’s shared box face shows that Rhino 9’s Split puts A’s mesh faces of the patch into the outer piece, and B’s coincident box face is discarded. Intersection keeps the same A mesh faces but puts them in the correct (inner) piece, and so does Rhino 7 Split. In Rhino 8, the same marker ends up in both pieces (
piece0=True piece1=True), which is a third, also wrong, behaviour. - What triggers it. The coincidence test itself. With
B = (0..5-d)^3, Split fails for every d belowMeshBooleanOptions.Toleranceand is correct above it. This was tested for t = 1e-12, 2.3283064365386963e-09 (the default), 1e-6, 1e-4 and 1e-2, in both Rhino 8 and Rhino 9. At d = t the result varies.
This suggests that the Split path classifies patches lying on the cutter’s boundary as “outside”, instead of routing them the way Difference and Intersection do. This is an inference from measurements only.
Expected
Split in Rhino 9 should return the same two pieces as Difference + Intersection, as it does in Rhino 7 for all T01–T06 cases and in Rhino 8 for T02 and T05.
Workaround
In Rhino 9 Beta, CreateBooleanDifference + CreateBooleanIntersection give the correct result in every tested case with shared faces, except the single-precision rotated case described under Separately.
Separately
Nearly coincident faces break all three mesh booleans in Rhino 8 and Rhino 9, but not in Rhino 7. With B = (0..5+d)^3 protruding from A by d, the behaviour is not monotone in d (see G01–G09 above). These gap tests (G01–G09, GT01–GT05) and the tolerance test use script-built double-precision boxes, because single-precision vertices cannot store gaps this small.
- d = 2e-9: Rhino 9 Split wrong, Difference/Intersection correct.
- d = 3e-9, 5e-9: all three correct in Rhino 9.
- d = 1e-8 … 7e-8: all three return open meshes in Rhino 8 and Rhino 9.
- d ≥ 1e-7: all three correct in every version.
Rhino 7 is correct in this band, but is wrong in T09, T12, T13 and T16–T18 (see table).
In Rhino 9, MeshBooleanOptions.Tolerance = 1e-6 makes these gaps count as coincident. Difference and Intersection are then correct, while Split shows the bug above. This is shown by GT01–GT06, which are only run in Rhino 8 and Rhino 9 because Rhino 7 has no MeshBooleanOptions API:
case Rhino 8.35 Rhino 9 Beta
GT01 d = 2e-09, Tolerance = 1e-6 W W W W C C
GT02 d = 1e-08, Tolerance = 1e-6 W W W W C C
GT03 d = 3e-08, Tolerance = 1e-6 W W W W C C
GT04 d = 7e-08, Tolerance = 1e-6 W W W W C C
GT05 d = 1e-07, Tolerance = 1e-6 W W W W C C
GT06 single precision rot 37, Tolerance = 1e-6 W W W W C C
This case is easy to hit in practice. Meshes from Mesh.CreateFromBrep have single-precision vertices, and rotating the main case by 37° moves the vertices by about 2.5e-7. After that, all three operations fail in all three versions (T17). In Rhino 9, setting UseDoublePrecisionVertices = True before the transform makes Difference and Intersection correct again — but not Split (T18: Split W, Diff/Int C).
Possibly related: RH-87453 (MeshBooleanIntersection: Results in Open Mesh), from this thread.
Python script: MeshBooleanSplit_coincident_faces.py (24.5 KB)