Metadata-Version: 2.4
Name: vcti-shader-fringe
Version: 2.0.0
Summary: The fringe colormap shader feature: the fragment-stage math that turns a value into a colour.
Author: Visual Collaboration Technologies Inc.
License-Expression: LicenseRef-Proprietary
Project-URL: Repository, https://github.com/vcollab/vcti-python-shader-fringe
Project-URL: Changelog, https://github.com/vcollab/vcti-python-shader-fringe/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>=1.0.3
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

# vcti-shader-fringe

The fringe colormap shader feature: the fragment-stage math that turns a value into a colour.

## Overview

A fringe plot colours a 3D model by the values at its nodes and elements.
Turning each fragment's value into an RGBA colour is what `vcti-shader-fringe`
does.

A colormap here is a list of **bands**. Each band covers a value range and
carries the colours across it: one at each end, interpolated linearly or
logarithmically, or the same colour at both ends for a flat band. Bands for
below-range, above-range and missing values complete the list, so every value a
fragment can hold lands in one. Three more colours stand in when none does — the
value is NaN, it falls outside the list, or its band cannot be evaluated.

Which variant you build depends on the data. Continuous results — a displacement
magnitude, a von Mises stress — are floats and use `Colormap`. Categories — a
material id, a part number — are exact integers and use `DiscreteColormap`,
which keeps ids distinct up to 2³¹; carrying them as floats merges neighbouring
values above 2²⁴.

The package ships the Slang module a shader imports, the specs a build step
emits, and the same lookup written in Python — which the tests diff the shader
against, and which a legend renderer can call so its colours match the model.
Nothing here compiles or runs a shader.

## Installation

```bash
pip install vcti-shader-fringe
```

Requires Python 3.12, 3.13, or 3.14, matching `vcti-shader-base`, and so does
the `test` extra. Only the `gl` extra — the GL binding the shader tests need to
execute anything — is narrower in practice: on 3.14 it builds moderngl's
glcontext from source for want of a cp314 wheel, so those tests are run on 3.12
or 3.13.

### In `requirements.txt`

```
vcti-shader-fringe>=2.0.0
```

### In `pyproject.toml` dependencies

```toml
dependencies = [
    "vcti-shader-fringe>=2.0.0",
]
```

---

## Quick Start

### What the feature is

```python
from vcti.shader.fringe import DEFINITION, SLANG_DIR, ValueType, fragment_uniforms

DEFINITION.id            # 'fringe'
DEFINITION.role          # StageRole.FRAGMENT
DEFINITION.capabilities  # ('fringe-float', 'fringe-int') — one per variant
DEFINITION.slang_modules # ('colormap.slang',) — a shader does `import colormap;`
SLANG_DIR                # pass to the compiler as an import search path

[u.name for u in fragment_uniforms(ValueType.FLOAT)]
# ['u_bandBounds', 'u_numBands', 'u_lowerColors', 'u_upperColors',
#  'u_interpModes', 'u_interpSteps', 'u_nanColor', 'u_uncoveredColor',
#  'u_errorColor']

[u.name for u in fragment_uniforms(ValueType.INT)]
# ['u_bandBounds', 'u_numBands', 'u_bandColors', 'u_uncoveredColor']
```

### A continuous colormap

`linear_bands()` turns bounds and a palette into ramped bands;
`with_edge_bands()` frames them so the list covers the whole number line:

```python
from vcti.shader.fringe import Colormap, linear_bands, with_edge_bands

PALETTE = [(0.0, 0.26, 0.62, 1.0), (0.65, 0.84, 0.85, 1.0), (0.6, 0.0, 0.0, 1.0)]
GREY = (0.83, 0.83, 0.83, 1.0)

colormap = Colormap(
    with_edge_bands(
        linear_bands([0.0, 1.0, 2.0], PALETTE),   # one colour per bound
        below_color=PALETTE[0],
        above_color=PALETTE[-1],
        no_value_color=GREY,
        no_value_lower=1e30,
    )
)
colormap.validate()   # raises ValueError on a gap, overlap, or bad log bound
```

`constant_bands()` gives banded contours — one colour per band, so it takes one
fewer colour than `linear_bands()` does. A band is flat when both its colours
are the same, which is all `constant_bands()` builds.

