Metadata-Version: 2.4
Name: vcti-shader-clip
Version: 1.0.0
Summary: The clip shader feature: the fragment-stage test that discards the fragments lying on the far side of any client-supplied plane.
Author: Visual Collaboration Technologies Inc.
License-Expression: LicenseRef-Proprietary
Project-URL: Repository, https://github.com/vcollab/vcti-python-shader-clip
Project-URL: Changelog, https://github.com/vcollab/vcti-python-shader-clip/blob/main/CHANGELOG.md
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: <3.15,>=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: vcti-shader-base>=2.0.0
Provides-Extra: gl
Requires-Dist: vcti-shader-compiler[gl]>=4.0.0; extra == "gl"
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Requires-Dist: vcti-shader-compiler>=4.0.0; extra == "test"
Requires-Dist: numpy; extra == "test"
Provides-Extra: lint
Requires-Dist: ruff; extra == "lint"
Provides-Extra: typecheck
Requires-Dist: mypy; extra == "typecheck"
Dynamic: license-file

# Shader Clip

The clip shader feature: the fragment-stage test that discards the fragments lying on the far side of any client-supplied plane.

## Overview

A clip plane cuts a model open. The client names a plane; everything on the far
side of it stops being drawn, so the interior becomes visible without anything
being rebuilt. Moving the plane is a uniform update, not a change to geometry.

A **plane** is the four numbers of a plane equation — the unit normal in `xyz`,
the signed offset in `w` — and a fragment survives when it is on the near side
of every plane bound. One plane is a single cut, two facing planes a slab, six
a box; the feature applies whatever set is bound and names none of those
shapes. Binding **no** planes keeps everything, which is how clipping is turned
off without a second shader.

This package is the *declaration* of that feature: the Slang that performs the
test, a Python mirror of the same arithmetic, and the `ShaderDefinition` and
uniform specs saying what a build step must bind for it to work. It computes
nothing at import and draws nothing at runtime. Building a shader from the
declaration, and running it, belong elsewhere.

## Installation

```bash
pip install vcti-shader-clip
```

### In `requirements.txt`

```
vcti-shader-clip>=1.0.0
```

### In `pyproject.toml` dependencies

```toml
dependencies = [
    "vcti-shader-clip>=1.0.0",
]
```

---

## Quick Start

Build a plane, and ask what it keeps:

```python
from vcti.shader.clip import clip_keeps, plane_through

# The normal points into the half that survives, so this keeps z >= 0.
cut = plane_through(point=(0.0, 0.0, 0.0), normal=(0.0, 0.0, 1.0))

assert clip_keeps((0.0, 0.0, 5.0), [cut])
assert not clip_keeps((0.0, 0.0, -5.0), [cut])

# A point exactly on the plane is kept: the comparison is >= 0.
assert clip_keeps((0.0, 0.0, 0.0), [cut])
```

Two facing planes are a slab, and no planes at all keep everything:

```python
slab = [
    plane_through((0.0, 0.0, -1.0), (0.0, 0.0, 1.0)),
    plane_through((0.0, 0.0, 1.0), (0.0, 0.0, -1.0)),
]
assert clip_keeps((0.0, 0.0, 0.5), slab)
assert not clip_keeps((0.0, 0.0, 9.0), slab)

assert clip_keeps((0.0, 0.0, 9.0), [])
```

What a build step binds, and the values to bind:

```python
from vcti.shader.clip import DEFINITION, clip_uniforms, fragment_uniforms

assert DEFINITION.id == "clip"
assert [spec.name for spec in fragment_uniforms()] == ["u_clipPlanes", "u_clipPlaneCount"]

uniforms = clip_uniforms(slab)
assert uniforms["u_clipPlaneCount"] == 2
assert len(uniforms["u_clipPlanes"]) == 6  # padded to the declared length
```

In a composed fragment stage, the Slang side is one line. The module exports a
*predicate*; the layer composing features emits the `discard`, which is what
lets it place the test ahead of shading work a discard would waste:

```glsl
if (!clipKeeps(position, u_clipPlanes, u_clipPlaneCount)) { discard; }
```

**The one obligation a client can miss:** the planes must be expressed in
whatever space `position` arrives in. Binding world-space planes against
eye-space positions produces a picture that looks plausible and cuts in the
wrong place. See [docs/design.md](docs/design.md).

---

## Key API

| Name | What it is |
|---|---|
| `DEFINITION` | The `ShaderDefinition` — id `clip`, fragment stage, capability `clip` |
| `MAX_PLANES` | The fixed length of the uniform array — six, because six planes is a box |
| `SLANG_DIR` | Where `clip.slang` installed, for the compiler's import search path |
| `fragment_uniforms()` | The two `UniformSpec`s a build step binds |
| `Plane` | A plane as `(nx, ny, nz, offset)`, with `.normal` and `.flipped()` |
| `plane_through(point, normal)` | A plane through `point`, normal normalized |
| `clip_keeps(point, planes)` | Whether `point` survives them all — the CPU mirror |
| `plane_distance(plane, point)` | The signed distance; positive on the surviving side |
| `check_plane` / `check_planes` | The bind-time validation the shader does not do |
| `clip_uniforms(planes)` | The two uniform values, padded and validated |

---

## Dependencies

[`vcti-shader-base`](https://github.com/vcollab/vcti-python-shader-base) — the
zero-dependency `ShaderDefinition`, `StageRole` and `UniformSpec` types — and
nothing else at runtime. Declaring a feature is pure data.

`vcti-shader-compiler` and `numpy` are test-only: the suite compiles and runs
the shader to diff it against the Python mirror, but declaring the feature needs
neither.

---

## Documentation

| If you want to… | Read |
|---|---|
| Understand the architecture and design decisions | [docs/design.md](docs/design.md) |
| See practical, real-world usage | [docs/patterns.md](docs/patterns.md) |
| Navigate and understand the source | [docs/source-guide.md](docs/source-guide.md) |
