Skip to content

Primitive Decomposition ​

Identifying irreducible operations vs. compositions across all domains.

Three-Layer Architecture ​

┌─────────────────────────────────────────────────────────┐
│  Layer 3: Pattern-Matching Optimizer                    │
│  - Recognizes compositions, applies algebraic rules     │
│  - Constant folding, dead code elimination              │
│  - Fuses operations for performance                     │
└─────────────────────────────────────────────────────────┘
                          ▲
                          │ input graphs
┌─────────────────────────────────────────────────────────┐
│  Layer 2: Ergonomic Helpers                             │
│  - Convenience functions that compose primitives        │
│  - blur(), grayscale(), Gravity, QuadIn                 │
│  - User-facing, domain-specific vocabulary              │
└─────────────────────────────────────────────────────────┘
                          │ expands to
                          ▼
┌─────────────────────────────────────────────────────────┐
│  Layer 1: True Primitives                               │
│  - Irreducible operations that cannot decompose further │
│  - Convolve, MapPixels, Integrate, RgbToHsl             │
│  - Minimal orthogonal basis for each domain             │
└─────────────────────────────────────────────────────────┘

What Makes a Primitive? ​

A true primitive is an operation that:

  1. Cannot be expressed as a composition of other operations in the domain
  2. Is not a trivial wrapper around another operation with default parameters
  3. Adds unique capability - removing it removes expressiveness

Examples ​

Primitive:

rust
// Convolve is primitive - it's the fundamental spatial operation
pub struct Convolve { pub kernel: Kernel }

Not primitive (composition):

rust
// Blur is Convolve with Gaussian kernel, applied multiple times
pub fn blur(radius: f32) -> impl Op {
    let kernel = Kernel::gaussian(radius);
    Convolve { kernel }.repeat(passes_for_radius(radius))
}

Not primitive (trivial wrapper):

rust
// Upsample is just Resize with 2x dimensions - deprecated
pub fn upsample(img: &Image) -> Image {
    Resize::new(img.width() * 2, img.height() * 2).apply(img)
}

Decomposition Patterns by Domain ​

Image Processing ​

LayerOperations
PrimitivesConvolve, MapPixels, RemapUv, Composite, Resize, FFT, IFFT
Helpersblur, sharpen, grayscale, invert, glow, bloom, vignette
Blur = Convolve { Gaussian } repeated
Sharpen = Convolve { sharpen_kernel }
Grayscale = MapPixels { luminance }
Glow = threshold + blur + composite

Particle Systems ​

LayerOperations
PrimitivesPositionProvider, VelocityProvider, Acceleration, Damping, NoisePerturbation
HelpersPointEmitter, SphereEmitter, Gravity, Wind, Turbulence
PointEmitter = FixedPosition + ConeVelocity + LifetimeRange
Gravity = Acceleration { (0, -9.81, 0) }
Turbulence = NoisePerturbation { curl_noise }

Physics Constraints ​

LayerOperations
PrimitivesPointConstraint, AxisAlignment, spring force, damping
HelpersDistanceConstraint, HingeConstraint, BallJoint

All constraints follow the same solver pattern:

  1. compute_error() - position/angular deviation
  2. apply_correction() - adjust bodies based on inverse mass

Easing Functions ​

LayerOperations
Primitives(none - all are dew expressions)
Helpersquad_in, cubic_out, elastic_in_out, smoothstep
quad_in(t) = t * t           // Mul(T, T)
cubic_in(t) = t * t * t      // Mul(Mul(T, T), T)
smoothstep(t) = t² * (3 - 2t)

Easing functions are not primitives - they're expression builders that return AST nodes the optimizer can fold.

IK Solvers ​

LayerOperations
PrimitivesSolveCcd, SolveFabrik
Helpers(none - these are already primitive)

IK algorithms are irreducible - CCD and FABRIK are different approaches to the same problem, not compositions.

Benefits of Decomposition ​

1. Smaller API Surface ​

Instead of 50 blur variants, expose one Convolve and let users compose.

2. Better Optimization ​

The optimizer works with primitives:

MapPixels(grayscale) + MapPixels(invert)
  → MapPixels(grayscale ∘ invert)  // Fused

3. Serialization Consistency ​

Primitives are the canonical form in saved graphs. Helpers expand at construction time.

4. Testing Coverage ​

Testing N primitives covers all their compositions. N helpers would require N² interaction tests.

Implementing Decomposition ​

Step 1: Identify Primitives ​

Look for operations that:

  • Introduce unique state transformations
  • Cannot be expressed via existing operations
  • Are required for domain completeness

Step 2: Add Op Structs ​

Every primitive needs an op struct:

rust
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "dynop", derive(Op))]
#[cfg_attr(feature = "dynop", op(input = Image, output = Image))]
pub struct Convolve {
    pub kernel: Kernel,
}