### A discrete colormap

```python
from vcti.shader.fringe import DiscreteBand, DiscreteColormap, category_bands

materials = DiscreteColormap(category_bands([STEEL, ALUMINIUM, COPPER]))

by_id = DiscreteColormap((                 # raw solver ids, in ranges
    DiscreteBand(1_000_000, 2_000_000, STEEL),
    DiscreteBand(2_000_000, 3_000_000, ALUMINIUM),
))
```

### Looking up a colour

`fringe_color()` is the Python version of the shipped Slang lookup:

```python
from vcti.shader.fringe import fringe_color, fringe_color_int

# The colormap built above spans 0 to 2, ramping through PALETTE.
assert fringe_color(0.0, colormap) == PALETTE[0]        # the first bound exactly
assert fringe_color(1.0, colormap) == PALETTE[1]        # the middle bound
assert fringe_color(-1.0, colormap) == PALETTE[0]       # below it, from the edge band
assert fringe_color(3.402823466e38, colormap) == GREY   # the no-value sentinel
assert fringe_color(float("nan"), colormap) == colormap.nan_color

assert fringe_color_int(1_500_000, by_id) == STEEL      # the category path
```

Those are assertions rather than comments because `tests/test_docs.py` executes
every block on this page: a claim that stops holding fails the suite instead of
misleading a reader.

### Handing it to the GPU

```python
colormap.uniforms()
# {'u_bandBounds': [(-inf, 0.0), (0.0, 10.0), ...], 'u_numBands': 5, ...}
```

Arrays are padded to `MAX_BANDS`; the shader reads only the first `u_numBands`.

---

## Key API

| Name | What it is |
|---|---|
| `DEFINITION` | the `ShaderDefinition` the feature declares itself with |
| `SLANG_DIR` | the installed `slang/` directory — an `import` search path |
| `fragment_uniforms(kind)` | the uniforms a shader declares, per variant |
| `fragment_inputs(kind)` | the vertex attribute the integer variant reads |
| `fragment_outputs()` | the `fragColor` output the feature writes |
| `ValueType` | `FLOAT` or `INT` — which variant a shader is built for |
| `Band` | one half-open range, its two colours, and its interpolation mode |
| `Colormap` | a band list, three fallback colours, and an optional step count |
| `DiscreteBand` | one half-open range of integer categories and its colour |
| `DiscreteColormap` | a band list and the colour for a category none holds |
| `Colormap.validate()` | raises on a gap, an overlap, or a log band reaching zero |
| `Colormap.uniforms()` | the `{uniform: value}` mapping, padded to `MAX_BANDS` |
| `linear_bands()` | bounds + one colour each → ramped bands |
| `constant_bands()` | bounds + one colour per band → flat bands |
| `category_bands()` | one band per consecutive integer category |
| `with_edge_bands()` | frames authored bands with below/above/no-value |
| `fringe_color(value, colormap)` | the colour a fragment takes — same result as the shader |
| `fringe_color_int(value, colormap)` | the same, for integer categories |
| `band_color()`, `band_parameter()` | the same, for a single band already in hand |
| `InterpMode` | `LINEAR` or `LOG` |
| `INTERP_MODES` | the same as a name-to-number map, as published on the spec |
| `MAX_BANDS` | band-array capacity, shared with the Slang module |

`colormap.slang` publishes `applyFringe`, `applyFringeInt` and
`fringeBandColor`. All take every value as an argument — nothing in the module
reads a uniform, so a shipped shader and the test probes run the same code from
different sources.

---

## Dependencies

- [`vcti-shader-base`](https://github.com/vcollab/vcti-python-shader-base) — the
  `ShaderDefinition` record and the spec types, itself zero-dependency.

Nothing else at runtime. `vcti-shader-compiler` and `numpy` are **test-only**:
the tests run the shader on a GPU and compare it against the Python version,
but declaring the feature needs neither.

---

## Documentation

| If you want to… | Read |
|---|---|
| Get started using the package | Quick Start above |
| Build the colormaps a viewer actually ships | [docs/patterns.md](docs/patterns.md) |
| Understand the colour model and the decisions behind it | [docs/design.md](docs/design.md) |
| Navigate or modify the source | [docs/source-guide.md](docs/source-guide.md) |
