# PathVader

Boolean path operations for Rive — union, intersect, difference, and XOR (exclusive or) across multiple shapes, with the result output as a native Rive path you can style with fills, strokes, gradients, and feather.

## Overview

PathVader is a 5-script system that combines overlapping shapes using standard boolean operations. It captures the geometry of each shape, computes the boolean result using the PolyBool library (Modified Martinez sweep-line algorithm), classifies each result region's fill/hole status by testing against the original input shapes, and outputs the result with correct winding for Rive's clockwise fill renderer.

## Files

```
effects/path-vader/
  PathVaderCapture.luau    ← path-effect (add to each source shape)
  PathVaderRenderer.luau   ← node-script (computes boolean + classifies winding)
  PathVaderOutput.luau     ← path-effect (outputs result to a native shape)
  lib/
    PathVaderBus.luau      ← util (shared data channel with pre-classified winding)
    PolyBool.luau          ← util (polygon boolean engine, MIT)
  CHANGELOG.md             ← unified changelog for all scripts
  README.md                ← this file
```

## Setup

### Step 1: Add captures to source shapes

For each shape you want included in the boolean operation:

1. Select the shape's **fill** in the editor
2. Add `PathVaderCapture` as a path effect
3. Set `group` to the same string across all shapes (default: `"default"`)
4. Set `key` to a **unique** string per shape (e.g., `"circle1"`, `"rect1"`, `"blob"`)
5. Set `resolution` (1–5) for curve smoothness — higher = smoother but more points

The capture tessellates bezier curves into polylines (PolyBool operates on straight edges), transforms to world space, normalizes winding to CW, and publishes to the bus. The original shape is suppressed (empty path returned).

### Step 2: Add the renderer

1. Add a node (e.g., an empty group or shape) anywhere in the artboard
2. Add `PathVaderRenderer` as a node script on that node
3. Set `group` to match the captures
4. Set `mode` to the desired boolean operation

The renderer reads all captured polygons, computes the boolean result via PolyBool, classifies each result region's winding, and writes the pre-classified result back to the bus.

### Step 3: Choose your output method

#### Option A: Script-drawn (quick preview)

The renderer can draw the result directly:

- `showStroke = true` → draws the outline
- `showFill = true` → draws the filled boolean shape
- Adjust `strokeColor`, `strokeThickness`, `fillColor`

This is fast for previewing but limited to solid colors.

#### Option B: Native Rive shape (full styling)

For gradients, feather, multiple fills/strokes, blend modes:

1. Create a new shape (rectangle, ellipse, or custom — its geometry will be replaced)
2. Style it with whatever fills, strokes, gradients, feather you want
3. Add `PathVaderOutput` as a path effect on the shape's fill or stroke
4. Set `group` to match
5. The shape's original path is replaced with the boolean result each frame

The output reads pre-classified winding from the bus and emits CW/CCW contours accordingly. World-to-local transform is applied automatically — the output shape can be positioned anywhere.

## Boolean Modes

Set the `mode` input on PathVaderRenderer:

| Mode | Description | Fill rule |
|------|-------------|-----------|
| `"union"` | Merge all shapes into one solid shape | Fill where covered by ≥1 shape |
| `"intersect"` | Only the area where ALL shapes overlap | Fill where covered by all shapes |
| `"difference"` | First shape minus the rest | Fill where in first shape only |
| `"differenceRev"` | Rest minus the first shape | Fill where in any shape except first |
| `"xor"` | Exclusive or — toggle fill at each shape boundary | Fill where covered by an odd number of shapes |

For `difference` and `differenceRev`, the "first shape" is determined by bus key iteration order.

## Controls

### PathVaderCapture (per shape)

| Input | Type | Default | Description |
|-------|------|---------|-------------|
| `group` | string | `"default"` | Group identifier — must match renderer and output |
| `key` | string | `"shape1"` | Unique identifier for this shape within the group |
| `resolution_r1_5` | number | `3` | Curve smoothness: 1=fast (12 segs/curve), 5=ultra (60 segs/curve) |

### PathVaderRenderer (central computation)

| Input | Type | Default | Description |
|-------|------|---------|-------------|
| `group` | string | `"default"` | Group identifier |
| `mode` | string | `"union"` | Boolean operation: union, intersect, difference, differenceRev, xor |
| `showStroke` | boolean | `true` | Draw stroke outline (script-drawn) |
| `showFill` | boolean | `false` | Draw filled shape (script-drawn) |
| `strokeColor` | Color | white | Stroke color |
| `strokeThickness_r1_100` | number | `2` | Stroke width |
| `fillColor` | Color | gray | Fill color |
| `debug` | boolean | `false` | Log polygon counts, result regions, areas, and windings |

### PathVaderOutput (native shape output)

| Input | Type | Default | Description |
|-------|------|---------|-------------|
| `group` | string | `"default"` | Group identifier — must match renderer |
| `debug` | boolean | `false` | Log region/point counts |

## Resolution Guide

The `resolution` control on each capture determines how many straight segments approximate each bezier curve. Higher values produce smoother output at the cost of more computation.

