🐍 Blender Python API Reference#

Every public class and method listed here is reachable after from bl_ext.user_default.ppf_contact_solver.ops.api import solver. See 🐍 Blender Python API for a narrative walkthrough of the same surface.

Classes:

class Solver#

Top-level entry point for the ZOZO Contact Solver.

Available as solver when imported via:

from bl_ext.user_default.ppf_contact_solver.ops.api import solver

Scene parameters are accessed via param (a SceneParam proxy). Groups, pins, and invisible colliders are created via the methods below.

Unrecognized attribute access falls through to bpy.ops.zozo_contact_solver.<name>(), so every operator registered under that namespace (including every MCP handler) can be called as a method on solver.

Example:

solver.param.gravity = (0, 0, -9.8)
group = solver.create_group("Sphere", type="SOLID")
group.add("Sphere")
group.param.solid_density = 100
create_group(name: str = '', type: str = 'SOLID') Group#

Create a new dynamics group.

Parameters:
  • name – Display name for the group. Empty string leaves the auto-generated name in place.

  • type – One of "SOLID", "SHELL", "ROD", "STATIC", "PDRD", "SAND".

Returns:

A Group proxy for the newly created group.

Example:

group = solver.create_group("Shirt", type="SHELL")
group.add("Shirt")
get_group(group_uuid: str) Group#

Look up a group by UUID.

Parameters:

group_uuid – UUID string of the group.

Returns:

A Group proxy.

Raises:

KeyError – If the group does not exist.

Example:

uuid = solver.get_groups()[0].uuid
group = solver.get_group(uuid)
get_groups() list[Group]#

Return Group proxies for every active group.

Example:

for group in solver.get_groups():
    print(group.uuid)
delete_all_groups() Solver#

Delete every active group and the pins they own.

Returns:

self for chaining.

Example:

solver.delete_all_groups()
clear() Solver#

Reset the entire solver state to defaults.

Deletes every active group, resets scene parameters to their property defaults, clears merge pairs, invisible colliders, dynamic parameters, previously fetched frames, saved pin keyframes, and any residual MESH_CACHE modifiers on mesh objects. Call this at the top of any script that needs a clean slate.

Returns:

self for chaining.

Example:

solver.clear()
solver.param.gravity = (0, 0, -9.8)
create_curve(name: str, *, bevel_depth: float = 0.0, bevel_resolution: int = 2, resolution_u: int = 4, dimensions: str = '3D', clear_existing: bool = True) Curve#

Start building a multi-spline Bezier curve object.

Returns a Curve builder. Use Curve.add_spline() for each spline, optionally Curve.set_material() to color them, then Curve.finalize() to link the resulting object into the scene.

Parameters:
  • name – Object name. When clear_existing is true (the default) any existing object with this name is removed first so re-running the script starts from a clean slate.

  • bevel_depth – Tube radius for visualization (Blender’s Curve.bevel_depth). 0 leaves the curve as a wireframe.

  • bevel_resolution – Tube cross-section subdivisions (Curve.bevel_resolution).

  • resolution_u – Spline interpolation resolution (Curve.resolution_u).

  • dimensions – "3D" (default) or "2D".

  • clear_existing – Set False to skip the same-name cleanup.

Returns:

A Curve builder.

Example:

curve = solver.create_curve("Strands", bevel_depth=3e-3)
for points, closed in strands:
    curve.add_spline(points, closed=closed)
obj = curve.finalize()
snap(object_a: str, object_b: str) Solver#

Translate object_a so its nearest vertex lands on object_b.

Parameters:
  • object_a – Name of the mesh that moves.

  • object_b – Name of the mesh that stays in place.

Returns:

self for chaining.

Raises:

ValueError – If either object is missing, not a mesh, or validation in the underlying mutation service fails.

Example:

solver.snap("Shirt", "Mannequin")
scan_meshes(*object_names: str, merge_threshold: float = 0.0001, area_eps: float = 0.0) dict[str, dict]#

Report the geometry the solver rejects, modifying nothing.

Parameters:
  • *object_names – One or more mesh object names.

  • merge_threshold – Vertices closer together than this, in local units, count as near-coincident. Matches Blender’s Merge by Distance default.

  • area_eps – Faces at or below this area, in local units squared, count as degenerate. 0.0 reports only exactly zero-area faces.

Returns:

{object_name: report}. Each report carries object (that same name), n_errors, n_notes, total (every defect count summed), the n_verts / n_polys / merge_threshold / area_eps it was taken at, dependents (what a vertex-count change on that object would affect), and defects. A note is not a defect: an open quad panel is an ordinary cloth mesh.

defects is keyed by the eight names below, and count is the only key every entry has. The rest differ per defect, and three entries carry nothing but count when they find nothing, so read anything else behind a non-zero count or through dict.get:

  • near_duplicates: min_dist, min_dist_world, preview (up to eight (i, j) vertex-index pairs) and verts (every vertex index involved), all four present only when count is non-zero.

  • isolated_verts, hanging_verts: preview (up to eight vertex indices) and verts, always present and empty at count zero.

  • degenerate_faces: preview (up to eight face indices), present only when count is non-zero. No verts.

  • duplicate_faces: nothing beyond count.

  • surface: boundary, non_manifold and bad_winding, always present; count is their sum. No preview, no verts.

  • resplittable: max_fold_deg and past_flip, present only when count is non-zero. No verts.

  • linked_duplicate: siblings, the names of the objects sharing this mesh datablock, always present.

The per-vertex verts lists come back whole here. The MCP tool strips them from its payload, where the counts and the previews are what a client acts on.

Raises:

ValueError – If a name is missing, is not a mesh, is outside the active view layer, or cannot be selected in it (hidden in the viewport, hidden by its collection, or carrying Disable Selection), since the operators underneath read the selection and would skip it. Also if a named object answered with no report.

