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
use crate::types::*;
use std::collections::{HashMap, HashSet};

const EPS: f32 = 1e-8;

/// Options for building a beam bridge scenario.
/// Matches the JS `BeamBridgeOptions` from `bridgeScenario.ts`.
#[derive(Clone, Debug)]
pub struct BridgeOptions {
    pub span: f64,
    pub deck_width: f64,
    pub deck_thickness: f64,
    pub span_segments: u32,
    pub width_segments: u32,
    pub thickness_layers: u32,
    pub deck_mass: f64,
    pub pier_height: f64,
    pub supports_per_side: u32,
    pub support_width_segments: u32,
    pub support_depth_segments: u32,
    pub footing_thickness: f64,
    pub area_scale: f64,
    pub add_diagonals: bool,
    pub diag_scale: f64,
    pub normalize_areas: bool,
    pub bonds_x: bool,
    pub bonds_y: bool,
    pub bonds_z: bool,
}

impl Default for BridgeOptions {
    fn default() -> Self {
        Self {
            span: 18.0,
            deck_width: 5.0,
            deck_thickness: 0.6,
            span_segments: 30,
            width_segments: 10,
            thickness_layers: 2,
            deck_mass: 60_000.0,
            pier_height: 2.8,
            supports_per_side: 4,
            support_width_segments: 2,
            support_depth_segments: 2,
            footing_thickness: 0.12,
            area_scale: 0.05,
            add_diagonals: true,
            diag_scale: 0.6,
            normalize_areas: true,
            bonds_x: true,
            bonds_y: true,
            bonds_z: true,
        }
    }
}

fn sub(a: Vec3, b: Vec3) -> Vec3 {
    Vec3::new(a.x - b.x, a.y - b.y, a.z - b.z)
}

fn nrm(p: Vec3) -> Vec3 {
    let l = (p.x * p.x + p.y * p.y + p.z * p.z).sqrt();
    if l <= EPS {
        Vec3::new(0.0, 0.0, 0.0)
    } else {
        Vec3::new(p.x / l, p.y / l, p.z / l)
    }
}

/// Pick the dominant axis of a normal vector (largest absolute component).
/// Matches the JS `pick` function: ax >= ay && ax >= az ? 'x' : (ay >= az ? 'y' : 'z')
fn pick_axis(n: &Vec3) -> usize {
    let ax = n.x.abs();
    let ay = n.y.abs();
    let az = n.z.abs();
    if ax >= ay && ax >= az {
        0
    } else if ay >= az {
        1
    } else {
        2
    }
}

fn clamp(val: i32, lo: i32, hi: i32) -> i32 {
    val.max(lo).min(hi)
}

fn uniq(arr: &[i32]) -> Vec<i32> {
    let mut seen = HashSet::new();
    let mut result = Vec::new();
    for &v in arr {
        if seen.insert(v) {
            result.push(v);
        }
    }
    result
}

/// Build a beam bridge scenario matching the JS `buildBeamBridgeScenario` exactly.
pub fn build_bridge_scenario(opts: &BridgeOptions) -> ScenarioDesc {
    let span = opts.span;
    let deck_width = opts.deck_width;
    let deck_thickness = opts.deck_thickness;
    let raw_seg_x = opts.span_segments;
    let raw_seg_z = opts.width_segments;
    let raw_seg_y = opts.thickness_layers;
    let deck_mass = opts.deck_mass;
    let pier_height = opts.pier_height;
    let supports_per_side = opts.supports_per_side;
    let support_width_segments = opts.support_width_segments;
    let support_depth_segments = opts.support_depth_segments;
    let footing_thickness = opts.footing_thickness;
    let area_scale = opts.area_scale;
    let add_diagonals = opts.add_diagonals;
    let diag_scale = opts.diag_scale;
    let normalize_areas = opts.normalize_areas;
    let bonds_x = opts.bonds_x;
    let bonds_y = opts.bonds_y;
    let bonds_z = opts.bonds_z;

    let seg_x = (raw_seg_x as i32).max(1) as u32;
    let seg_y = (raw_seg_y as i32).max(1) as u32;
    let seg_z = (raw_seg_z as i32).max(1) as u32;

    let cell_x = span / seg_x as f64;
    let cell_y = deck_thickness / seg_y as f64;
    let cell_z = deck_width / seg_z as f64;

    let post_layers = (pier_height / cell_y).ceil().max(1.0) as u32;
    let deck_bottom_y = post_layers as f64 * cell_y;
    let deck_origin_x = -span * 0.5 + 0.5 * cell_x;
    let deck_origin_y = deck_bottom_y + 0.5 * cell_y;
    let deck_origin_z = -deck_width * 0.5 + 0.5 * cell_z;

    // gridDeck[ix][iy][iz] -> node index (-1 means empty)
    let mut grid_deck: Vec<Vec<Vec<i32>>> =
        vec![vec![vec![-1i32; seg_z as usize]; seg_y as usize]; seg_x as usize];

    let mut nodes: Vec<ScenarioNode> = Vec::new();
    let mut bonds: Vec<ScenarioBond> = Vec::new();
    let mut node_sizes: Vec<Vec3> = Vec::new();

    // Build deck nodes
    let deck_cell_volume = (cell_x * cell_y * cell_z) as f32;
    let mut deck_total_volume: f64 = 0.0;
    for ix in 0..seg_x {
        for iy in 0..seg_y {
            for iz in 0..seg_z {
                let px = deck_origin_x + ix as f64 * cell_x;
                let py = deck_origin_y + iy as f64 * cell_y;
                let pz = deck_origin_z + iz as f64 * cell_z;
                let node_idx = nodes.len() as i32;
                nodes.push(ScenarioNode {
                    centroid: Vec3::new(px as f32, py as f32, pz as f32),
                    mass: deck_cell_volume,
                    volume: deck_cell_volume,
                });
                node_sizes.push(Vec3::new(cell_x as f32, cell_y as f32, cell_z as f32));
                grid_deck[ix as usize][iy as usize][iz as usize] = node_idx;
                deck_total_volume += deck_cell_volume as f64;
            }
        }
    }

    // Scale masses so total deck mass matches
    let mass_scale: f64 = if deck_total_volume > 0.0 {
        deck_mass / deck_total_volume
    } else {
        0.0
    };
    if mass_scale != 1.0 {
        for n in nodes.iter_mut() {
            if n.volume > 0.0 {
                n.mass = n.volume * mass_scale as f32;
            }
        }
    }

    // Bond helpers
    let area_x = (cell_y * cell_z * area_scale) as f32;
    let area_y = (cell_x * cell_z * area_scale) as f32;
    let area_z = (cell_x * cell_y * area_scale) as f32;

    // Closure to add a bond between two node indices
    // We use a macro-like approach since closures borrowing nodes and bonds simultaneously is tricky
    let add_bond =
        |bonds: &mut Vec<ScenarioBond>, nodes: &[ScenarioNode], a: i32, b: i32, area: f32| {
            if a < 0 || b < 0 {
                return;
            }
            let na = &nodes[a as usize];
            let nb = &nodes[b as usize];
            let c = Vec3::new(
                (na.centroid.x + nb.centroid.x) * 0.5,
                (na.centroid.y + nb.centroid.y) * 0.5,
                (na.centroid.z + nb.centroid.z) * 0.5,
            );
            let n = nrm(sub(nb.centroid, na.centroid));
            bonds.push(ScenarioBond {
                node0: a as u32,
                node1: b as u32,
                centroid: c,
                normal: n,
                area: area.max(EPS),
            });
        };

    // Deck connectivity
    for ix in 0..seg_x {
        for iy in 0..seg_y {
            for iz in 0..seg_z {
                let cur = grid_deck[ix as usize][iy as usize][iz as usize];
                if cur < 0 {
                    continue;
                }
                if bonds_x && ix + 1 < seg_x {
                    add_bond(
                        &mut bonds,
                        &nodes,
                        cur,
                        grid_deck[(ix + 1) as usize][iy as usize][iz as usize],
                        area_x,
                    );
                }
                if bonds_y && iy + 1 < seg_y {
                    add_bond(
                        &mut bonds,
                        &nodes,
                        cur,
                        grid_deck[ix as usize][(iy + 1) as usize][iz as usize],
                        area_y,
                    );
                }
                if bonds_z && iz + 1 < seg_z {
                    add_bond(
                        &mut bonds,
                        &nodes,
                        cur,
                        grid_deck[ix as usize][iy as usize][(iz + 1) as usize],
                        area_z,
                    );
                }
                if add_diagonals {
                    if bonds_x && bonds_z && ix + 1 < seg_x && iz + 1 < seg_z {
                        add_bond(
                            &mut bonds,
                            &nodes,
                            cur,
                            grid_deck[(ix + 1) as usize][iy as usize][(iz + 1) as usize],
                            0.5 * (area_x + area_z) * diag_scale as f32,
                        );
                    }
                    if bonds_x && bonds_y && ix + 1 < seg_x && iy + 1 < seg_y {
                        add_bond(
                            &mut bonds,
                            &nodes,
                            cur,
                            grid_deck[(ix + 1) as usize][(iy + 1) as usize][iz as usize],
                            0.5 * (area_x + area_y) * diag_scale as f32,
                        );
                    }
                    if bonds_y && bonds_z && iy + 1 < seg_y && iz + 1 < seg_z {
                        add_bond(
                            &mut bonds,
                            &nodes,
                            cur,
                            grid_deck[ix as usize][(iy + 1) as usize][(iz + 1) as usize],
                            0.5 * (area_y + area_z) * diag_scale as f32,
                        );
                    }
                }
            }
        }
    }

    // Build posts under first and last span columns
    let post_x_cols: [u32; 2] = [0, seg_x - 1];
    let post_top_y_layer: u32 = 0;
    let post_top_y = deck_origin_y - 0.5 * cell_y;

    let post_span = (supports_per_side as i32).max(1);
    let mut slots: Vec<i32> = Vec::new();
    for i in 0..post_span {
        let t = if post_span == 1 {
            0.5
        } else {
            i as f64 / (post_span - 1) as f64
        };
        let val = (t * (seg_z as f64 - 1.0)).round() as i32;
        slots.push(clamp(val, 0, seg_z as i32 - 1));
    }

    for &ix_edge in &post_x_cols {
        let ix_cover_raw: Vec<i32> = (0..support_depth_segments as i32)
            .map(|k| {
                if ix_edge == 0 {
                    clamp(ix_edge as i32 + k, 0, seg_x as i32 - 1)
                } else {
                    clamp(ix_edge as i32 - k, 0, seg_x as i32 - 1)
                }
            })
            .collect();
        let ix_cover = uniq(&ix_cover_raw);
        let ix_cover_set: HashSet<i32> = ix_cover.iter().copied().collect();

        for &base_z in &slots {
            let cover_z_raw: Vec<i32> = (0..support_width_segments as i32)
                .map(|k| {
                    clamp(
                        base_z + k - ((support_width_segments as i32 - 1) / 2),
                        0,
                        seg_z as i32 - 1,
                    )
                })
                .collect();
            let cover_z = uniq(&cover_z_raw);
            let cover_z_set: HashSet<i32> = cover_z.iter().copied().collect();
            let mut post_map: HashMap<(i32, i32, i32), i32> = HashMap::new();

            // Create stacks
            for &iz in &cover_z {
                for &ixp in &ix_cover {
                    for py in 0..post_layers as i32 {
                        let y_center = post_top_y - py as f64 * cell_y - 0.5 * cell_y;
                        let node_idx = nodes.len() as i32;
                        let px = deck_origin_x + ixp as f64 * cell_x;
                        let pz = deck_origin_z + iz as f64 * cell_z;
                        let volume = (cell_x * cell_y * cell_z) as f32;
                        nodes.push(ScenarioNode {
                            centroid: Vec3::new(px as f32, y_center as f32, pz as f32),
                            mass: volume * mass_scale as f32,
                            volume,
                        });
                        node_sizes.push(Vec3::new(cell_x as f32, cell_y as f32, cell_z as f32));
                        post_map.insert((ixp, py, iz), node_idx);

                        if py > 0 {
                            if let Some(&prev_idx) = post_map.get(&(ixp, py - 1, iz)) {
                                add_bond(&mut bonds, &nodes, prev_idx, node_idx, area_y);
                            }
                        } else {
                            let deck_index =
                                grid_deck[ixp as usize][post_top_y_layer as usize][iz as usize];
                            add_bond(&mut bonds, &nodes, node_idx, deck_index, area_y);
                        }
                    }

                    // Footing under this column (mass=0 support)
                    let foot_center_y =
                        post_top_y - post_layers as f64 * cell_y - 0.5 * footing_thickness;
                    let f_idx = nodes.len() as i32;
                    let f_px = deck_origin_x + ixp as f64 * cell_x;
                    let f_pz = deck_origin_z + iz as f64 * cell_z;
                    nodes.push(ScenarioNode {
                        centroid: Vec3::new(f_px as f32, foot_center_y as f32, f_pz as f32),
                        mass: 0.0,
                        volume: 0.0,
                    });
                    node_sizes.push(Vec3::new(
                        cell_x as f32,
                        footing_thickness as f32,
                        cell_z as f32,
                    ));
                    if let Some(&lowest_post_idx) = post_map.get(&(ixp, post_layers as i32 - 1, iz))
                    {
                        add_bond(&mut bonds, &nodes, f_idx, lowest_post_idx, area_y);
                    }
                }
            }

            // Lateral bonds within the post cluster
            for &iz in &cover_z {
                for &ixp in &ix_cover {
                    for py in 0..post_layers as i32 {
                        let cur = match post_map.get(&(ixp, py, iz)) {
                            Some(&v) => v,
                            None => continue,
                        };
                        let nx = if ix_edge == 0 { ixp + 1 } else { ixp - 1 };
                        if ix_cover_set.contains(&nx) {
                            if let Some(&nb) = post_map.get(&(nx, py, iz)) {
                                add_bond(&mut bonds, &nodes, cur, nb, area_x);
                            }
                        }
                        let nz = iz + 1;
                        if cover_z_set.contains(&nz) {
                            if let Some(&nbz) = post_map.get(&(ixp, py, nz)) {
                                add_bond(&mut bonds, &nodes, cur, nbz, area_z);
                            }
                        }
                    }
                }
            }
        }
    }

    // Isotropic area normalization — apply uniform scale factor (geometric mean
    // of per-axis scales) to avoid directional bond strength bias.
    if normalize_areas && !bonds.is_empty() {
        let size_x = span as f32;
        let size_y = (deck_thickness + pier_height + footing_thickness) as f32;
        let size_z = deck_width as f32;
        let target = [
            size_y * size_z, // x
            size_x * size_z, // y
            size_x * size_y, // z
        ];
        let mut sum = [0.0f32; 3];
        for b in bonds.iter() {
            let axis = pick_axis(&b.normal);
            sum[axis] += b.area;
        }
        let mut axis_scales: Vec<f32> = Vec::new();
        for k in 0..3 {
            if sum[k] > 0.0 {
                axis_scales.push(target[k] / sum[k]);
            }
        }
        let uniform_scale = if !axis_scales.is_empty() {
            let product: f32 = axis_scales.iter().copied().product();
            product.powf(1.0 / axis_scales.len() as f32)
        } else {
            1.0
        };
        for b in bonds.iter_mut() {
            b.area *= uniform_scale;
        }
    }

    ScenarioDesc {
        nodes,
        bonds,
        node_sizes,
        collider_shapes: Vec::new(),
    }
}