Metadata-Version: 2.4
Name: pyvista-trueform
Version: 0.2.0
Summary: PyVista accessor for trueform: exact mesh booleans, arrangements, intersection curves, and scalar-field cuts.
Author: XLAB
Author-email: Ziga Sajovic <info@polydera.com>
License: # PolyForm Noncommercial License 1.0.0
        
        <https://polyformproject.org/licenses/noncommercial/1.0.0>
        
        ## Acceptance
        
        In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
        
        ## Copyright License
        
        The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose.  However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
        
        ## Distribution License
        
        The licensor grants you an additional copyright license to distribute copies of the software.  Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
        
        ## Notices
        
        You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software.  For example:
        
        > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
        
        ## Changes and New Works License
        
        The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
        
        ## Patent License
        
        The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
        
        ## Noncommercial Purposes
        
        Any noncommercial purpose is a permitted purpose.
        
        ## Personal Uses
        
        Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose.
        
        ## Noncommercial Organizations
        
        Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding.
        
        ## Fair Use
        
        You may have "fair use" rights for the software under the law. These terms do not limit them.
        
        ## No Other Rights
        
        These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else.  These terms do not imply any other licenses.
        
        ## Patent Defense
        
        If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
        
        ## Violations
        
        The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice.  Otherwise, all your licenses end immediately.
        
        ## No Liability
        
        ***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
        
        ## Definitions
        
        The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
        
        **You** refers to the individual or entity agreeing to these terms.
        
        **Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization.  **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise.  Control can be direct or indirect.
        
        **Your licenses** are all the licenses granted to you for the software under these terms.
        
        **Use** means anything you do with the software requiring one of your licenses.
        
Project-URL: Homepage, https://trueform.polydera.com
Project-URL: Repository, https://github.com/polydera/pyvista-trueform
Project-URL: Issues, https://github.com/polydera/pyvista-trueform/issues
Keywords: pyvista,mesh,geometry,mesh-boolean,csg,computational-geometry
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: LICENSE.noncommercial
Requires-Dist: trueform>=0.10.3
Requires-Dist: pyvista>=0.48
Requires-Dist: numpy>=1.23
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Dynamic: license-file