Example:

report = solver.scan_meshes("Shirt")["Shirt"]
if report["defects"]["near_duplicates"]["count"]:
    solver.merge_by_distance("Shirt")
merge_by_distance(*object_names: str, merge_threshold: float = 0.0001, clear_stale_caches: bool = True) Solver#

Weld near-coincident vertices. Changes the vertex count.

A surviving pair sits far inside the contact gap, where the cubic barrier’s mass / gap^2 dynamic stiffness contributes Hessian entries many orders of magnitude larger than the rest of the row, so the fp32 Newton matrix loses rank and the run stops on the solver’s SPD guard naming no geometry. Welding is what makes such a mesh simulable.

The vertex count moves, which invalidates the PC2 display cache and any captured deformation on the object, and can shift which vertices a pin group holds. scan_meshes() reports those per object under dependents. Run Transfer again afterward.

Parameters:
  • *object_names – One or more mesh object names.

  • merge_threshold – Weld vertices closer together than this, in local units.

  • clear_stale_caches – Delete the display and capture caches the count change invalidates, which is what the panel’s confirmation does. Pass False to keep them, and expect the viewport overlay to read data sized for the old vertex count until the next Transfer rewrites it.

Returns:

self for chaining.

Raises:

ValueError – If a name is missing, is not a mesh, or the object is outside the active view layer or cannot be selected in it (hidden in the viewport, hidden by its collection, or carrying Disable Selection).

Example:

solver.merge_by_distance("Shirt", merge_threshold=1e-4)
remove_loose_vertices(*object_names: str, clear_stale_caches: bool = True) Solver#

Delete vertices that belong to no face. Changes the vertex count.

The solver averages a vertex’s contact parameters over its incident faces and aborts when it has none. Loose edges go with the vertices they connect. Pinned vertices are exempt, since a pin holds them regardless, and a SAND particle mesh is skipped outright: every grain center is legitimately faceless.

The vertex count moves, which invalidates the PC2 display cache and any captured deformation on the object, and can shift which vertices a pin group holds. Run Transfer again afterward.

Parameters:
  • *object_names – One or more mesh object names.

  • clear_stale_caches – Delete the display and capture caches the count change invalidates, which is what the panel’s confirmation does. Pass False to keep them, and expect the viewport overlay to read data sized for the old vertex count until the next Transfer rewrites it.

Returns:

self for chaining.

Raises:

ValueError – If a name is missing, is not a mesh, or the object is outside the active view layer or cannot be selected in it (hidden in the viewport, hidden by its collection, or carrying Disable Selection).

Example:

solver.remove_loose_vertices("Shirt")
dissolve_degenerate_faces(*object_names: str, merge_threshold: float = 0.0001, clear_stale_caches: bool = True) Solver#

Collapse zero-area faces and zero-length edges.

Changes the vertex count. A face with no area has no defined normal, so the contact normal and the bending hinge built on it are both undefined.

The vertex count moves, which invalidates the PC2 display cache and any captured deformation on the object, and can shift which vertices a pin group holds. Run Transfer again afterward.

Parameters:
  • *object_names – One or more mesh object names.

  • merge_threshold – Edges shorter than this, in local units, are collapsed.

  • clear_stale_caches – Delete the display and capture caches the count change invalidates, which is what the panel’s confirmation does. Pass False to keep them, and expect the viewport overlay to read data sized for the old vertex count until the next Transfer rewrites it.

Returns:

self for chaining.

Raises:

ValueError – If a name is missing, is not a mesh, or the object is outside the active view layer or cannot be selected in it (hidden in the viewport, hidden by its collection, or carrying Disable Selection).

Example:

solver.dissolve_degenerate_faces("Shirt")
delete_duplicate_faces(*object_names: str) Solver#

Delete faces that repeat an existing face’s vertex set.

Two faces on the same vertices contribute their contact and elastic terms twice. The vertex count is unchanged, so no cache is invalidated. Run Transfer again afterward.

Parameters:

*object_names – One or more mesh object names.

Returns:

self for chaining.

Raises:

ValueError – If a name is missing, is not a mesh, or the object is outside the active view layer or cannot be selected in it (hidden in the viewport, hidden by its collection, or carrying Disable Selection).

Example:

solver.delete_duplicate_faces("Shirt")
triangulate_for_solver(*object_names: str) Solver#

Triangulate every face with more than three corners.

Transfer triangulates on its own at encode time, so this is for when the triangulation has to be visible and stable in the viewport: with the diagonals fixed in the mesh, Blender has none left to re-pick from the deformed shape, and the displayed surface stops drifting from the simulated one. The vertex count is unchanged, so no cache is invalidated. For a mesh whose symmetry matters under bending, use symmetric_triangulate() instead.

Parameters:

*object_names – One or more mesh object names.

Returns:

self for chaining.

Raises:

ValueError – If a name is missing, is not a mesh, or the object is outside the active view layer or cannot be selected in it (hidden in the viewport, hidden by its collection, or carrying Disable Selection).

Example:

solver.triangulate_for_solver("Shirt")
recalculate_normals_outside(*object_names: str) Solver#

Make face winding consistent and outward.

Inconsistent winding flips the normal a face contributes, which the contact and inflate terms read. The vertex count is unchanged, so no cache is invalidated, but the encoder captures winding at Transfer time, so run Transfer again afterward.

Parameters:

*object_names – One or more mesh object names.

Returns:

self for chaining.

Raises:

ValueError – If a name is missing, is not a mesh, or the object is outside the active view layer or cannot be selected in it (hidden in the viewport, hidden by its collection, or carrying Disable Selection).

Example:

solver.recalculate_normals_outside("Shirt")
symmetric_triangulate(*object_names: str) Solver#

Triangulate by poking each face, keeping the mesh symmetric.