| Resolution | Segments per cubic | Best for |
|---|---|---|
| 1 | 12 | Quick preview, small shapes, rectangles |
| 2 | 24 | Small/medium shapes |
| 3 | 36 | Default — smooth for most shapes |
| 4 | 48 | Large curves, high-quality output |
| 5 | 60 | Maximum smoothness, sub-pixel segments |

At resolution 5, each segment on a 100px curve is ~1.6px — visually indistinguishable from a true curve.

## How Winding Classification Works

PolyBool computes boolean operations correctly but outputs regions with arbitrary winding direction and no outer/hole metadata. Rive's `FillRule::clockwise` renderer fills regions with positive winding only:

- CW contour → winding +1 → **fills**
- CCW contour inside CW → winding +1-1 = 0 → **hole**
- Standalone CCW → winding -1 → **invisible**

PathVader classifies each result region using the mathematical definition of the boolean operation:

1. **Get guaranteed interior point** — edge midpoint offset inward along the edge normal (not centroid, which can be outside concave polygons). The offset direction is computed from the polygon's signed area and the screen coordinate convention (Y-down).

2. **Test against original inputs** — count how many original input polygons contain the interior point using ray-casting point-in-polygon.

3. **Apply boolean rule:**
   - **XOR**: odd count → CW (fill), even count → CCW (hole)
   - **Union**: count ≥ 1 → CW (fill)
   - **Intersect**: count = all inputs → CW (fill), else CCW
   - **Difference**: in primary only → CW, else CCW

4. **Emit contours** — CW regions are emitted forward, CCW regions reversed. All contours go into the same Path. Rive's clockwise fill handles the rest: CW fills, CCW subtracts.

This approach uses zero heuristics — it directly implements the mathematical definition of each boolean operation.

## Architecture

```
┌─────────────────────┐     ┌─────────────────────┐
│ Shape A             │     │ Shape B             │
│ ┌─────────────────┐ │     │ ┌─────────────────┐ │
│ │ PathVaderCapture│ │     │ │ PathVaderCapture│ │ 
│ │ key="shapeA"    │ │     │ │ key="shapeB"    │ │
│ └────────┬────────┘ │     │ └────────┬────────┘ │
└──────────┼──────────┘     └──────────┼──────────┘
           │                           │
           ▼                           ▼
    ┌──────────────────────────────────────┐
    │         PathVaderBus (util)          │
    │  Stores world-space polygons per key │
    └──────────────────┬───────────────────┘
                       │
                       ▼
    ┌──────────────────────────────────────┐
    │     PathVaderRenderer (node-script)  │
    │  1. PolyBool boolean operation       │
    │  2. Classify regions vs inputs       │
    │  3. Write {regions, windings} to bus │
    │  4. Optionally draw stroke/fill      │
    └──────────────────┬───────────────────┘
                       │
                       ▼
    ┌──────────────────────────────────────┐
    │      PathVaderBus (result)           │
    │  {regions, windings} per group       │
    └──────────────────┬───────────────────┘
                       │
                       ▼
    ┌──────────────────────────────────────┐
    │   Output Shape with native styling   │
    │ ┌──────────────────────────────────┐ │
    │ │ PathVaderOutput (path-effect)    │ │
    │ │ Reads pre-classified windings    │ │
    │ │ Emits CW/CCW per region          │ │
    │ └──────────────────────────────────┘ │
    │ Fill: gradient, solid, feather, etc. │
    │ Stroke: any width, cap, join, etc.   │
    └──────────────────────────────────────┘
```

## Coordinate Spaces

- **Captures** transform local path points to **world (artboard) space** using the node's `worldTransform`
- **Renderer** computes boolean and classification in world space, offsets by its own position for script drawing
- **Output** transforms from world space to the output shape's **local space** using the inverse of its `worldTransform`

All shapes can be at different positions, rotations, and scales — the system handles coordinate transforms automatically.

## Technical Details

### Why tessellation?

PolyBool (and all polygon boolean algorithms) operate on straight-edge polygons, not bezier curves. Curves are tessellated into polylines by PathVaderCapture. At resolution 3 (default), each cubic bezier becomes 36 line segments. Straight edges are subdivided every ~10px for accurate intersection detection.

### Why not standard nonZero fill?

Rive's `FillRule::clockwise` fills positive winding only (confirmed from C++ GPU renderer source: "Clockwise only fills in forward triangles"). This is different from standard nonZero (which fills any non-zero winding). A standalone CCW contour has winding -1, which is non-zero but NOT positive — it doesn't fill. This is why winding classification is essential: every result region must be intentionally CW (fill) or CCW (hole inside a CW region).

### Rive's hole mechanism

A CCW contour **inside** a CW contour creates winding +1-1=0 at the overlap → the overlap doesn't fill → visible hole. Multiple contours in the same Path combine their winding numbers automatically via the GPU triangulator's sweep-line.

## Credits

- **PolyBool** — polygon boolean engine ported from [EgoMoose/PolyBool-Lua](https://github.com/EgoMoose/PolyBool-Lua) (MIT License), originally [polybooljs](https://github.com/voidqk/polybooljs) by Sean Connelly (MIT License)
- **PathVader** — Copyright (c) 2026 IVG Design. All rights reserved. Free to use, copy, modify, and distribute.