Step 3: Helpers Delegate ​

Helpers construct and apply primitives:

rust
pub fn blur(image: &Image, radius: f32) -> Image {
    let kernel = Kernel::gaussian(radius);
    Convolve { kernel }.apply(image)
}

Step 4: Document the Mapping ​

Add to docs/archive/decomposition-audit.md:

markdown
| Helper | Decomposes To |
|--------|---------------|
| blur | `Convolve { Gaussian }` repeated |

Trait-Based Unification ​

Where primitives share patterns, use traits:

rust
// All constraints follow same pattern
pub trait ConstraintSolver {
    fn compute_error(&self, bodies: &[RigidBody]) -> Option<ConstraintError>;
    fn apply_correction(&self, bodies: &mut [RigidBody], stiffness: f32);
}

// All emitters compose from providers
pub trait PositionProvider { fn sample(&self, rng: &mut Rng) -> Vec3; }
pub trait VelocityProvider { fn sample(&self, pos: Vec3, rng: &mut Rng) -> Vec3; }

This makes the primitive pattern explicit and enables custom implementations.

Complete Reference by Crate ​

unshape-image ​

Primitives (Layer 1):

StructPurpose
ConvolveSpatial filtering with arbitrary kernel
ResizeScale image to new dimensions
CompositeBlend images with blend modes
RemapUvArbitrary UV coordinate transformation
MapPixelsPer-pixel color transformation via ColorExpr

Helpers (Layer 2):

FunctionDecomposes To
blur()Convolve { gaussian_kernel }
sharpen()Convolve { sharpen_kernel }
grayscale()MapPixels { ColorExpr::grayscale() }
invert()MapPixels { ColorExpr::invert() }
adjust_brightness_contrast()MapPixels { ColorExpr::brightness_contrast() }
adjust_hsl()MapPixels { ColorExpr::hsl_adjust() }
color_matrix()MapPixels { ColorExpr::matrix() }
extract_channel()MapPixels { ColorExpr::Vec4 { ch, ch, ch, 1 } }
swap_channels()MapPixels { ColorExpr::Vec4 { swapped } }

unshape-field ​

Primitives (Layer 1):

StructPurpose
MapTransform field output
ZipCombine two fields into tuple
Zip3Combine three fields into tuple
FnFieldWrap a function as field
Scale, Translate, RotateInput coordinate transforms

Helpers (Layer 2):

FunctionDecomposes To
add(a, b)Zip(a, b).map(|(x, y)| x + y)
mul(a, b)Zip(a, b).map(|(x, y)| x * y)
sub(a, b)Zip(a, b).map(|(x, y)| x - y)
div(a, b)Zip(a, b).map(|(x, y)| x / y)
lerp(a, b, t)Zip3(a, b, t).map(|(a, b, t)| a*(1-t) + b*t)
mix(a, b, t)Alias for lerp

unshape-particle ​

Primitives (Layer 1):

Trait/StructPurpose
PositionProviderGenerate spawn positions
VelocityProviderGenerate initial velocities
LifetimeProviderGenerate lifetimes
AttributeProviderGenerate size/color attributes
IntegratorTime integration algorithm

Implementations:

  • FixedPosition, SpherePosition, BoxPosition
  • ConeVelocity, RadialVelocity
  • LifetimeRange, FixedAttributes
  • EulerIntegrator, SemiImplicitEulerIntegrator

Helpers (Layer 2):

StructDecomposes To
CompositeEmitter<P,V,L,A>Combines provider traits
PointEmitterCompositeEmitter<FixedPosition, ...>
SphereEmitterCompositeEmitter<SpherePosition, ...>

unshape-physics ​

Primitives (Layer 1):

Trait/StructPurpose
ConstraintSolverUnified constraint interface
PointConstraintFix point in world space
DistanceConstraintMaintain distance between bodies
HingeConstraintRotational joint

All constraints implement ConstraintSolver:

  • compute_error() → position/angular deviation
  • apply_correction() → adjust bodies
  • stiffness() → constraint strength

unshape-rig ​

Primitives (Layer 1):

StructPurpose
SolveCcdCCD IK algorithm
SolveFabrikFABRIK IK algorithm
AnimationClipKeyframe animation data

IK solvers are irreducible - different algorithms for the same problem.

unshape-expr-field (Easing) ​

Primitives (Layer 1): None - all easing uses dew's arithmetic primitives (+, *, pow, sin, cos).

Helpers (Layer 2):

FunctionReturns AST
quad_in(t)Mul(t, t)
cubic_in(t)Mul(Mul(t, t), t)
smoothstep(t)Mul(t², Sub(3, Mul(2, t)))

Easing functions are expression builders, not runtime computations.

See Also ​