A single-diagonal triangulation breaks a mirror-symmetric mesh’s symmetry, which shows up as a lopsided drape under bending. Poking inserts a center vertex and fans the face into triangles around it instead.

That adds one vertex per face, so the PC2 display cache and any captured deformation on the object are invalidated exactly as the count-changing repairs invalidate them. Nothing here deletes them: run Transfer to rewrite the display cache, and Capture Deformation (or recapture_all_deformations()) to re-take the captures.

Parameters:

*object_names – One or more mesh object names.

Returns:

self for chaining.

Raises:

ValueError – If a name is missing, is not a mesh, or the object is outside the active view layer or cannot be selected in it (hidden in the viewport, hidden by its collection, or carrying Disable Selection).

Example:

solver.symmetric_triangulate("Shirt")
convert_to_particle_mesh(object_name: str, grain_radius: float, extra_spacing: float = 0.0, rng_seed: int = 0) int#

Replace a solid mesh with the grain cloud a SAND group simulates.

Destructive: the faces are discarded and the object becomes a faceless mesh of loose vertices carrying a render-only Particle Mesh modifier. The grain count is not chosen, it is whatever fills the volume at the given separation, which is why it is the return value.

grain_radius is stamped onto the object and is what the encoder reads, in preference to the group’s sand_grain_radius. The non-overlapping seed spacing derives from it and it is also the contact skin, so it is locked once the object is converted and the panel shows it read-only. Pick it before converting.

Parameters:
  • object_name – Name of a mesh object that has faces and is not already a particle mesh.

  • grain_radius – Physical grain radius, which is also the contact skin.

  • extra_spacing – Gap added between grains beyond touching. 0.0 packs them as densely as non-overlap allows.

  • rng_seed – Seed for the Poisson-disk seeding, for a repeatable cloud.

Returns:

The number of grains seeded.

Raises:

ValueError – If the object is missing, is not a mesh, has no faces, is already a particle mesh, grain_radius is not positive, or no grain fits inside the mesh.

Example:

n_grains = solver.convert_to_particle_mesh(
    "Pile", grain_radius=0.01,
)
sand = solver.create_group("Sand", type="SAND")
sand.add("Pile")
sand.param.sand_particle_mass = 10.0  # grams per grain
add_merge_pair(object_a: str, object_b: str) Solver#

Mark two objects to be merged at their shared contact.

Parameters:
  • object_a – Name of the first mesh.

  • object_b – Name of the second mesh.

Returns:

self for chaining.

Raises:

ValueError – If either object is missing, not a mesh, or the pair is invalid.

Example:

solver.add_merge_pair("SleeveLeft", "BodyLeft")
remove_merge_pair(object_a: str, object_b: str) Solver#

Remove a previously added merge pair.

The ordering of object_a and object_b does not matter; the pair is matched by UUID in either direction.

Parameters:
  • object_a – Name of the first mesh.

  • object_b – Name of the second mesh.

Returns:

self for chaining.

Raises:

ValueError – If validation fails for the given pair.

Example:

solver.remove_merge_pair("SleeveLeft", "BodyLeft")
get_merge_pairs() list[tuple[str, str]]#

Return every merge pair as a list of (object_a, object_b) tuples.

Example:

for a, b in solver.get_merge_pairs():
    print(f"{a} <-> {b}")
clear_merge_pairs() Solver#

Remove every merge pair.

Returns:

self for chaining.

Example:

solver.clear_merge_pairs()
add_wall(position, normal) Wall#

Add an invisible infinite-plane wall collider.

Parameters:
  • position – (x, y, z) world-space point on the plane.

  • normal – (nx, ny, nz) outward-facing plane normal. Need not be unit-length.

Returns:

A chainable Wall builder bound to the newly added collider.

Raises:

ValueError – If the position or normal fails vec3 validation.

Example:

solver.add_wall(position=(0, 0, 0), normal=(0, 0, 1))
add_sphere(position, radius) Sphere#

Add an invisible sphere collider.

Parameters:
  • position – (x, y, z) world-space center.

  • radius – Sphere radius.

Returns:

A chainable Sphere builder bound to the newly added collider.

Raises:

ValueError – If the position or radius fails validation.

Example:

solver.add_sphere(position=(0, 0, 1.0), radius=0.25)
get_invisible_colliders() list#

Return every invisible collider as a list of (type, name) tuples.

type is one of "WALL" or "SPHERE".

Example:

for kind, name in solver.get_invisible_colliders():
    print(kind, name)
clear_invisible_colliders() Solver#

Remove every invisible collider.

Returns:

self for chaining.

Example:

solver.clear_invisible_colliders()
recapture_all_deformations() Solver#

Re-capture every deforming STATIC collider and every animated pin.

One pass over the whole scene instead of one Capture Deformation per object. The statics are captured first and the pins after, since the two share the depsgraph and cannot run at once.

Note

The captures advance on Blender’s event loop: this returns once the first one has started, and the rest complete over later ticks. A script that keeps running on the same tick holds the loop and blocks them, so schedule whatever depends on the caches and gate it on is_capture_running(). A Blender started with --background runs no ticks and captures nothing.

Returns:

self for chaining.

Raises:

ValueError – If there is nothing to re-capture, or a capture or bake is already running.

Example:

import bpy

def transfer_when_captured():
    if solver.is_capture_running():
        return 0.1  # come back in 100 ms
    solver.transfer_data()
    return None

solver.recapture_all_deformations()
bpy.app.timers.register(transfer_when_captured)
clear_all_deformations() Solver#

Delete every captured deformation cache in the scene.

Covers the STATIC-collider deform caches and the animated-pin captures across the active groups, plus any cache orphaned by an object that was deleted or taken out of its group, which nothing else reaches. The objects keep their deformers, so recapture_all_deformations() rebuilds what this removes.

Returns:

self for chaining.

Raises:

ValueError – If there is no captured cache to clear, or a capture or bake is already running.

Example:

solver.clear_all_deformations()
is_capture_running() bool#

True while a deformation capture is in flight.

Covers both phases of recapture_all_deformations(), the STATIC-collider captures and the pin captures, so a script waits on the whole pass with one predicate. Read it from a timer or a handler: a capture advances only when the script has handed control back to Blender’s event loop.

Example:

import bpy

def report_when_done():
    if solver.is_capture_running():
        return 0.1  # come back in 100 ms
    print("captures finished")
    return None

bpy.app.timers.register(report_when_done)
export_usd(filepath: str) Solver#

Export the simulated mesh sequence as a USD cache.

A lighter result than baking shape keys: the deformation is sampled per frame straight from the solver cache into a file other DCC tools play back, and the scene itself is left untouched.

Every frame must be fetched before the export runs, and the export refuses while a run, bake or capture is in flight, and outside Object Mode. Rod and curve objects are not carried by this format; get_unexportable_curves() names the ones that will be left out.

Parameters:

filepath – Destination path, taken as written once a // blend-relative prefix is resolved. The suffix is what picks the USD flavor, so give one of .usdc (crate), .usda (ASCII), .usd or .usdz (package). The parent directory must exist.

Returns:

self for chaining.

Raises:

ValueError – If frames are unfetched, another solver activity is in progress, the scene is in Edit or Sculpt mode, there is no simulated mesh sequence, the destination directory does not exist, or the export did not complete.

Example:

solver.export_usd("/tmp/drape.usdc")
export_alembic(filepath: str) Solver#

Export the simulated mesh sequence as an Alembic (ABC) cache.

A lighter result than baking shape keys: the deformation is sampled per frame straight from the solver cache into a file other DCC tools play back, and the scene itself is left untouched.

Every frame must be fetched before the export runs, and the export refuses while a run, bake or capture is in flight, and outside Object Mode. Rod and curve objects are not carried by this format; get_unexportable_curves() names the ones that will be left out.

Parameters:

filepath – Destination .abc path, taken as written once a // blend-relative prefix is resolved. The parent directory must exist.

Returns:

self for chaining.

Raises:

ValueError – If frames are unfetched, another solver activity is in progress, the scene is in Edit or Sculpt mode, there is no simulated mesh sequence, the destination directory does not exist, or the export did not complete.

Example:

solver.export_alembic("/tmp/drape.abc")
get_unexportable_curves() list[str]#

Names of the simulated curves a cache export leaves out.

A rod deforms through a frame-change handler rather than through the cache modifier the exporters sample, so a CURVE object carrying a solver cache cannot be written to USD or Alembic. Bake Animation is the route that carries one.

Example:

missing = solver.get_unexportable_curves()
if missing:
    print("not exported:", ", ".join(missing))
solver.export_usd("/tmp/drape.usdc")
class SceneParam#

Attribute proxy for scene and SSH/connection parameters.

Accessed as Solver.param. Supports both get and set via attribute access. Writes go through the zozo_contact_solver.set operator (with auto type coercion), reads fall through to the scene’s addon state or SSH state.

gravity is an alias for gravity_3d.

Example:

solver.param.step_size = 0.004
print(solver.param.gravity)

Dynamic (keyframed) parameters are accessed via dyn():

solver.param.dyn("gravity").time(60).hold().time(61).change((0, 0, 9.8))
dyn(key: str) DynParam#

Select a parameter for dynamic keyframing.

Parameters:

key – One of "gravity", "wind", "air_density", "air_friction", "vertex_air_damp".

Returns:

A chainable DynParam builder.

Raises:

ValueError – If key is not one of the valid dynamic keys.

Example:

solver.param.dyn("gravity").time(60).hold().time(61).change((0, 0, 9.8))
class DynParam#

Fluent builder for dynamic scene parameter keyframes.

Mirrors the frontend session.param.dyn() API but uses frames instead of seconds. Obtained from SceneParam.dyn().

Valid parameter keys: "gravity", "wind", "air_density", "air_friction", "vertex_air_damp".

Frames must be strictly increasing within a chain. Every mutating method returns self so operations chain.

Example:

solver.param.dyn("gravity").time(60).hold().time(61).change((0, 0, 9.8))
solver.param.dyn("wind").time(30).hold().time(31).change((0, 1, 0), strength=5.0)
time(frame: int) DynParam#

Advance the frame cursor.

Parameters:

frame – Target frame (must be strictly greater than the current cursor position).

Returns:

self for chaining.

Raises:

ValueError – If frame is not strictly increasing.

Example:

solver.param.dyn("gravity").time(60).hold().time(61).change((0, 0, 9.8))
hold() DynParam#

Hold the previous value at the current cursor frame (step function).

Returns:

self for chaining.

Example:

solver.param.dyn("gravity").time(60).hold().time(61).change((0, 0, 9.8))
change(value, strength=None) DynParam#

Set a new value at the current cursor frame.

Parameters:
  • value – For "gravity", an (x, y, z) tuple. For "wind", an (x, y, z) direction tuple. For scalar keys ("air_density", "air_friction", "vertex_air_damp"), a float.

  • strength – Wind strength (only for "wind").

Returns:

self for chaining.

Example:

solver.param.dyn("wind").time(30).hold().time(31).change((0, 1, 0), strength=5.0)
clear() DynParam#

Remove this dynamic parameter entirely.

Returns:

self for chaining (though no further method on this builder will do anything meaningful after clear()).

Example:

solver.param.dyn("wind").clear()
class Group#

A dynamics group proxy.

Created via Solver.create_group(). Material parameters are accessed via param. Every mutating method returns self so operations chain.

Example:

group = solver.create_group("Shirt", type="SHELL")
group.add("Shirt").set_overlay_color(0.9, 0.2, 0.1)
group.param.friction = 0.5
group.param.shell_density = 1.0
property uuid#
Type:

str

The UUID of this group. Stable across renames.

Example:

group = solver.create_group("Shirt", type="SHELL")
same_group = solver.get_group(group.uuid)
property name#
Type:

str

Display name of this group.

Example:

for g in solver.get_groups():
    if g.name == "WovenStrands":
        g.delete()
property slot#
Type:

int

The object_group_N slot index this group occupies.

This is the index every group operator addresses, resolved through object_group_{index}. It is not ObjectGroup.index, which numbers the active groups consecutively for display: the two agree only while every slot below this one is active, so a scene that has ever deleted a group can have them disagree.

Raises:

ValueError – If no slot holds this group’s UUID.

Example:

group = solver.create_group("Shirt", type="SHELL")
print(f"stored in object_group_{group.slot}")
property type#
Type:

str

Dynamics type of this group.

One of "SOLID", "SHELL", "ROD", "STATIC", "PDRD", "SAND". Set at creation through Solver.create_group().

Example:

for g in solver.get_groups():
    if g.type == "ROD":
        g.param.length_factor = 0.97
property param#
Type:

GroupParam

Material and simulation parameter proxy. See GroupParam.

Example:

group.param.friction = 0.5
group.param.shell_density = 1.0
set_overlay_color(r: float, g: float, b: float, a: float = 1.0) Group#

Set the viewport overlay color for this group and enable it.

Parameters:
  • r – Red channel in [0, 1].

  • g – Green channel in [0, 1].

  • b – Blue channel in [0, 1].

  • a – Alpha in [0, 1] (default 1.0).

Returns:

self for chaining.

Example:

group.set_overlay_color(0.9, 0.2, 0.1)  # red overlay
add(*object_names: str) Group#

Add mesh objects to this group by name.

Parameters:

*object_names – One or more Blender object names.

Returns:

self for chaining.

Example:

group.add("Shirt", "Skirt", "Sleeve")
remove(object_name: str) Group#

Remove an object from this group.

Parameters:

object_name – Name of the object to remove.

Returns:

self for chaining.

Example:

group.remove("Sleeve")
set_velocity(object_name: str, direction: tuple[float, float, float], speed: float, frame: int = 1, angular_axis: 'int | str' = 'PC3', angular_speed: float = 0.0, angular_axis_custom: tuple[float, float, float] = (0.0, 0.0, 1.0), enable_translational: bool = True, enable_angular: bool | None = None) Group#

Keyframe a velocity on an object assigned to this group.

Appends an entry to the assigned object’s velocity_keyframes collection. Call once with frame=1 for an initial-velocity launch; call again with higher frame values to build a velocity schedule.

Parameters:
  • object_name – Name of an object already added to this group via add().

  • direction – (dx, dy, dz) velocity direction; normalized by the solver before use.

  • speed – Velocity magnitude in m/s.

  • frame – Frame at which the keyframe takes effect. 1 (the default) is the initial-velocity slot.

  • angular_axis – Axis to spin about (SOLID, SHELL, PDRD only). One of "PC1"/"PC2"/"PC3" (principal axes, resolved dynamically from the simulated geometry), "X"/"Y"/"Z" (fixed world axes), or "CUSTOM" (the angular_axis_custom vector). Ints 0/1/2 are accepted as PC1/PC2/PC3 for convenience. Ignored when angular_speed == 0.

  • angular_speed – Signed spin speed in degrees per second (0 = no spin).

  • angular_axis_custom – World-space (x, y, z) axis used when angular_axis == "CUSTOM" (normalized before use).

  • enable_translational – Overwrite the translational velocity at this frame. When False the keyframe leaves translation alone (e.g. a pure-spin keyframe).

  • enable_angular – Overwrite the angular velocity at this frame. Defaults to True when angular_speed is non-zero, else False. Pass explicitly to override.

Returns:

self for chaining.

Raises:

ValueError – If the object is not assigned to this group, or a keyframe already exists at the requested frame.

Example:

ball = solver.create_group("Ball", type="SOLID")
ball.add("Sphere")
ball.set_velocity("Sphere", direction=(1, 0, 0), speed=2.3)
set_hinge(object_name: str, pca_axis: int = 2, enable: bool = True) Group#

Pin a PDRD body assigned to this group as a hinge (per object).

Locks the body’s position and restricts its rotation to one principal (PCA) axis of its rest shape, the building block for gears. The group must be of type PDRD. This is a per-object setting, so different bodies in the same group can be hinged on different axles.

Parameters:
  • object_name – Name of an object already added to this group via add().

  • pca_axis – Which principal axis is the free axle: 0 (largest extent), 1 (middle), or 2 (thinnest, the usual axle for a flat gear or disk). Defaults to 2.

  • enable – Set False to clear the hinge and let the body move freely. Defaults to True.

Returns:

self for chaining.

Raises:

ValueError – If the group is not PDRD, the object is not assigned to it, or pca_axis is not in {0, 1, 2}.

Example:

gears = solver.create_group("Gears", type="PDRD")
gears.add("GearA")
gears.set_hinge("GearA", pca_axis=2)
create_pin(object_name: str, vertex_group_name: str, indices: list[int] | None = None) Pin#

Pin a vertex group (mesh) or set of control points (curve).

Parameters:
  • object_name – Name of the mesh or curve object.

  • vertex_group_name – For meshes, the name of an existing vertex group on the object. For curves, the logical name used for the curve’s _pin_<vertex_group_name> custom property holding the pinned control-point indices.

  • indices – Control-point indices for curves only. When given, the curve’s _pin_<vertex_group_name> property is written before the pin is registered, so the same call both defines and binds the pin. Must be None for meshes (meshes use existing vertex groups).

Returns:

A Pin proxy for the newly created pin.

Raises:

ValueError – If the object is missing, not a mesh or curve, the vertex group does not exist on a mesh, the _pin_<name> property is missing on a curve and no indices were supplied, or indices is passed for a mesh.

Example:

# Mesh: vertex group must already exist.
pin = group.create_pin("Cloth", "collar")
pin.move_by(delta=(0, 0, 0.2), frame_start=1, frame_end=60)

# Curve: pass control-point indices to define and bind in
# one call.
rod_pin = rod_group.create_pin(
    "WovenCylinder", "left", indices=[0, 7, 14, 21],
)
get_pins() list[Pin]#

Return all pins in this group as Pin proxies.

Example:

for pin in group.get_pins():
    print(pin.object_name, pin.vertex_group_name)
delete() None#

Delete this group and every pin it owns.

Example:

group.delete()
class GroupParam#

Proxy for material and simulation parameters on a group.

Accessed via Group.param. Attribute access is whitelisted: reading or writing a name outside the whitelist raises AttributeError.

Whitelisted attributes:

  • Solver model: solid_model, shell_model, rod_model (rod_model currently accepts only "ARAP": the enum has a single item and ROD groups force-pin it to ARAP)

  • Density: solid_density, shell_density, rod_density

  • Young’s modulus: solid_young_modulus, shell_young_modulus, rod_young_modulus

  • Poisson ratio: solid_poisson_ratio, shell_poisson_ratio

  • Contact: friction, use_group_bounding_box_diagonal, contact_gap, contact_gap_rat, contact_offset, contact_offset_rat. When use_group_bounding_box_diagonal is True (the default), the solver consumes contact_gap_rat * bbox-diagonal and contact_offset_rat * bbox-diagonal; set it to False to consume the absolute contact_gap / contact_offset values directly.

  • Strain limit: enable_strain_limit, strain_limit_percent

  • Inflation: enable_inflate, inflate_pressure

  • Plasticity: enable_plasticity, plasticity, plasticity_threshold

  • Bend plasticity: enable_bend_plasticity, bend_plasticity, bend_plasticity_threshold, bend_rest_angle_source

  • Bend stiffness: bend, drawn on Shell and Rod groups

  • Rest-shape scale: shrink_x / shrink_y (Shell, per axis), shrink (Solid, uniform), length_factor (Rod, labeled Shrink in the panel; it scales the rod’s rest edge length)

  • Stitch: stitch_stiffness (Solid, Shell and Rod)

  • PDRD-specific: pdrd_density (kg/m^3, volumetric). The PDRD hinge joint is per-object, set via set_hinge() (not a group material).

  • Sand-specific: sand_grain_radius (meters), sand_particle_mass (grams; the encoder sends kilograms), sand_friction. Once Solver.convert_to_particle_mesh() has run, the radius it seeded is stamped on the object and the encoder reads that in preference to sand_grain_radius.

Example:

# Solver model
group.param.solid_model = "ARAP"
group.param.shell_model = "ARAP"

# Density (kg/m^3 solid, kg/m^2 shell, kg/m rod)
group.param.solid_density = 100.0
group.param.shell_density = 0.3
group.param.rod_density = 0.05

# Young's modulus (Pa) and Poisson ratio
group.param.solid_young_modulus = 1.0e6
group.param.shell_young_modulus = 5.0e5
group.param.rod_young_modulus = 1.0e7
group.param.solid_poisson_ratio = 0.45
group.param.shell_poisson_ratio = 0.30

# Contact (absolute mode)
group.param.use_group_bounding_box_diagonal = False
group.param.friction = 0.5
group.param.contact_gap = 0.001
group.param.contact_offset = 0.002

# Contact (ratio mode -- defaults)
group.param.use_group_bounding_box_diagonal = True
group.param.contact_gap_rat = 0.1
group.param.contact_offset_rat = 0.2

# Strain limit (percentage; 5.0 == 5% allowed stretch)
group.param.enable_strain_limit = True
group.param.strain_limit_percent = 5.0

# Inflation
group.param.enable_inflate = True
group.param.inflate_pressure = 100.0

# Plasticity (shells and tets only)
group.param.enable_plasticity = True
group.param.plasticity = 0.3
group.param.plasticity_threshold = 0.2
group.param.enable_bend_plasticity = True
group.param.bend_plasticity = 0.5
group.param.bend_plasticity_threshold = 0.1
group.param.bend_rest_angle_source = "FROM_GEOMETRY"

# Bending (Shell and Rod), and the Shell per-axis rest-shape scale
group.param.bend = 1.0e-4
group.param.shrink_x = 0.99
group.param.shrink_y = 0.97

# Solid uniform rest-shape scale, and the Rod rest-length scale
group.param.shrink = 0.98
group.param.length_factor = 0.97

# Stitch (Solid, Shell and Rod)
group.param.stitch_stiffness = 5.0e4

# Sand-specific (particle mass in grams)
group.param.sand_particle_mass = 10.0
group.param.sand_friction = 0.4
class Pin#

A pinned vertex group bound to a dynamics group.

Created via Group.create_pin(object_name, vertex_group_name). Every mutating method returns self so operations chain.

Example:

pin = group.create_pin("Cloth", "hem")
pin.move_by(delta=(0, 0, 1.0), frame_start=1, frame_end=60)
pin.unpin(frame=120)
property object_name#
Type:

str

Name of the mesh object this pin belongs to.

Example:

pin = group.create_pin("Cloth", "hem")
print(pin.object_name)  # "Cloth"
property vertex_group_name#
Type:

str

Name of the vertex group this pin targets.

Example:

pin = group.create_pin("Cloth", "hem")
print(pin.vertex_group_name)  # "hem"
pull(strength: float = 1.0) Pin#

Use pull force instead of hard pin constraint.

Pull allows the vertices to move but applies a restoring force toward their target position.

Parameters:

strength – Pull force strength (default 1.0).

