Repository: pyvista/pyvista

Issue:
Crinkle clip duplicates cells in the unclipped surface 
### Describe the bug, what's wrong, and what you expected.

When I implement the crinkle clip feature in pyvista.DataSetFilters.clip and set crinkle=True and return_clipped=True there are common cells between the clipped and unclipped datasets. I would expect that the unclipped surface would be effectively the original surface, without the clipped part. 

It would be great if this could be changed because I would like to continue clipping the unclipped surface again but without the duplicated cells at interface.

Thanks! 

### Steps to reproduce the bug.

```python
import pyvista as pv
import numpy as np

dataset = pv.examples.download_bunny_coarse()
clipped, unclipped = dataset.clip('y', return_clipped=True, crinkle=True)

clipped.save(".../bunny_top.vtk")
unclipped.save(".../bunny_bottom.vtk")
```

### System Information

```shell
--------------------------------------------------------------------------------
  Date: Thu Feb 22 15:43:29 2024 GMT Standard Time
            
                OS : Windows
            CPU(s) : 36
           Machine : AMD64
      Architecture : 64bit
               RAM : 255.7 GiB
       Environment : IPython
        GPU Vendor : NVIDIA Corporation
      GPU Renderer : Quadro RTX 4000/PCIe/SSE2
       GPU Version : 4.5.0 NVIDIA 536.67
  MathText Support : False
  Python 3.8.8 (default, Apr 13 2021, 15:08:03) [MSC v.1916 64 bit (AMD64)]
           pyvista : 0.42.2
               vtk : 9.2.6
             numpy : 1.20.1
        matplotlib : 3.3.4
            scooby : 0.7.2
             pooch : v1.7.0
            pillow : 8.2.0
           imageio : 2.9.0
             PyQt5 : 5.9.2
           IPython : 7.22.0
        ipywidgets : 7.6.3
             scipy : 1.6.2
              tqdm : 4.59.0
        jupyterlab : 3.0.14
      nest_asyncio : 1.5.1
  Intel(R) oneAPI Math Kernel Library Version 2021.2-Product Build 20210312
  for Intel(R) 64 architecture applications
--------------------------------------------------------------------------------
```


### Screenshots

![image](https://github.com/pyvista/pyvista/assets/46969157/b489e1fc-55be-442b-b2c6-aa30fe61d21c)


Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pyvista/pyvista/pull/6060?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 96.64%. Comparing base [(`05ac920`)](https://app.codecov.io/gh/pyvista/pyvista/commit/05ac920de42913f531bdc53049afe69c9e20df07?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista) to head [(`a00f0ec`)](https://app.codecov.io/gh/pyvista/pyvista/pull/6060?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #6060      +/-   ##
==========================================
- Coverage   96.96%   96.64%   -0.33%     
==========================================
  Files         140      140              
  Lines       24520    24520              
==========================================
- Hits        23776    23697      -79     
- Misses        744      823      +79     
```

</details>
MatthewFlamm: I am +1 for including this feature, and it makes sense based on the description of the parameters. But, are there also any use cases for returning the crinkle cut from both directions here (current behavior)?  I can't think of any obvious ones, but it seems probable that someone would want this behavior.
banesullivan: > are there also any use cases for returning the crinkle cut from both directions here (current behavior)? I can't think of any obvious ones, but it seems probable that someone would want this behavior.

I also cannot think of any and I think that when calling the clip function with crinkle enabled and wanting both sides of the clip, there should not be any overlap.

If someone wants to crinkle both sides on the overlap, then thats still possible, its just two seperate clips and can be done two ways:

1. By flipping the normal:

```py
a = dataset.clip('y', crinkle=True)
b = dataset.clip('-y', crinkle=True)
```

2. Using the `invert` option:

```py
a = dataset.clip('y', crinkle=True, invert=True)
b = dataset.clip('y', crinkle=True, invert=False)
```


```py
import pyvista as pv
from pyvista import examples

dataset = examples.download_bunny_coarse()
clipped, unclipped = dataset.clip('y', return_clipped=True, crinkle=True)

a = dataset.clip('y', crinkle=True)
b = dataset.clip('-y', crinkle=True)
# a = dataset.clip('y', crinkle=True, invert=True)
# b = dataset.clip('y', crinkle=True, invert=False)

assert (a == clipped)
assert not (b == unclipped)

pl = pv.Plotter(shape=(1,2))
pl.add_mesh(clipped, color='blue')
pl.add_mesh(unclipped, color='red', style='wireframe', line_width=5)
pl.subplot(0, 1)
pl.add_mesh(a, color='blue')
pl.add_mesh(b, color='red', style='wireframe', line_width=5)

pl.link_views()
pl.show()
```
![screenshot-3](https://github.com/pyvista/pyvista/assets/22067021/1f8e0941-8889-411b-958f-52f74f8cd0a7)


MatthewFlamm: I think it is fine as the PR has it.  There could be an unexpected breaking change for someone using the current behavior.  We might argue it was unintended behavior, so they should not have been using it?  I'm mostly trying to think through whether a 'breaking change' label should be applied, or this is just a bug fix.  Not trying to change the PR itself.

I thought of one case, two crinkle cuts that are offset in space are used in opposing directions to obtain a sliver of cells to compare cell size distribution (common in CFD for example).  Currently, a user could achieve this by either inverting one clip or using `return_clip=True` from one clip.
banesullivan: While in my opinion this is a bug and unintended behavior I think you provide a great example and I want to note that this bug has been around for 2 years (ref #2316). I think a breaking change label to denote that the output mesh is indeed different is appropriate here.

Patch:
diff --git a/pyvista/core/filters/data_set.py b/pyvista/core/filters/data_set.py
index b8d966bf..39aa957d 100644
--- a/pyvista/core/filters/data_set.py
+++ b/pyvista/core/filters/data_set.py
@@ -73,8 +73,10 @@ class DataSetFilters:
             a = _get_output(alg, oport=0)
             b = _get_output(alg, oport=1)
             if crinkle:
-                a = self.extract_cells(np.unique(a.cell_data['cell_ids']))
-                b = self.extract_cells(np.unique(b.cell_data['cell_ids']))
+                set_a = set(a.cell_data['cell_ids'])
+                set_b = set(b.cell_data['cell_ids']) - set_a
+                a = self.extract_cells(list(set_a))
+                b = self.extract_cells(list(set_b))
             return a, b
         clipped = _get_output(alg)
         if crinkle:


Test patch:
diff --git a/tests/core/test_dataset_filters.py b/tests/core/test_dataset_filters.py
index 7a88f221..d800e6df 100644
--- a/tests/core/test_dataset_filters.py
+++ b/tests/core/test_dataset_filters.py
@@ -91,11 +91,16 @@ def test_clip_filter(datasets):
                 assert isinstance(clp, pv.UnstructuredGrid)
 
     # crinkle clip
-    clp = pv.Wavelet().clip(normal=(1, 1, 1), crinkle=True)
+    mesh = pv.Wavelet()
+    clp = mesh.clip(normal=(1, 1, 1), crinkle=True)
     assert clp is not None
-    clp1, clp2 = pv.Wavelet().clip(normal=(1, 1, 1), return_clipped=True, crinkle=True)
+    clp1, clp2 = mesh.clip(normal=(1, 1, 1), return_clipped=True, crinkle=True)
     assert clp1 is not None
     assert clp2 is not None
+    set_a = set(clp1.cell_data['cell_ids'])
+    set_b = set(clp2.cell_data['cell_ids'])
+    assert set_a.isdisjoint(set_b)
+    assert set_a.union(set_b) == set(range(mesh.n_cells))
 
 
 @skip_mac