![pyvista-trueform](https://raw.githubusercontent.com/polydera/pyvista-trueform/main/assets/header.png)

![Six overlapping spheres split into their domains by one csg_graph arrangement](https://raw.githubusercontent.com/polydera/pyvista-trueform/main/assets/hero_domains_multiblock.png)

Exact mesh booleans, intersection curves, self-intersection repair,
isocontours and isobands, and N-ary CSG on [PyVista](https://pyvista.org)
meshes. PyVista in, PyVista out, through one `.trueform` accessor on
`pyvista.PolyData`.

## Getting Started

```bash
pip install pyvista-trueform
```

**The accessor** registers itself on import and answers through
`.trueform` on any `pyvista.PolyData`:

```python
import pyvista as pv
import pyvista_trueform  # registers the accessor

pv.Cube().trueform.volume()
```

**The cache** is whole-value, keyed by the dataset's VTK modification
time — the held instance is what lets trueform's spatial tree, face
membership, and edge link build lazily once:

```python
import numpy as np

a = pv.Cube()
mesh = a.trueform.to_mesh()
assert a.trueform.to_mesh() is mesh  # same cached instance, MTime unchanged

np.asarray(a.points)[0] += 1.0       # raw mutation: MTime NOT bumped
a.Modified()                         # tell VTK, so the cache rebuilds
assert a.trueform.to_mesh() is not mesh
```

**A worked example** — a boolean carves the shape, `domains` reads the
pieces the arrangement made, and `pick` names the one a ray struck:

```python
import numpy as np
import pyvista as pv
import pyvista_trueform as tfpv
import trueform as tf

a = pv.Cube()
b = pv.Cube(center=(0.5, 0.5, 0.5))

carved = a.trueform.difference(b)
carved.cell_data["trueform_labels"]  # per-face source: 0 (a) or 1 (b)
blocks = tfpv.domains([a, b])        # every overlap pocket, its own block
ray = tf.Ray(origin=np.array([-2, 0, 0], dtype=np.float32),
            direction=np.array([1, 0, 0], dtype=np.float32))
hit = tfpv.pick(blocks, ray)
hit.block_index                      # which block the ray struck
```

**The contract** the package holds itself to — dialect laws, the full
surface, the gotchas — is one call away:

```python
print(tfpv.agents())  # the package contract, for agents and humans alike
```

The sections below cover the rest of the surface these three lean on.

### N-ary CSG

Build one arrangement of N operands and answer arbitrarily many boolean
expressions against it:

```python
from pyvista_trueform import csg_graph

c = pv.Cube(center=(0.25, 0.5, 0.0))  # quads, mixed ngons: normalized losslessly
graph = csg_graph([a, b, c])
graph.mesh(tf.op(0) - (tf.op(1) | tf.op(2)))

graph.domains()                       # every watertight piece, as a MultiBlock
graph.intersection_curves()           # the seams, as line PolyData
graph.outer_shell()                   # boundary of everything the operands enclose
graph.native                          # escape hatch: the raw trueform.CsgGraph

csg_graph([a])                        # one operand is legal: its own self arrangement
```

### Conversions

`to_trueform` and `to_pyvista` cross the boundary; only `to_trueform`
copies:

```python
from pyvista_trueform import to_trueform, to_pyvista, curves_to_pyvista

mesh = to_trueform(a)                        # detached trueform.Mesh, copies the geometry
to_pyvista(mesh)                             # zero-copy: shares mesh's own NumPy buffers
curves_to_pyvista(tf.boundary_curves(mesh))  # any (paths, points) pair, zero-copy
```

### IO

```python
from pyvista_trueform import read, write

write("cube.obj", a)                          # dispatched on suffix; .obj keeps a's own quad faces
read("cube.obj")                              # zero-copy conversion back to PolyData

write("cube.stl", a.trueform.triangulated())  # STL: triangles only
read("cube.stl")                              # STL welds duplicate vertices on the way in
```

### Domains and N-ary arrangements

```python
from pyvista_trueform import domains, mesh_arrangements, split_into_domains

domains([a, b, c])            # every watertight domain, named by id, as a MultiBlock
mesh_arrangements([a, b, c])  # the whole arrangement instead, as one labeled PolyData
a.trueform.domains()          # one mesh's own overlap pockets, from its self arrangement

arranged = mesh_arrangements([a, b, c])
split_into_domains(arranged, exclude_outer_shell=True)  # already cut: labeled and
                                                        # split, no second arrangement
```

### Picking

`pick` and `closest` answer over any MultiBlock (nested ones flatten; a
plain PolyData works too, as block 0):

```python
from pyvista_trueform import pick, closest

blocks = domains(csg_graph([a, b]))
ray = tf.Ray(origin=np.array([-2, 0, 0], dtype=np.float32),
            direction=np.array([1, 0, 0], dtype=np.float32))
pick(blocks, ray)                 # hit.block_index, hit.point: first block struck
closest(blocks, [0.0, 0.0, 0.0])  # same shape, by proximity instead of a ray
```

### Remesh

```python
tri = a.trueform.triangulated()

tri.trueform.remeshed(0.1)             # isotropic, target edge length
tri.trueform.decimated(0.5)            # quadric-error, target face proportion
tri.trueform.simplified()              # quadric-error, to an error budget instead

labels = np.zeros(tri.n_cells, dtype=np.int32)
remeshed = tri.trueform.remeshed(0.1, preserve_regions=labels)
remeshed.cell_data["trueform_labels"]  # preserve_regions rides back as cell data
```

### Queries

```python
a.trueform.volume()                  # .area(), .signed_volume(), .mean_edge_length() too
a.trueform.is_closed()               # .is_open(), .is_manifold(), .is_non_manifold(),
                                     # .euler_characteristic() too
a.trueform.distance(b)               # euclidean; .intersects(b) too
a.trueform.signed_distance(b)        # negative inside b; batched over a's own points
a.plot(scalars=a.trueform.signed_distance(b), cmap="polydera_div")  # packaged map, registered on import
a.trueform.closest_point([0, 0, 0])  # (face_id, distance, point)
a.trueform.closest_points([2, 0, 0], k=3)  # the k nearest, closest first
a.trueform.closest_point_pair(b)     # witness pair between a and b
a.trueform.principal_curvatures()    # (k0, k1); .shape_index() too

seg = tf.Segment(np.array([[2, 0, 0], [3, 0, 0]], dtype=np.float32))
a.trueform.distance(seg)             # queries take trueform primitives; .closest_point(seg) too

n, labels = a.trueform.connected_components()  # (n, per-face component labels)
a.trueform.split_components()        # one block per component, as a MultiBlock
a.trueform.boundary_curves()         # open edges, as line PolyData
a.trueform.boundary_edges()          # the dataset's own point ids instead; .boundary_paths(),
                                     # .non_manifold_edges(), .non_manifold_paths() too

ray = tf.Ray(origin=np.array([-2, 0, 0], dtype=np.float32),
            direction=np.array([1, 0, 0], dtype=np.float32))
a.trueform.ray_cast(ray)             # (face_id, t); config=(min_t, max_t) bounds it
```

All of these answer against the cached mesh, so the spatial tree
amortizes across calls.

### Registration

```python
from pyvista_trueform import (align_rigid, align_similarity, align_icp,
                             align_obb, align_knn, chamfer_distance)

align_rigid(a, b)       # Kabsch: rotation + translation, correspondence required
align_similarity(a, b)  # + uniform scale, same correspondence requirement
align_icp(a, b)         # iterative closest point, no correspondence needed
align_obb(a, b)         # oriented-bounding-box alignment, no correspondence
align_knn(a, b)         # one soft-correspondence step
chamfer_distance(a, b)  # one-way chamfer measure

matrix = align_rigid(a, b)                    # 4x4 delta: maps a's current points onto b
aligned = a.transform(matrix, inplace=False)  # apply to a itself to align it with b
```

### Lines and tubes

```python
from pyvista_trueform import connect_lines, tube

line = pv.Line((0, 0, 0), (1, 0, 0))
tube(line, radius=0.1)  # triangle tube; unordered 2-point segments connect first
connect_lines(line)     # just the assembly: segments in, polylines out, same point ids
```

### Generators

```python
tfpv.box(2.0, 1.0, 3.0)                                   # ticks subdivide each axis
tfpv.sphere(1.0, stacks=20, segments=20)
tfpv.cylinder(1.0, 2.0, segments=20)
tfpv.plane(10.0, 5.0)                                     # ticks subdivide each axis
tfpv.sphere(1.0, dtype=np.float64, index_dtype=np.int64)  # trueform's defaults apply when omitted
```

### Examples

![A sphere fractured by a grid of cutters in one csg_graph expression, read back as exploded domain chunks](https://raw.githubusercontent.com/polydera/pyvista-trueform/main/assets/hero_csg_fracture.png)

Seven standalone scripts in `examples/`, each with an importable
`compute()` and a plotting `main()` (`python examples/<name>.py`); the
gallery ships in the Polydera color scheme through the shared
`examples/_theme.py`:

- `boolean.py` — a boolean difference recut live under a dragged sphere
  widget, the result labeled by source.
- `csg_fracture.py` — a sphere minus a grid of cutters through one
  csg_graph expression, read back as volumetric chunks; clicking a chunk
  names it through `closest`.
- `isobands.py` — height isobands and isocontours overlaid on a
  surface, recut live from a band-count slider.
- `signed_distance.py` — a hills surface recolored live by signed
  distance to a dragged torus probe, diverging about the zero seam.
- `curvature.py` — Gaussian curvature across a torus and mean curvature
  across a hills surface, from one `principal_curvatures` call each.
- `remesh_and_simplify.py` — remeshed, decimated, and simplified side by
  side.
- `raycast_depth.py` — an orthographic depth image from one batched
  `tf.Ray` cast.

### Coming with trueform 0.11

Orientation verdicts (`orient_faces_consistently`,
`ensure_positive_orientation`) — the Python binding still returns only the
repaired faces array, no verdict yet.

## License

Dual-licensed:
- **Noncommercial**: [PolyForm Noncommercial License 1.0.0](./LICENSE.noncommercial)
- **Commercial**: Contact [info@polydera.com](mailto:info@polydera.com)