Returns:

self for chaining.

Example:

group.create_pin("Cloth", "shoulder").pull(strength=2.5)
spin(axis: tuple[float, float, float] = (1, 0, 0), angular_velocity: float = 360.0, flip: bool = False, center: tuple[float, float, float] | None = None, center_mode: str | None = None, center_direction: tuple[float, float, float] | None = None, center_vertex: int | None = None, frame_start: int = 1, frame_end: int = 60, transition: str = 'LINEAR') Pin#

Add a spin operation to this pin.

Parameters:
  • axis – Rotation axis vector.

  • angular_velocity – Degrees per second.

  • flip – Reverse spin direction.

  • center – Center of rotation (for ABSOLUTE mode).

  • center_mode – "CENTROID", "ABSOLUTE", "MAX_TOWARDS", or "VERTEX". If None, inferred from other args (None center β†’ "CENTROID").

  • center_direction – Direction for MAX_TOWARDS mode.

  • center_vertex – Vertex index for VERTEX mode.

  • frame_start – Start frame.

  • frame_end – End frame.

  • transition – "LINEAR" or "SMOOTH".

Returns:

self for chaining.

Example:

# Spin about the centroid at 180 deg/s for frames 1-60
pin.spin(axis=(0, 0, 1), angular_velocity=180.0)
# Spin about an absolute world-space pivot
pin.spin(axis=(0, 1, 0), center=(0, 0, 1),
         frame_start=30, frame_end=90)
scale(factor: float = 1.0, center: tuple[float, float, float] | None = None, center_mode: str | None = None, center_direction: tuple[float, float, float] | None = None, center_vertex: int | None = None, frame_start: int = 1, frame_end: int = 60, transition: str = 'LINEAR') Pin#

Add a scale operation to this pin.

Parameters:
  • factor – Scale factor.

  • center – Center point (for ABSOLUTE mode).

  • center_mode – "CENTROID", "ABSOLUTE", "MAX_TOWARDS", or "VERTEX". If None, inferred from other args (None center β†’ "CENTROID").

  • center_direction – Direction for MAX_TOWARDS mode.

  • center_vertex – Vertex index for VERTEX mode.

  • frame_start – Start frame.

  • frame_end – End frame.

  • transition – "LINEAR" or "SMOOTH".

Returns:

self for chaining.

Example:

# Shrink to 50% over frames 1-60 about the centroid
pin.scale(factor=0.5, transition="SMOOTH")
torque(magnitude: float = 1.0, axis_component: str = 'PC3', flip: bool = False, frame_start: int = 1, frame_end: int = 60) Pin#

Add a torque operation to this pin.

Applies a rotational force around a PCA-computed axis.

Parameters:
  • magnitude – Torque in NΒ·m.

  • axis_component – "PC1" (major), "PC2" (middle), or "PC3" (minor).

  • flip – Reverse torque direction.

  • frame_start – Start frame.

  • frame_end – End frame.

Returns:

self for chaining.

Example:

pin.torque(magnitude=2.0, axis_component="PC1",
           frame_start=1, frame_end=30)
move_by(delta: tuple[float, float, float] = (0, 0, 0), frame_start: int = 1, frame_end: int = 60, transition: str = 'LINEAR') Pin#

Ramp a translation of the pinned vertices over a frame range.

Parameters:
  • delta – (dx, dy, dz) offset.

  • frame_start – Start frame.

  • frame_end – End frame.

  • transition – "LINEAR" or "SMOOTH".

Returns:

self for chaining.

Example:

# Lift 1.0m along +Z between frames 10 and 90
pin.move_by(delta=(0, 0, 1.0),
            frame_start=10, frame_end=90,
            transition="SMOOTH")
unpin(frame: int) Pin#

Release this pin after the given number of frames.

Sets the duration on the underlying pin item so the encoder knows when to stop enforcing the pin constraint.

Parameters:

frame – Number of frames the pin stays active, counted from the solve’s Starting Frame (a count, not a frame number, despite the keyword’s name).

Returns:

self for chaining.

Example:

pin.move_by(delta=(0, 0, 1.0), frame_start=1, frame_end=60)
pin.unpin(frame=120)  # released 120 frames after the solve starts
delete() None#

Remove this pin from its group.

Raises:

ValueError – If the owning group or pin item can no longer be found (for example, after solver.clear()).

Example:

pin = group.create_pin("Cloth", "hem")
pin.delete()  # remove the pin entry from the group
class Wall#

Chainable builder for invisible wall colliders.

Returned by Solver.add_wall(). Keyframe frames must be strictly increasing. Every mutating method returns self.

Example:

solver.add_wall((0, 0, 0), (0, 0, 1)).param.friction = 0.5
(solver.add_wall((0, 0, 0), (0, 1, 0))
       .time(60).hold().time(61).move_to((0, 1, 0)))
property param#
Type:

ColliderParam

Collider parameter proxy. See ColliderParam.

Example:

wall = solver.add_wall((0, 0, 0), (0, 0, 1))
wall.param.friction = 0.5
time(frame: int) Wall#

Advance the keyframe cursor.

Parameters:

frame – Target frame (must be strictly greater than the current cursor position).

Returns:

self for chaining.

Raises:

ValueError – If frame is not strictly increasing.

Example:

(solver.add_wall((0, 0, 0), (0, 0, 1))
       .time(60).move_to((0, 0, 0.5)))
hold() Wall#

Hold the previous position at the current cursor frame.

Returns:

self for chaining.

Example:

(solver.add_wall((0, 0, 0), (0, 0, 1))
       .time(60).hold().time(90).move_to((0, 0, 0.5)))
move_to(position) Wall#

Keyframe a new absolute position at the current cursor frame.

Parameters:

position – (x, y, z) world-space position.

Returns:

self for chaining.

Example:

(solver.add_wall((0, 0, 0), (0, 0, 1))
       .time(60).move_to((0, 0, 1.0)))
move_by(delta) Wall#

Keyframe a position offset from the previous keyframe.

Parameters:

delta – (dx, dy, dz) offset added to the previous keyframed position.

Returns:

self for chaining.

Example:

(solver.add_wall((0, 0, 0), (0, 0, 1))
       .time(60).move_by((0, 0, 0.25)))
delete() None#

Remove this wall collider from the scene.

Example:

wall = solver.add_wall((0, 0, 0), (0, 0, 1))
wall.delete()
class Sphere#

Chainable builder for invisible sphere colliders.

Returned by Solver.add_sphere(). Keyframe frames must be strictly increasing. Every mutating method returns self.

Example:

solver.add_sphere((0, 0, 0), 0.98).invert().hemisphere()
(solver.add_sphere((0, 0, 0), 1.0)
       .time(60).hold().time(61).radius(0.5))
property param#
Type:

ColliderParam

Collider parameter proxy. See ColliderParam.

Example:

sphere = solver.add_sphere((0, 0, 0), 1.0)
sphere.param.friction = 0.3
invert() Sphere#

Flip the sphere inside-out so contact is on the inside surface.

Returns:

self for chaining.

Example:

solver.add_sphere((0, 0, 0), 1.0).invert()
hemisphere() Sphere#

Treat this collider as a hemisphere rather than a full sphere.

Returns:

self for chaining.

Example:

solver.add_sphere((0, 0, 0), 1.0).hemisphere()
time(frame: int) Sphere#

Advance the keyframe cursor.

Parameters:

frame – Target frame (must be strictly greater than the current cursor position).

Returns:

self for chaining.

Raises:

ValueError – If frame is not strictly increasing.

Example:

(solver.add_sphere((0, 0, 0), 1.0)
       .time(60).move_to((0, 0, 1.0)))
hold() Sphere#

Hold the previous position and radius at the current cursor frame.

Returns:

self for chaining.

Example:

(solver.add_sphere((0, 0, 0), 1.0)
       .time(60).hold().time(90).radius(0.5))
move_to(position) Sphere#

Keyframe a new absolute position at the current cursor frame.

Parameters:

position – (x, y, z) world-space position.

Returns:

self for chaining.

Example:

(solver.add_sphere((0, 0, 0), 1.0)
       .time(60).move_to((0, 0, 2.0)))
radius(r) Sphere#

Keyframe a new radius at the current cursor frame.

Parameters:

r – New radius.

Returns:

self for chaining.

Example:

(solver.add_sphere((0, 0, 0), 1.0)
       .time(60).radius(0.25))  # shrink over 60 frames
transform_to(position, radius) Sphere#

Keyframe both position and radius together.

Parameters:
  • position – (x, y, z) world-space position.

  • radius – New radius.

Returns:

self for chaining.

Example:

(solver.add_sphere((0, 0, 0), 1.0)
       .time(60).transform_to((0, 0, 1.0), 0.5))
delete() None#

Remove this sphere collider from the scene.

Example:

sphere = solver.add_sphere((0, 0, 0), 1.0)
sphere.delete()
class ColliderParam#

Attribute proxy for invisible-collider parameters.

Accessed via Wall.param or Sphere.param. Attribute access is whitelisted: reading or writing a name outside the whitelist raises AttributeError.

Whitelisted attributes:

  • friction: contact friction coefficient

  • contact_gap: contact gap thickness

  • thickness: wall/sphere shell thickness

  • enable_active_duration: True to limit collider lifetime

  • active_duration: the first Blender frame at which the collider is no longer active (exclusive cutoff), not a frame count

Example:

wall = solver.add_wall((0, 0, 0), (0, 0, 1))
wall.param.friction = 0.5
wall.param.contact_gap = 0.002
wall.param.thickness = 0.01
wall.param.enable_active_duration = True
wall.param.active_duration = 60  # active through frame 59; off from 60

sphere = solver.add_sphere((0, 0, 1), 0.5)
sphere.param.friction = 0.3
class Curve#

Builder for a multi-spline Bezier curve object.

Created via Solver.create_curve(). Each add_spline() appends one Bezier spline to the underlying curve datablock; finalize() links the resulting object into the active scene and returns it.

Pin definition is not part of this builder. Pass the control-point indices to Group.create_pin() instead, which writes the _pin_<name> custom property and registers the pin in one call.

Example:

curve = solver.create_curve("WovenCylinder", bevel_depth=3e-3)
for points, closed in strands:
    curve.add_spline(points, closed=closed)
obj = curve.finalize()

rod = solver.create_group("Strands", type="ROD")
rod.add(obj.name)
rod.create_pin(obj.name, "left", indices=left_indices)
property name#
Type:

str

Object name this builder will create on finalize().

add_spline(points, *, closed: bool = False) int#

Append a Bezier spline with AUTO handles.

Parameters:
  • points – Iterable of (x, y, z) control-point coordinates (a NumPy array of shape (n, 3) works).

  • closed – Set True to make the spline cyclic.

Returns:

Zero-based index of the new spline within this curve. Use it with set_material().

Raises:

ValueError – If points has fewer than two coordinates.

set_material(spline_index: int, material: 'bpy.types.Material') Curve#

Bind a material to a spline by index.

The material is appended to the curve’s slots if it isn’t already present. Pre-existing slots are reused so repeated calls with the same material don’t grow the slot list.

Parameters:
  • spline_index – Index returned by add_spline().

  • material – An existing bpy.types.Material. Create it with bpy.data.materials.new(...) before calling.

Returns:

self for chaining.

Raises:

IndexError – If spline_index is out of range.

finalize() 'bpy.types.Object'#

Create the bpy.types.Object, link it to the scene, and return it.

Raises:

RuntimeError – If called more than once on the same builder.