π 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
solverwhen imported via:from bl_ext.user_default.ppf_contact_solver.ops.api import solverScene parameters are accessed via
param(aSceneParamproxy). 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 onsolver.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
Groupproxy 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
Groupproxy.- 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
Groupproxies 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:
selffor 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_CACHEmodifiers on mesh objects. Call this at the top of any script that needs a clean slate.- Returns:
selffor 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
Curvebuilder. UseCurve.add_spline()for each spline, optionallyCurve.set_material()to color them, thenCurve.finalize()to link the resulting object into the scene.- Parameters:
name β Object name. When
clear_existingis 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).0leaves 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
Falseto skip the same-name cleanup.
- Returns:
A
Curvebuilder.
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:
selffor 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.0reports only exactly zero-area faces.
- Returns:
{object_name: report}. Each report carriesobject(that same name),n_errors,n_notes,total(every defect count summed), then_verts/n_polys/merge_threshold/area_epsit was taken at,dependents(what a vertex-count change on that object would affect), anddefects. A note is not a defect: an open quad panel is an ordinary cloth mesh.defectsis keyed by the eight names below, andcountis the only key every entry has. The rest differ per defect, and three entries carry nothing butcountwhen they find nothing, so read anything else behind a non-zerocountor throughdict.get:near_duplicates:min_dist,min_dist_world,preview(up to eight(i, j)vertex-index pairs) andverts(every vertex index involved), all four present only whencountis non-zero.isolated_verts,hanging_verts:preview(up to eight vertex indices) andverts, always present and empty atcountzero.degenerate_faces:preview(up to eight face indices), present only whencountis non-zero. Noverts.duplicate_faces: nothing beyondcount.surface:boundary,non_manifoldandbad_winding, always present;countis their sum. Nopreview, noverts.resplittable:max_fold_degandpast_flip, present only whencountis non-zero. Noverts.linked_duplicate:siblings, the names of the objects sharing this mesh datablock, always present.
The per-vertex
vertslists 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^2dynamic 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 underdependents. 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
Falseto keep them, and expect the viewport overlay to read data sized for the old vertex count until the next Transfer rewrites it.
- Returns:
selffor 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
Falseto keep them, and expect the viewport overlay to read data sized for the old vertex count until the next Transfer rewrites it.
- Returns:
selffor 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
Falseto keep them, and expect the viewport overlay to read data sized for the old vertex count until the next Transfer rewrites it.
- Returns:
selffor 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:
selffor 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:
selffor 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:
selffor 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:
selffor 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_radiusis stamped onto the object and is what the encoder reads, in preference to the groupβssand_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.0packs 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_radiusis 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:
selffor 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:
selffor 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:
selffor 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
Wallbuilder 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
Spherebuilder 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:
selffor 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--backgroundruns no ticks and captures nothing.- Returns:
selffor 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:
selffor 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),.usdor.usdz(package). The parent directory must exist.- Returns:
selffor 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
.abcpath, taken as written once a//blend-relative prefix is resolved. The parent directory must exist.- Returns:
selffor 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 thezozo_contact_solver.setoperator (with auto type coercion), reads fall through to the sceneβs addon state or SSH state.gravityis an alias forgravity_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
DynParambuilder.- 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 fromSceneParam.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
selfso 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:
selffor 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:
selffor 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"), afloat.strength β Wind strength (only for
"wind").
- Returns:
selffor chaining.
Example:
solver.param.dyn("wind").time(30).hold().time(31).change((0, 1, 0), strength=5.0)
- class Group#
A dynamics group proxy.
Created via
Solver.create_group(). Material parameters are accessed viaparam. Every mutating method returnsselfso 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_Nslot index this group occupies.This is the index every group operator addresses, resolved through
object_group_{index}. It is notObjectGroup.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 throughSolver.create_group().Example:
for g in solver.get_groups(): if g.type == "ROD": g.param.length_factor = 0.97
- property param#
- Type:
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](default1.0).
- Returns:
selffor 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:
selffor 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:
selffor 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_keyframescollection. Call once withframe=1for an initial-velocity launch; call again with higherframevalues 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"(theangular_axis_customvector). Ints0/1/2are accepted asPC1/PC2/PC3for convenience. Ignored whenangular_speed == 0.angular_speed β Signed spin speed in degrees per second (0 = no spin).
angular_axis_custom β World-space
(x, y, z)axis used whenangular_axis == "CUSTOM"(normalized before use).enable_translational β Overwrite the translational velocity at this frame. When
Falsethe keyframe leaves translation alone (e.g. a pure-spin keyframe).enable_angular β Overwrite the angular velocity at this frame. Defaults to
Truewhenangular_speedis non-zero, elseFalse. Pass explicitly to override.
- Returns:
selffor 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), or2(thinnest, the usual axle for a flat gear or disk). Defaults to2.enable β Set
Falseto clear the hinge and let the body move freely. Defaults toTrue.
- Returns:
selffor chaining.- Raises:
ValueError β If the group is not PDRD, the object is not assigned to it, or
pca_axisis 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 beNonefor meshes (meshes use existing vertex groups).
- Returns:
A
Pinproxy 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 noindiceswere supplied, orindicesis 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
Pinproxies.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 raisesAttributeError.Whitelisted attributes:
Solver model:
solid_model,shell_model,rod_model(rod_modelcurrently accepts only"ARAP": the enum has a single item and ROD groups force-pin it to ARAP)Density:
solid_density,shell_density,rod_densityYoungβs modulus:
solid_young_modulus,shell_young_modulus,rod_young_modulusPoisson ratio:
solid_poisson_ratio,shell_poisson_ratioContact:
friction,use_group_bounding_box_diagonal,contact_gap,contact_gap_rat,contact_offset,contact_offset_rat. Whenuse_group_bounding_box_diagonalisTrue(the default), the solver consumescontact_gap_rat* bbox-diagonal andcontact_offset_rat* bbox-diagonal; set it toFalseto consume the absolutecontact_gap/contact_offsetvalues directly.Strain limit:
enable_strain_limit,strain_limit_percentInflation:
enable_inflate,inflate_pressurePlasticity:
enable_plasticity,plasticity,plasticity_thresholdBend plasticity:
enable_bend_plasticity,bend_plasticity,bend_plasticity_threshold,bend_rest_angle_sourceBend stiffness:
bend, drawn on Shell and Rod groupsRest-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 viaset_hinge()(not a group material).Sand-specific:
sand_grain_radius(meters),sand_particle_mass(grams; the encoder sends kilograms),sand_friction. OnceSolver.convert_to_particle_mesh()has run, the radius it seeded is stamped on the object and the encoder reads that in preference tosand_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 returnsselfso 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:
selffor 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". IfNone, inferred from other args (Nonecenter β"CENTROID").center_direction β Direction for
MAX_TOWARDSmode.center_vertex β Vertex index for
VERTEXmode.frame_start β Start frame.
frame_end β End frame.
transition β
"LINEAR"or"SMOOTH".
- Returns:
selffor 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
ABSOLUTEmode).center_mode β
"CENTROID","ABSOLUTE","MAX_TOWARDS", or"VERTEX". IfNone, inferred from other args (Nonecenter β"CENTROID").center_direction β Direction for
MAX_TOWARDSmode.center_vertex β Vertex index for
VERTEXmode.frame_start β Start frame.
frame_end β End frame.
transition β
"LINEAR"or"SMOOTH".
- Returns:
selffor 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:
selffor 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:
selffor 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:
selffor 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 returnsself.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:
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:
selffor 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:
selffor 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:
selffor 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:
selffor 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 returnsself.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:
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:
selffor 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:
selffor 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:
selffor 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:
selffor 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:
selffor 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:
selffor 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:
selffor 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.paramorSphere.param. Attribute access is whitelisted: reading or writing a name outside the whitelist raisesAttributeError.Whitelisted attributes:
friction: contact friction coefficientcontact_gap: contact gap thicknessthickness: wall/sphere shell thicknessenable_active_duration:Trueto limit collider lifetimeactive_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(). Eachadd_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
Trueto make the spline cyclic.
- Returns:
Zero-based index of the new spline within this curve. Use it with
set_material().- Raises:
ValueError β If
pointshas 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 withbpy.data.materials.new(...)before calling.
- Returns:
selffor chaining.- Raises:
IndexError β If
spline_indexis 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.