How to Implement Pinned Constraints in ppf-contact-solver: Vertex Groups and Animation Methods

You implement pinned constraints in ppf-contact-solver by instantiating a PinHolder—via scene.add().pin() or Group.create_pin()—chaining transformation operations like move_by() or spin(), and targeting either mesh vertex groups or curve control-point indices, which the Rust kernel then validates and serializes to TOML.

The st-tech/ppf-contact-solver provides a robust Python front-end and Blender integration for creating pinned constraints that animate specific vertices during physics simulation. Understanding how to implement pinned constraints requires knowledge of the PinHolder builder pattern and the specific vertex group types supported by the solver's Rust back-end.

Understanding the Pinned Constraint Architecture

The constraint system revolves around three core abstractions defined in frontend/_scene_pin_.py.

PinHolder and PinData

The PinHolder class (lines 35–46 in frontend/_scene_pin_.py) acts as the high-level builder that records pinning operations on a set of vertex indices. It mirrors a Rust validator and emits a TOML description for the solver during scene compilation.

The PinData dataclass stores:

  • The target vertex indices
  • A list of Operation objects
  • Optional metadata including unpin_time, pull_strength, and pin_group_id

Operation Classes

All transformations inherit from the abstract Operation class (lines 40–62). Concrete implementations include:

  • MoveByOperation
  • MoveToOperation
  • SpinOperation
  • ScaleOperation
  • TorqueOperation
  • TransformKeyframeOperation

Each operation contains parameters for the transformation and forwards work to the Rust kernel via _rust.scene_…_apply (lines 64–226).

How to Implement Pinned Constraints

Step 1: Create a Pin Holder

You can obtain a PinHolder through two primary APIs according to your environment.

From the pure-Python front-end:

from ppf_contact_solver.frontend import Scene

scene = Scene()
pin = scene.add("sphere").pin()  # Pins all vertices of the object

From the Blender add-on:


# From blender_addon/ops/api/group.py lines 47-78

group = solver.create_group("Cloth", type="SHELL")
pin = group.create_pin("ClothMesh", "collar")  # Pins specific vertex group

Step 2: Chain Animation Operations

Add concrete operations to PinData.operations using fluent method calls. Each call invokes the same operation on the Rust mirror (self._rust) for validation.


# Move pinned vertices +8 units on X between t=0 and t=5

pin.move_by([8, 0, 0], t_start=0.0, t_end=5.0)

# Spin around Y-axis at 180°/s from t=5 onward

pin.spin(
    center=None,
    axis=[0, 1, 0],
    angular_velocity=180.0,
    t_start=5.0,
    t_end=float("inf"),
)

Step 3: Configure Metadata

Set optional constraint properties before serialization:

pin.pull(strength=2.0)        # Increase pull force

pin.unpin(time=12.0)         # Release at t=12s

pin.interp("bezier")         # Set easing for subsequent ops

Step 4: Serialization

When the scene compiles, _pin_to_toml_dict walks the PinData objects, converts operations via _pin_op_to_toml_dict, and passes the dictionary to _rust.scene_format_pin_toml for solver ingestion.

Supported Vertex Groups

The Group.create_pin method in blender_addon/ops/api/group.py accepts two distinct target types.

Mesh Vertex Groups

For mesh objects, provide the name of an existing vertex group from the mesh's vertex_groups collection.


# The "Collar" group must exist on the mesh before calling

group.create_pin("MyMesh", "Collar")

Curve Control-Point Groups

For curve objects, the system uses custom properties to store pinning indices.

Method A: Supply indices directly (creates the property automatically):

group.create_pin("MyCurve", "left", indices=[0, 7, 14])

# Creates custom property "_pin_left" containing JSON array

Method B: Pre-populate the custom property:


# Ensure obj["_pin_left"] exists with JSON array of integers

group.create_pin("MyCurve", "left")

Complete Implementation Examples

Python-Only Script

from ppf_contact_solver.frontend import Scene

scene = Scene()
obj = scene.add("cube")
pin = obj.pin()

# Animate: translate 4m on Z, then scale to 0.5

pin.move_by([0, 0, 4], t_start=0.0, t_end=2.0) \
   .scale(0.5, t_start=2.0, t_end=5.0)

scene.save("my_simulation.toml")

Blender Operator

import bpy

solver = bpy.context.scene.zozo_contact_solver

# Create dynamics group

cloth_group = solver.create_group("Cloth", type="SHELL")

# Pin "collar" vertex group and animate

cloth_group.add("ClothMesh") \
           .create_pin("ClothMesh", "collar") \
           .move_by([0, 0, 0.2], t_start=1, t_end=60) \
           .spin(axis=[0, 0, 1], angular_velocity=90, t_start=10, t_end=30)

Summary

  • PinHolder in frontend/_scene_pin_.py is the primary interface for building pinned constraints through method chaining.
  • Vertex group support includes existing mesh vertex groups and curve control-point indices stored as custom properties "_pin_<name>".
  • Available operations cover linear motion, rotation, scaling, torque, and keyframe-based transformations, each implemented as a concrete subclass of Operation.
  • Entry points differ by environment: use scene.add().pin() for pure Python or Group.create_pin() (defined in blender_addon/ops/api/group.py lines 47–78) for Blender integration.
  • Serialization converts the Python PinData structure to TOML via Rust validation functions for solver consumption.

Frequently Asked Questions

What is the difference between PinHolder and PinData?

PinHolder is the high-level builder class that provides the fluent API (move_by, spin, etc.) and manages the Rust mirror instance. PinData is the underlying dataclass (lines 35–46 in frontend/_scene_pin_.py) that actually stores the vertex indices, operations list, and metadata like unpin_time. The holder manipulates the data class during scene construction.

Can I pin individual vertices without creating a vertex group?

Yes, but only when using the curve control-point workflow or the pure Python front-end. For curves, you can pass a specific indices list to Group.create_pin, which stores them in a custom property. For meshes via the Blender API, you must reference an existing vertex group; however, the underlying PinHolder itself works with raw vertex indices, so custom Python scripts using the front-end Scene API can pin arbitrary index arrays.

How do I release pinned vertices during the simulation?

Call the .unpin(time) method on your pin instance, which sets the unpin_time metadata in PinData. When the scene serializes to TOML, this value instructs the Rust solver to release the constraint at the specified simulation time, allowing the vertices to respond to physics freely thereafter.

Which source file handles the TOML serialization for pins?

The conversion logic resides in frontend/_scene_pin_.py. Specifically, _pin_to_toml_dict iterates over PinData objects, while _pin_op_to_toml_dict converts each operation instance to its dictionary representation. The final TOML string generation is delegated to _rust.scene_format_pin_toml in the Rust kernel.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →