Mesh cutting logic

When two fault meshes intersect or nearly intersect, one mesh is cut by the other so that their geometries are consistent — the lower-priority fault terminates at the surface of the higher-priority fault. This page describes how the code decides whether a cut is needed and, once it is, which half of the cut mesh to keep.

Deciding whether to cut

The entry point is FaultMesh.decide_whether_to_cut(self, other_mesh, ...).

Step 1: check for geometric intersection

The method first calls get_intersecting_triangles(other_mesh) to find triangles in self that intersect or come within a threshold distance of other_mesh. If no such triangles exist the meshes do not interact and the method returns False immediately — no cut is performed.

Step 2: override by higher-priority faults

If a list of higher_meshes is supplied, the code checks whether a higher-priority fault has already separated self and other_mesh onto opposite sides of itself. For each higher-priority mesh:

  1. Both self and other_mesh must intersect the higher-priority mesh (otherwise the check is skipped for that pair).

  2. The signed implicit distances from every vertex of self and other_mesh to the higher-priority mesh surface are computed.

  3. If the two faults lie predominantly on opposite sides of the higher-priority mesh (one is mainly positive, the other mainly negative) and neither fault has significant penetration beyond the higher-priority surface (i.e. neither has many vertices more than higher_mesh_tolerance on the wrong side), then the higher-priority fault is already acting as a separator. In this situation, cutting self against other_mesh is unnecessary and the method returns False.

Step 3: edge-intersection test (always active)

The intersecting triangles are compared against the four edge groups of self (top, bottom, left, right — identified by depth and by the largest spatial gap in the side vertices). If the intersection zone touches two or more distinct edges the intersection genuinely crosses the mesh, and the method returns True.

This test catches the common case where one fault clearly cuts through another from one edge to another (e.g. top to bottom, or left to right).

Step 4: fancy cutting heuristics (optional)

When fancy_cutting=True and the simple edge test did not fire, additional heuristics are applied.

  1. Find the two most widely separated intersection-triangle centres. These are the centres of the intersecting triangles that are furthest apart from each other (max_distance). For large intersection zones the convex-hull method is used for efficiency; for small zones brute-force pairwise distances are used.

  2. Identify the closest mesh edge for each centre. Each of the two extreme centres is matched to its nearest mesh edge (top, bottom, left, or right) by minimum Euclidean distance to the edge vertices.

  3. Decision tree:

    • Centres are closest to different edges — the intersection zone spans two different edges, which strongly suggests a genuine through-cutting geometry. The ratio max_distance / edge_dist is compared to threshold (default 0.9), where edge_dist is the distance between the closest edge points to each centre. If the ratio exceeds the threshold, cut; otherwise do not.

    • Centres are closest to the same edge, and that edge is short (edge_length < min_distance) — the intersection is near a short edge. The ratio max_distance / edge_length is compared to threshold. If it exceeds the threshold, cut.

    • Centres are closest to the same edge, and that edge is long:

      • If both centres are very close to that edge (within 3 km), the intersection zone hugs a single long edge. Cut if edge_dist > min_distance or edge_dist / edge_length > 0.5; otherwise do not cut.

      • If only one centre is very close to the edge, use the second-closest edge for that centre instead and re-compute edge_dist. Apply the threshold test on max_distance / edge_dist_second.

      • If neither centre is close to any edge, do not cut.

    If fancy_cutting=False (the default), the method returns False after the edge test.


Choosing which side to keep after cutting

Once a cut is decided, FaultMesh.cut_mesh (or cut_mesh_pv) clips self against other_mesh in both directions using PyVista’s clip_surface:

  • clipped1 — the portion of self that lies on the outside of other_mesh (invert=False)

  • clipped2 — the portion of self that lies on the inside of other_mesh (invert=True)

Degenerate cases

If either clipped half has no surface points (points with z ≥ −surface_tolerance, i.e. near the Earth’s surface), it is discarded immediately and the other half is kept. This handles cases where the clip removes essentially all of one side.

General case: proximity to the fault trace

In the normal case both halves have surface points. A KD-tree is built from the supplied fault_trace — the 3-D polyline of the fault’s surface expression.

cut_mesh counts, for each half, how many surface points lie within surface_max_dist_tolerance of the trace. The half with more nearby points is kept. This reflects the expectation that the correct side of the cut mesh should have its surface expression close to the mapped fault trace.

cut_mesh_pv uses a slightly different criterion: it keeps the half whose surface points have the lower mean distance to the fault trace, which is more robust when the two halves have different numbers of surface points.

Safety check

If the retained half has fewer than 3 points or 1 cell (i.e. the clip produced an essentially empty mesh), the original uncut mesh is returned unchanged with a warning.