blast-stress-solver 0.4.1

Blast stress solver for destructible structures, with optional Rapier3D integration
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
# blast-stress-solver

Rust bindings for the Blast stress solver, packaged for straightforward native
and WebAssembly consumption from Cargo.

This crate gives you two main layers:

- `ExtStressSolver` for Blast-only stress, bond failure, and split handling
- `blast_stress_solver::rapier::DestructionRuntime` for plugging destructibles
  into an existing Rapier app while keeping `PhysicsPipeline::step(...)` in the
  consumer

`DestructibleSet` still exists as the lower-level escape hatch for advanced
integration and explicit non-contact force injection.

If you want to auto-generate bonds from arbitrary pre-fractured piece meshes,
enable the `authoring` feature and use `blast_stress_solver::authoring`.

## Installation

Core solver only:

```toml
[dependencies]
blast-stress-solver = "0.4.1"
```

With built-in scenario builders:

```toml
[dependencies]
blast-stress-solver = { version = "0.4.1", features = ["scenarios"] }
```

With Rapier integration and scenario builders:

```toml
[dependencies]
blast-stress-solver = { version = "0.4.1", features = ["rapier", "scenarios"] }
rapier3d = { version = "0.30", default-features = false, features = ["dim3", "f32"] }
```

With auto-bond authoring helpers:

```toml
[dependencies]
blast-stress-solver = { version = "0.4.1", features = ["authoring"] }
```

## Target support

The published crate currently ships packaged backends for:

- `aarch64-apple-darwin`
- `wasm32-unknown-unknown`

For other Apple/Linux native targets, the published crate falls back to
compiling the bundled Blast C++ sources on the consumer machine with a normal
C++17 toolchain. That removes the need to vendor the PhysX monorepo just to
support x86_64 macOS or Linux consumers.

The packaged native and `wasm32-unknown-unknown` backends both include the
public `authoring` API.

`wasm32-unknown-unknown` intentionally stays prepackaged: downstream Rust wasm
builds do not need Emscripten, wasi-sdk, or a second Blast-side loader.

For other Apple/Linux native targets, `authoring` still works through the same
bundled C++17 source-build fallback used by the core solver.

Advanced overrides:

- `BLAST_STRESS_SOLVER_STATIC_LIB_PATH=/abs/path/to/libblast_stress_solver_ffi.a`
- `BLAST_STRESS_SOLVER_LIB_DIR=/abs/path/to/lib/dir`
- `BLAST_STRESS_SOLVER_FORCE_SOURCE_BUILD=1` for the bundled Apple/Linux native
  fallback

## Quick Start

The easiest way to understand the API is in two steps:

1. Build a destructible wall and drive the Blast solver directly.
2. Take the same wall and let `DestructionRuntime` integrate it into an
   existing Rapier world.

The examples below are intentionally explicit about which code belongs to your
application and which code is specific to `blast-stress-solver`.

### 1. Blast-only example: build and collapse a simple wall

This version does not use Rapier yet. It shows the minimum Blast-side workflow:

1. Build a wall scenario.
2. Create `ExtStressSolver` from its nodes and bonds.
3. Apply gravity and an impact force.
4. Ask Blast for fracture commands and apply them.

```rust
use blast_stress_solver::scenarios::{build_wall_scenario, WallOptions};
use blast_stress_solver::{ExtStressSolver, ForceMode, SolverSettings, Vec3};

fn main() {
    // Step 1: consumer app setup.
    // Choose a small wall so the example stays readable.
    let wall_options = WallOptions {
        span: 4.0,
        height: 2.0,
        thickness: 0.30,
        span_segments: 8,
        height_segments: 4,
        layers: 1,
        deck_mass: 200.0,
        ..WallOptions::default()
    };
    let wall = build_wall_scenario(&wall_options);

    // Step 2: Blast-specific setup.
    // Convert the scenario into the node/bond descriptors expected by the solver.
    let (nodes, bonds) = wall.to_solver_descs();

    // Use intentionally weak limits so the wall can visibly fail under load.
    let settings = SolverSettings {
        max_solver_iterations_per_frame: 64,
        compression_elastic_limit: 0.01,
        compression_fatal_limit: 0.05,
        tension_elastic_limit: 0.01,
        tension_fatal_limit: 0.05,
        shear_elastic_limit: 0.01,
        shear_fatal_limit: 0.05,
        ..SolverSettings::default()
    };

    let mut solver =
        ExtStressSolver::new(&nodes, &bonds, &settings).expect("failed to create solver");

    // Pick the top-center block as the impact target.
    let impact_node =
        (wall_options.height_segments - 1) * wall_options.span_segments + wall_options.span_segments / 2;
    let impact_position = wall.nodes[impact_node as usize].centroid;

    // Step 3: consumer app simulation loop.
    // In a real game or app, this would run once per frame.
    for frame in 0..30 {
        // Blast-specific: tell the solver about persistent gravity.
        solver.add_gravity(Vec3::new(0.0, -9.81, 0.0));

        // Consumer app event: inject a one-time impulse-like load.
        if frame == 0 {
            solver.add_force(
                impact_node,
                impact_position,
                Vec3::new(5_000.0, 0.0, 0.0),
                ForceMode::Force,
            );
        }

        // Blast-specific: solve stress for the accumulated loads.
        solver.update();

        // Blast-specific: convert overstressed bonds into fracture commands.
        let commands = solver.generate_fracture_commands();
        let broken_bonds: usize = commands.iter().map(|cmd| cmd.bond_fractures.len()).sum();

        // Blast-specific: apply those fractures and let Blast split actors.
        let split_events = solver.apply_fracture_commands(&commands);

        println!(
            "frame={frame:02} actors={} overstressed={} broken_bonds={} split_events={}",
            solver.actor_count(),
            solver.overstressed_bond_count(),
            broken_bonds,
            split_events.len(),
        );

        if !split_events.is_empty() {
            println!("wall split after frame {frame}");
            break;
        }
    }
}
```

What to notice:

- `build_wall_scenario(...)` is optional convenience. If you already have your
  own nodes and bonds, you can skip the `scenarios` feature and build
  `NodeDesc` / `BondDesc` directly.
- The bottom row of the built-in wall is automatically marked as support
  (`mass == 0.0`), so the wall has something fixed to break away from.
- `ExtStressSolver` owns the Blast family/actor state. After fractures are
  applied, `actor_count()` grows as disconnected pieces split apart.

## Authoring quick start

`authoring` is the Rust-side equivalent of the JS package's triangle-based
auto-bonding path. The intended flow is:

1. collect one triangle soup per piece
2. mark whether each piece is bondable
3. call `create_bonds_from_triangles(...)` or
   `build_scenario_from_pieces(...)`

Runnable example:

```bash
cargo run --example auto_bond_wall --features authoring
```

The full copy-pasteable source lives in
`examples/auto_bond_wall.rs`. It builds two touching cuboids, runs the public
auto-bonding API, and prints the resulting bond.

Notes:

- triangle vertices must already be expressed in the target scenario space
- `bondable` means "participates in bond generation", not "fixed to the world"
- a fixed support is still represented by `ScenarioNode { mass: 0.0, .. }`
- `build_scenario_from_pieces(...)` is the highest-level convenience API
- `create_bonds_from_triangles(...)` is the lower-level API when you already
  manage `ScenarioDesc` assembly yourself
- the packaged Bevy demo in `blast-stress-demo-rs` now rebuilds its embedded
  fractured scene packs through this same public authoring API at load time

### 2. Rapier example: the same wall, now integrated into an existing Rapier app

This version shows the recommended runtime for real consumers.

`DestructionRuntime` does **not** replace Rapier. Your app still owns the Rapier
world, the physics pipeline, and the frame loop. `blast-stress-solver` handles
the destruction-specific part:

1. keep a Blast stress graph for the structure
2. derive accepted impacts from Rapier contacts
3. detect failed bonds and split Blast actors
4. create/update/destroy the corresponding Rapier bodies and colliders
5. resimulate the same frame when topology changes

```rust
use blast_stress_solver::rapier::{
    ContactImpactOptions, DestructionRuntime, DestructionRuntimeOptions, FracturePolicy,
    GracePeriodOptions, RapierWorldAccess,
};
use blast_stress_solver::scenarios::{build_wall_scenario, WallOptions};
use blast_stress_solver::{SolverSettings, Vec3};
use rapier3d::prelude::*;

fn main() {
    // Step 1: consumer app setup.
    // Build the same simple wall shape.
    let wall_options = WallOptions {
        span: 4.0,
        height: 2.0,
        thickness: 0.30,
        span_segments: 8,
        height_segments: 4,
        layers: 1,
        deck_mass: 200.0,
        ..WallOptions::default()
    };
    let wall = build_wall_scenario(&wall_options);

    // Step 2: Blast-specific setup.
    // Create the destruction controller that bridges Blast and Rapier.
    let settings = SolverSettings {
        max_solver_iterations_per_frame: 64,
        compression_elastic_limit: 0.01,
        compression_fatal_limit: 0.05,
        tension_elastic_limit: 0.01,
        tension_fatal_limit: 0.05,
        shear_elastic_limit: 0.01,
        shear_fatal_limit: 0.05,
        ..SolverSettings::default()
    };
    let fracture_policy = FracturePolicy {
        idle_skip: false,
        ..FracturePolicy::default()
    };

    let runtime_options = DestructionRuntimeOptions {
        contact_impacts: ContactImpactOptions {
            min_total_impulse: 8.0,
            min_external_speed: 0.75,
            ..ContactImpactOptions::default()
        },
        grace: GracePeriodOptions {
            sibling_steps: 1,
            impact_source_steps: 2,
        },
        ..DestructionRuntimeOptions::default()
    };

    let mut destructible = DestructionRuntime::from_scenario(
        &wall,
        settings,
        Vec3::new(0.0, -9.81, 0.0),
        fracture_policy,
        runtime_options,
    )
    .expect("failed to create destruction runtime");

    // Step 3: consumer app setup.
    // You still own the Rapier world and physics pipeline.
    let mut rigid_bodies = RigidBodySet::new();
    let mut colliders = ColliderSet::new();
    let mut island_manager = IslandManager::new();
    let mut impulse_joints = ImpulseJointSet::new();
    let mut multibody_joints = MultibodyJointSet::new();

    let mut physics_pipeline = PhysicsPipeline::new();
    let mut broad_phase = BroadPhaseBvh::new();
    let mut narrow_phase = NarrowPhase::new();
    let mut ccd_solver = CCDSolver::new();
    let integration_parameters = IntegrationParameters::default();
    let gravity = Vector::new(0.0, -9.81, 0.0);

    // Blast-specific: create the initial Rapier bodies/colliders that match
    // the current Blast actor table.
    destructible.initialize(&mut rigid_bodies, &mut colliders);

    // Consumer app: add an ordinary Rapier projectile.
    let projectile = RigidBodyBuilder::dynamic()
        .translation(vector![-6.0, 1.4, 0.0])
        .linvel(vector![18.0, 0.0, 0.0])
        .additional_mass(20.0)
        .build();
    let projectile_handle = rigid_bodies.insert(projectile);
    let projectile_collider = ColliderBuilder::ball(0.35).restitution(0.05).build();
    colliders.insert_with_parent(projectile_collider, projectile_handle, &mut rigid_bodies);

    // Step 4: consumer app frame loop.
    for frame in 0..120 {
        let mut world = RapierWorldAccess {
            bodies: &mut rigid_bodies,
            colliders: &mut colliders,
            island_manager: &mut island_manager,
            broad_phase: &mut broad_phase,
            narrow_phase: &mut narrow_phase,
            impulse_joints: &mut impulse_joints,
            multibody_joints: &mut multibody_joints,
            ccd_solver: &mut ccd_solver,
        };

        // Consumer app:
        // Advance Rapier as usual, but let the runtime wrap the step so it can
        // derive impacts from ordinary Rapier contacts, fracture, split, and
        // resimulate when needed.
        let fracture_step = destructible.step_frame(
            frame as f32 * integration_parameters.dt,
            integration_parameters.dt,
            &mut world,
            &(),
            &(),
            |pass, world| {
                physics_pipeline.step(
                    &gravity,
                    &integration_parameters,
                    world.island_manager,
                    world.broad_phase,
                    world.narrow_phase,
                    world.bodies,
                    world.colliders,
                    world.impulse_joints,
                    world.multibody_joints,
                    world.ccd_solver,
                    pass,
                    pass,
                );
            },
        );

        println!(
            "frame={frame:03} passes={} accepted_impacts={} fractures={} split_events={} new_bodies={} actors={} bodies={}",
            fracture_step.rapier_passes,
            fracture_step.accepted_impacts,
            fracture_step.fractures,
            fracture_step.split_events,
            fracture_step.new_bodies,
            destructible.actor_count(),
            destructible.body_count(),
        );

        if fracture_step.split_events > 0 {
            println!("wall has started splitting into separate Rapier bodies");
            break;
        }
    }
}
```

What to notice:

- Your app still owns `RigidBodySet`, `ColliderSet`, the Rapier pipeline, and
  the main frame loop.
- `DestructionRuntime::initialize(...)` is the point where Blast’s initial
  actor graph becomes actual Rapier bodies and colliders.
- `DestructionRuntime::step_frame(...)` is the recommended integration point.
  That is where this crate inspects Rapier contacts, injects accepted impacts,
  applies fractures, rewrites Rapier body/collider topology, and requests
  same-frame resimulation when needed.
- `physics_pipeline.step(...)` is still your responsibility. This crate
  integrates into Rapier instead of hiding it.

## WebAssembly

This crate supports downstream Rust applications that build for
`wasm32-unknown-unknown`.

The intended model is:

- your application depends on `blast-stress-solver` as a normal Rust dependency
- your application builds one final wasm output
- no Blast-specific sidecar wasm or JS loader is required

### Downstream `wasm-bindgen` example

```toml
[lib]
crate-type = ["cdylib"]

[dependencies]
blast-stress-solver = "0.4.1"
wasm-bindgen = "0.2"
```

```rust
use blast_stress_solver::{BondDesc, ExtStressSolver, NodeDesc, SolverSettings, Vec3};
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn blast_tick() -> u32 {
    let nodes = [
        NodeDesc { centroid: Vec3::new(0.0, 0.0, 0.0), mass: 0.0, volume: 1.0 },
        NodeDesc { centroid: Vec3::new(0.0, 1.0, 0.0), mass: 10.0, volume: 1.0 },
    ];
    let bonds = [BondDesc {
        centroid: Vec3::new(0.0, 0.5, 0.0),
        normal: Vec3::new(0.0, 1.0, 0.0),
        area: 1.0,
        node0: 0,
        node1: 1,
    }];

    let Some(mut solver) = ExtStressSolver::new(&nodes, &bonds, &SolverSettings::default()) else {
        return u32::MAX;
    };

    solver.add_gravity(Vec3::new(0.0, -9.81, 0.0));
    solver.update();
    solver.node_count()
}
```

That build still emits one final application wasm file. There is no extra Blast
runtime bundle to host or load manually.

## Features

- `rapier`: enables Rapier3D integration helpers
- `scenarios`: enables built-in wall, tower, and bridge scenario builders

## Publishing

Do **not** run `cargo publish` directly against
`blast/blast-stress-solver-rs/Cargo.toml`. The source crate is marked
`publish = false` on purpose so local publishes cannot accidentally ship the
monorepo build layout.

Local crates.io preflight:

```bash
scripts/publish-blast-stress-solver.sh --dry-run
```

Local publish:

```bash
scripts/publish-blast-stress-solver.sh
```

Tag-driven GitHub Actions release:

1. bump `version` in `blast/blast-stress-solver-rs/Cargo.toml`
2. commit and push the branch
3. push a matching tag such as:

```bash
git tag blast-stress-solver-v<version>
git push origin blast-stress-solver-v<version>
```

That workflow stages the crate, runs the packaged native/wasm/demo-consumer
proofs, dry-runs publish, publishes to crates.io, and creates a GitHub release.

## Notes

- Local publish-style proof:
  `scripts/assemble-blast-stress-solver-package.sh --verify-demo-consumer`
  stages the crate, verifies the packaged native and wasm smoke consumers, and
  runs the real `blast/blast-stress-demo-rs` headless fracture test against the
  staged package.
- The published crate is distributed as packaged Rust source plus:
  - prebuilt backend artifacts for `aarch64-apple-darwin` and `wasm32-unknown-unknown`
  - bundled native C++ sources for the Apple/Linux source-build fallback
- The monorepo development setup can still build the backend from source, but
  consumers no longer need to vendor the monorepo to get that native fallback.