carapace_wasm/input_v1/tables.rs
1//! Bulk per-(entity-kind) tables of the wire format (`docs/input-format.md`), one
2//! set for both profiles (`header.ndm` 2 or 3). Dimension-agnostic entities
3//! (nodes, trusses, zero-lengths, fibers, constraints, loads) have one table;
4//! formulations that differ between 2D and 3D (beam-columns) have one table
5//! each, suffixed `2d`/`3d`, and the table of the other profile must be empty
6//! (`DecodeError::TableNotInProfile`).
7//!
8//! Node/element/pattern references inside these tables are already
9//! resolved to dense 0-based row indices by the (not-yet-written)
10//! `pysees` compiler — the "duplicate/missing tags and every reference
11//! between entities" validation the compiler is responsible for
12//! happens upstream of decode, so decode only ever sees plain array
13//! indices, never sparse OpenSees-style tags.
14//!
15//! Each field is a plain `Vec`, not yet the transferable `Float64Array`/
16//! `Uint8Array` values the wire format ultimately specifies for
17//! `postMessage`: `boundary.rs`'s `serde-wasm-bindgen` decoding today
18//! accepts an ordinary JS array for each of these (or a typed array,
19//! copied element-by-element) rather than transferring one. Once real
20//! transferable-array support lands, each `Vec<f64>`/`Vec<u32>` here is
21//! exactly the shape a `Float64Array`/`Uint32Array` copies into. Every table
22//! of [`super::CarapaceInputV1`] may be omitted, which is the same as empty.
23//!
24//! `#[serde(rename_all = "camelCase")]` throughout so the JS/TS shape
25//! matches the PySees naming (`nodeI`, not `node_i`).
26
27use serde::{Deserialize, Serialize};
28
29/// Node table: `coords` stride `ndm` (x, y[, z]); `fixed` one bitmask byte per
30/// node (bit `k` = DOF `k`: ux, uy, rz in 2D; ux, uy, uz, rx, ry, rz in 3D);
31/// mass is sparse, addressed via a parallel node-index array since most nodes
32/// carry none.
33#[derive(Debug, Clone, Default, Serialize, Deserialize, tsify::Tsify)]
34#[serde(rename_all = "camelCase")]
35pub struct NodeTable {
36 pub coords: Vec<f64>,
37 pub fixed: Vec<u8>,
38 pub mass_node_index: Vec<u32>,
39 /// Stride `ndf` (3 in 2D, 6 in 3D), parallel to `mass_node_index`.
40 pub mass: Vec<f64>,
41}
42
43impl NodeTable {
44 pub fn node_count(&self, ndm: usize) -> usize {
45 self.coords.len() / ndm
46 }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, tsify::Tsify)]
50#[serde(rename_all = "camelCase")]
51pub enum TransformSpec {
52 Linear,
53 PDelta,
54 Corotational,
55}
56
57/// `GeomTransf3`'s wire mirror. No `Corotational3` — spatial corotational
58/// geometry doesn't exist in `core` yet (`GeomTransf3`'s own doc comment).
59#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, tsify::Tsify)]
60#[serde(
61 tag = "kind",
62 rename_all = "camelCase",
63 rename_all_fields = "camelCase"
64)]
65pub enum TransformSpec3 {
66 Linear3 { vec_xz: [f64; 3] },
67 PDelta3 { vec_xz: [f64; 3] },
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, tsify::Tsify)]
71#[serde(
72 tag = "kind",
73 rename_all = "camelCase",
74 rename_all_fields = "camelCase"
75)]
76pub enum IntegrationSpec {
77 Legendre { points: u32 },
78 Lobatto { points: u32 },
79}
80
81/// Same fields in 2D and 3D.
82#[derive(Debug, Clone, Default, Serialize, Deserialize, tsify::Tsify)]
83#[serde(rename_all = "camelCase")]
84pub struct TrussTable {
85 pub node_i: Vec<u32>,
86 pub node_j: Vec<u32>,
87 pub area: Vec<f64>,
88 /// Index into the material arena.
89 pub material: Vec<u32>,
90 pub density: Vec<f64>,
91}
92
93#[derive(Debug, Clone, Default, Serialize, Deserialize, tsify::Tsify)]
94#[serde(rename_all = "camelCase")]
95pub struct ElasticBeamColumn2dTable {
96 pub node_i: Vec<u32>,
97 pub node_j: Vec<u32>,
98 pub e: Vec<f64>,
99 pub a: Vec<f64>,
100 pub iz: Vec<f64>,
101 pub transform: Vec<TransformSpec>,
102 pub density: Vec<f64>,
103}
104
105#[derive(Debug, Clone, Default, Serialize, Deserialize, tsify::Tsify)]
106#[serde(rename_all = "camelCase")]
107pub struct ElasticBeamColumn3dTable {
108 pub node_i: Vec<u32>,
109 pub node_j: Vec<u32>,
110 pub e: Vec<f64>,
111 pub g: Vec<f64>,
112 pub a: Vec<f64>,
113 /// Torsional constant.
114 pub j: Vec<f64>,
115 pub iy: Vec<f64>,
116 pub iz: Vec<f64>,
117 pub transform: Vec<TransformSpec3>,
118 pub density: Vec<f64>,
119}
120
121/// Shared shape for 2D `DispBeamColumn` and `ForceBeamColumn` — both are one
122/// prismatic fiber section (see [`FiberTable`]) replicated across
123/// integration points by `core`'s own element constructors.
124#[derive(Debug, Clone, Default, Serialize, Deserialize, tsify::Tsify)]
125#[serde(rename_all = "camelCase")]
126pub struct FiberBeamColumn2dTable {
127 pub node_i: Vec<u32>,
128 pub node_j: Vec<u32>,
129 /// Index into `FiberTable::section_offsets`.
130 pub fiber_section: Vec<u32>,
131 pub integration: Vec<IntegrationSpec>,
132 pub corotational: Vec<bool>,
133 pub density: Vec<f64>,
134}
135
136/// Shared shape for `DispBeamColumn3` and `ForceBeamColumn3` — both are one
137/// prismatic biaxial fiber section (see [`FiberTable`]) replicated across
138/// integration points by `core`'s own element constructors. Unlike
139/// [`FiberBeamColumn2dTable`], there is no `corotational` field: neither
140/// spatial fiber element supports it yet, and no separate `transform` field —
141/// `g`/`j`/`vec_xz` are passed directly to the constructor, since these
142/// elements only ever use `Linear`-type geometry.
143#[derive(Debug, Clone, Default, Serialize, Deserialize, tsify::Tsify)]
144#[serde(rename_all = "camelCase")]
145pub struct FiberBeamColumn3dTable {
146 pub node_i: Vec<u32>,
147 pub node_j: Vec<u32>,
148 pub g: Vec<f64>,
149 /// Torsional constant — fiber sections don't carry torsion (see
150 /// [`FiberTable`]'s doc comment), so it's supplied here as a decoupled
151 /// elastic `G*J` term, same as `core::DispBeamColumn3`/`ForceBeamColumn3`.
152 pub j: Vec<f64>,
153 /// A vector not parallel to the member axis, fixing the local y/z
154 /// orientation — see `core::GeomTransf3::Linear3`'s doc comment.
155 pub vec_xz: Vec<[f64; 3]>,
156 /// Index into `FiberTable::section_offsets`.
157 pub fiber_section: Vec<u32>,
158 pub integration: Vec<IntegrationSpec>,
159 pub density: Vec<f64>,
160}
161
162/// One sparse `friction` row of [`ZeroLengthTable`]: the row's one `Friction`
163/// (2D) / `Friction3` (3D) — see their doc comments for the field meanings.
164/// `normal_dof` must already have a `materials` entry on the same row, and the
165/// shear DOFs must not (checked by `core` via `debug_assert`).
166#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, tsify::Tsify)]
167#[serde(rename_all = "camelCase")]
168pub struct FrictionRow {
169 pub row: u32,
170 pub normal_dof: u8,
171 /// One shear DOF in 2D, two in 3D (coupled to the same normal force).
172 pub shear_dofs: Vec<u8>,
173 pub mu: f64,
174 pub k0: f64,
175 pub b: f64,
176}
177
178/// One sparse `orient` row — OpenSees' `-orient x1 x2 x3 [yp1 yp2 yp3]`: local
179/// x is `x`; in 3D local z is `x × yp` and local y completes the frame; in 2D
180/// `yp` is absent, `x[2]` must be 0 and local y is local x turned 90 degrees
181/// counter-clockwise. Every DOF of the row is evaluated along those axes.
182/// Rows without an entry use the global axes; at most one entry per row.
183#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, tsify::Tsify)]
184#[serde(rename_all = "camelCase")]
185pub struct OrientRow {
186 pub row: u32,
187 pub x: [f64; 3],
188 /// Required in 3D, must be absent in 2D.
189 #[serde(default)]
190 #[tsify(optional)]
191 pub yp: Option<[f64; 3]>,
192}
193
194#[derive(Debug, Clone, Default, Serialize, Deserialize, tsify::Tsify)]
195#[serde(rename_all = "camelCase")]
196pub struct ZeroLengthTable {
197 pub node_i: Vec<u32>,
198 pub node_j: Vec<u32>,
199 /// Sparse `(zero_length row index, dof, material arena index)` — most
200 /// DOFs on a given `ZeroLength` carry no material at all.
201 pub materials: Vec<(u32, u8, u32)>,
202 /// At most one entry per row (`core::ZeroLength` allows only one friction).
203 #[serde(default)]
204 #[tsify(optional)]
205 pub friction: Vec<FrictionRow>,
206 #[serde(default)]
207 #[tsify(optional)]
208 pub orient: Vec<OrientRow>,
209}
210
211/// A `ZeroLength` driven by a coupled `FiberSection` (axial + moment
212/// response, `[ux, rz]` in 2D; axial + biaxial moment, `[ux, ry, rz]` in 3D)
213/// instead of `ZeroLength`'s independent per-DOF materials — see
214/// `core::ZeroLengthSection`'s doc comment.
215#[derive(Debug, Clone, Default, Serialize, Deserialize, tsify::Tsify)]
216#[serde(rename_all = "camelCase")]
217pub struct ZeroLengthSectionTable {
218 pub node_i: Vec<u32>,
219 pub node_j: Vec<u32>,
220 /// Index into `FiberTable::section_offsets`.
221 pub fiber_section: Vec<u32>,
222 /// Sparse `(zero_length_section row index, dof, material arena index)` —
223 /// an independent spring for a DOF the section has no resultant for: `uy`
224 /// in 2D; `uy`/`uz` (shear) or `rx` (torsion) in 3D.
225 pub materials: Vec<(u32, u8, u32)>,
226 #[serde(default)]
227 #[tsify(optional)]
228 pub orient: Vec<OrientRow>,
229}
230
231/// Fibers for every `DispBeamColumn`/`ForceBeamColumn` section, flattened
232/// and offset-indexed: section `k` occupies
233/// `section_offsets[k]..section_offsets[k + 1]` in `y`/`z`/`area`/`material`.
234/// `section_offsets` therefore has `num_sections + 1` entries. `z` is empty in
235/// a 2D model and parallel to `y` in a 3D model (biaxial bending) — torsion is
236/// deliberately excluded from the fiber loop in `core` (`FiberSection3`'s doc
237/// comment) and supplied instead as each owning table's own `g`/`j` fields.
238#[derive(Debug, Clone, Default, Serialize, Deserialize, tsify::Tsify)]
239#[serde(rename_all = "camelCase")]
240pub struct FiberTable {
241 pub section_offsets: Vec<u32>,
242 pub y: Vec<f64>,
243 #[serde(default)]
244 #[tsify(optional)]
245 pub z: Vec<f64>,
246 pub area: Vec<f64>,
247 /// Index into the material arena, parallel to `y`/`area`.
248 pub material: Vec<u32>,
249}
250
251impl FiberTable {
252 pub fn section_count(&self) -> usize {
253 self.section_offsets.len().saturating_sub(1)
254 }
255}
256
257#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, tsify::Tsify)]
258#[serde(
259 tag = "kind",
260 rename_all = "camelCase",
261 rename_all_fields = "camelCase"
262)]
263pub enum TimeSeriesSpec {
264 Constant,
265 Linear {
266 slope: f64,
267 },
268 Path {
269 times: Vec<f64>,
270 factors: Vec<f64>,
271 },
272 /// Zero before the first sample; zero after the last unless use_last is true.
273 BoundedPath {
274 times: Vec<f64>,
275 factors: Vec<f64>,
276 use_last: bool,
277 },
278}
279
280#[derive(Debug, Clone, Default, Serialize, Deserialize, tsify::Tsify)]
281#[serde(rename_all = "camelCase")]
282pub struct LoadPatternTable {
283 pub series: Vec<TimeSeriesSpec>,
284 pub scale_factor: Vec<f64>,
285}
286
287#[derive(Debug, Clone, Default, Serialize, Deserialize, tsify::Tsify)]
288#[serde(rename_all = "camelCase")]
289pub struct NodalLoadTable {
290 /// Index into `LoadPatternTable`.
291 pub pattern: Vec<u32>,
292 /// Index into `NodeTable`.
293 pub node: Vec<u32>,
294 pub dof: Vec<u8>,
295 pub value: Vec<f64>,
296 /// Index into `SequenceSpec::stages`: this load is only registered on
297 /// the `Domain` when that stage starts, not at decode time. This
298 /// matters whenever an earlier stage's `LoadControl` shares one
299 /// pseudo-time with every unfrozen pattern (core/src/analysis/
300 /// integrator.rs) — a pattern's reference load must not exist yet if
301 /// an earlier stage isn't meant to ramp it too (the same reason
302 /// core/tests/elements/force_beam.rs's native two-phase test adds its
303 /// lateral pattern's load only between phases, not upfront).
304 pub stage: Vec<u32>,
305}
306
307/// A plane (2D continuum) material, referenced by `triangles`/`quads` rows
308/// through the `planeMaterials` arena. Strain is `[eps_x, eps_y, gamma_xy]`
309/// with engineering shear.
310#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, tsify::Tsify)]
311#[serde(
312 tag = "kind",
313 rename_all = "camelCase",
314 rename_all_fields = "camelCase"
315)]
316pub enum PlaneMaterialSpec {
317 /// Isotropic linear elastic.
318 Isotropic {
319 e: f64,
320 nu: f64,
321 state: PlaneStateSpec,
322 },
323 /// Orthotropic plane stress; `angle` is the counter-clockwise angle in
324 /// radians from global x to material axis 1.
325 Orthotropic {
326 ex: f64,
327 ey: f64,
328 nu_xy: f64,
329 g_xy: f64,
330 angle: f64,
331 },
332 /// A symmetric positive-definite `D`, packed upper triangle row by row:
333 /// `[d11, d12, d13, d22, d23, d33]`.
334 ElasticMatrix { d: [f64; 6] },
335}
336
337#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, tsify::Tsify)]
338#[serde(rename_all = "camelCase")]
339pub enum PlaneStateSpec {
340 PlaneStress,
341 PlaneStrain,
342}
343
344/// 3-node constant-strain triangles (2D models only). `nodeIds` has stride 3
345/// (counter-clockwise); `material` indexes `planeMaterials`.
346#[derive(Debug, Clone, Default, Serialize, Deserialize, tsify::Tsify)]
347#[serde(rename_all = "camelCase")]
348pub struct TriangleTable {
349 pub node_ids: Vec<u32>,
350 pub thickness: Vec<f64>,
351 pub material: Vec<u32>,
352 pub density: Vec<f64>,
353}
354
355#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, tsify::Tsify)]
356#[serde(rename_all = "camelCase")]
357pub enum Quad4FormulationSpec {
358 Full,
359 /// Wilson-Taylor incompatible modes; linear materials only.
360 Enhanced,
361}
362
363/// 4-node bilinear quadrilaterals (2D models only). `nodeIds` has stride 4
364/// (counter-clockwise); `material` indexes `planeMaterials`.
365#[derive(Debug, Clone, Default, Serialize, Deserialize, tsify::Tsify)]
366#[serde(rename_all = "camelCase")]
367pub struct QuadTable {
368 pub node_ids: Vec<u32>,
369 pub thickness: Vec<f64>,
370 pub material: Vec<u32>,
371 pub density: Vec<f64>,
372 pub formulation: Vec<Quad4FormulationSpec>,
373}
374
375/// A shell section, referenced by `shell4s` rows through the `shellSections` arena. Generalized
376/// strain and resultants follow OpenSees' `ElasticMembranePlateSection` ordering.
377#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, tsify::Tsify)]
378#[serde(
379 tag = "kind",
380 rename_all = "camelCase",
381 rename_all_fields = "camelCase"
382)]
383pub enum ShellSectionSpec {
384 /// Isotropic elastic membrane + plate bending + transverse shear: modulus, Poisson ratio,
385 /// thickness and mass density (per unit volume).
386 ElasticMembranePlate { e: f64, nu: f64, h: f64, rho: f64 },
387}
388
389/// 3-node DKT/Allman shells (3D models only). `nodeIds` has stride 3 (the node order sets the local normal by the
390/// right-hand rule); `section` indexes `shellSections`.
391#[derive(Debug, Clone, Default, Serialize, Deserialize, tsify::Tsify)]
392#[serde(rename_all = "camelCase")]
393pub struct Shell3Table {
394 pub node_ids: Vec<u32>,
395 pub section: Vec<u32>,
396}
397
398/// 4-node MITC4 shells (3D models only). `nodeIds` has stride 4 (the node order fixes the
399/// local normal by the right-hand rule); `section` indexes `shellSections`.
400#[derive(Debug, Clone, Default, Serialize, Deserialize, tsify::Tsify)]
401#[serde(rename_all = "camelCase")]
402pub struct Shell4Table {
403 pub node_ids: Vec<u32>,
404 pub section: Vec<u32>,
405}
406
407/// Every element formulation, 2D and 3D. A kind that does not belong to the
408/// model's `ndm` is `DecodeError::ElementKindNotInProfile`.
409#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, tsify::Tsify)]
410#[serde(rename_all = "camelCase")]
411pub enum ElementKind {
412 Truss,
413 ElasticBeamColumn2d,
414 ElasticBeamColumn3d,
415 DispBeamColumn2d,
416 DispBeamColumn3d,
417 ForceBeamColumn2d,
418 ForceBeamColumn3d,
419 ZeroLength,
420 ZeroLengthSection,
421 Tri3,
422 Quad4,
423 Shell3,
424 Shell4,
425}
426
427impl ElementKind {
428 pub const ALL: [ElementKind; 13] = [
429 ElementKind::Truss,
430 ElementKind::ElasticBeamColumn2d,
431 ElementKind::ElasticBeamColumn3d,
432 ElementKind::DispBeamColumn2d,
433 ElementKind::DispBeamColumn3d,
434 ElementKind::ForceBeamColumn2d,
435 ElementKind::ForceBeamColumn3d,
436 ElementKind::ZeroLength,
437 ElementKind::ZeroLengthSection,
438 ElementKind::Tri3,
439 ElementKind::Quad4,
440 ElementKind::Shell3,
441 ElementKind::Shell4,
442 ];
443
444 /// The kind's table name as `DecodeError`s report it (snake_case).
445 pub fn table(self) -> &'static str {
446 match self {
447 ElementKind::Truss => "trusses",
448 ElementKind::ElasticBeamColumn2d => "elastic_beam_columns_2d",
449 ElementKind::ElasticBeamColumn3d => "elastic_beam_columns_3d",
450 ElementKind::DispBeamColumn2d => "disp_beam_columns_2d",
451 ElementKind::DispBeamColumn3d => "disp_beam_columns_3d",
452 ElementKind::ForceBeamColumn2d => "force_beam_columns_2d",
453 ElementKind::ForceBeamColumn3d => "force_beam_columns_3d",
454 ElementKind::ZeroLength => "zero_lengths",
455 ElementKind::ZeroLengthSection => "zero_length_sections",
456 ElementKind::Tri3 => "triangles",
457 ElementKind::Quad4 => "quads",
458 ElementKind::Shell3 => "shell3s",
459 ElementKind::Shell4 => "shell4s",
460 }
461 }
462
463 /// `Some(2)`/`Some(3)` for a formulation that exists only in that profile,
464 /// `None` for one shared by both.
465 pub fn only_ndm(self) -> Option<u8> {
466 match self {
467 ElementKind::Truss | ElementKind::ZeroLength | ElementKind::ZeroLengthSection => None,
468 ElementKind::ElasticBeamColumn2d
469 | ElementKind::DispBeamColumn2d
470 | ElementKind::ForceBeamColumn2d
471 | ElementKind::Tri3
472 | ElementKind::Quad4 => Some(2),
473 ElementKind::ElasticBeamColumn3d
474 | ElementKind::DispBeamColumn3d
475 | ElementKind::ForceBeamColumn3d
476 | ElementKind::Shell3
477 | ElementKind::Shell4 => Some(3),
478 }
479 }
480
481 pub fn in_profile(self, ndm: u8) -> bool {
482 self.only_ndm().is_none_or(|only| only == ndm)
483 }
484
485 /// The 2D continuum elements (they take body and edge loads).
486 pub fn is_continuum(self) -> bool {
487 matches!(self, ElementKind::Tri3 | ElementKind::Quad4)
488 }
489
490 /// The 3D shell elements (they take self-weight and pressure loads).
491 pub fn is_shell(self) -> bool {
492 matches!(self, ElementKind::Shell3 | ElementKind::Shell4)
493 }
494
495 /// Number of edges of a continuum element (zero for every other kind).
496 pub fn edge_count(self) -> u8 {
497 match self {
498 ElementKind::Tri3 => 3,
499 ElementKind::Quad4 => 4,
500 _ => 0,
501 }
502 }
503
504 /// Whether `core` applies a beam `Uniform` load to this kind (the beam-columns).
505 pub fn accepts_uniform_load(self) -> bool {
506 matches!(
507 self,
508 ElementKind::ElasticBeamColumn2d
509 | ElementKind::ElasticBeamColumn3d
510 | ElementKind::DispBeamColumn2d
511 | ElementKind::DispBeamColumn3d
512 | ElementKind::ForceBeamColumn2d
513 | ElementKind::ForceBeamColumn3d
514 )
515 }
516}
517
518#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, tsify::Tsify)]
519#[serde(
520 tag = "kind",
521 rename_all = "camelCase",
522 rename_all_fields = "camelCase"
523)]
524pub enum ElementLoadSpec {
525 /// Uniform load (force/length) in the element's *local* axes: `wx`
526 /// along the member axis, `wy` transverse in local +y, and in 3D `wz`
527 /// transverse in local +z (`vec_xz` defines `y`/`z`). `wz` is absent in a
528 /// 2D model and defaults to 0 in a 3D one.
529 ///
530 /// Only the beam-column kinds currently honor this (`core`'s
531 /// `Element::form_load_vector` falls through to zero for every other
532 /// kind) — decode rejects any other `element_kind` explicitly rather
533 /// than silently accepting a load that will never apply. Several loads
534 /// on one element in one pattern sum.
535 Uniform {
536 wx: f64,
537 wy: f64,
538 #[serde(default)]
539 #[tsify(optional)]
540 wz: Option<f64>,
541 },
542 /// Body force per unit volume in global axes (continuum elements, 2D).
543 Body { bx: f64, by: f64 },
544 /// Traction per unit length of edge `edge` in global axes; edge `k` joins
545 /// local node `k` to `k + 1` (continuum elements, 2D).
546 EdgeTraction { edge: u8, tx: f64, ty: f64 },
547 /// Pressure on edge `edge`, positive into the element along the inward
548 /// normal (continuum elements, 2D).
549 EdgePressure { edge: u8, pressure: f64 },
550 /// Pressure per unit area on a shell, positive along its local normal `e3` (right-hand rule
551 /// from the node order). Applied as consistent nodal forces.
552 ShellPressure { pressure: f64 },
553 /// Body acceleration on a shell in global axes (a gravity vector `g` points down); the force
554 /// per unit area is `rho h b`. Note OpenSees' `ShellMITC4` `-selfWeight` has the opposite sign.
555 ShellBody { bx: f64, by: f64, bz: f64 },
556}
557
558/// Identity multi-point constraints (`core::Domain::equal_dof`'s doc
559/// comment): row `i` ties `constrained[i]`'s dofs listed in `dofs` exactly
560/// to the same dofs of `retained[i]`. `dofs` is sparse per row — `(row,
561/// dof)` pairs, mirroring `ZeroLengthTable::materials`'s own sparse
562/// convention — since most ties only ever list one or two dofs, not every
563/// dof a node has.
564#[derive(Debug, Clone, Default, Serialize, Deserialize, tsify::Tsify)]
565#[serde(rename_all = "camelCase")]
566pub struct EqualDofTable {
567 pub retained: Vec<u32>,
568 pub constrained: Vec<u32>,
569 pub dofs: Vec<(u32, u8)>,
570}
571
572/// `core::Axis3`'s wire mirror — core's own type carries no `serde` derive
573/// (`Axis3`'s doc comment: it's a plain `repr(usize)` enum for internal
574/// indexing, not wire-facing), so this is decode's own translation, the
575/// same reason `TransformSpec`/`TransformSpec3` mirror `GeomTransf`/
576/// `GeomTransf3` rather than deriving on the `core` type directly.
577#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, tsify::Tsify)]
578#[serde(rename_all = "camelCase")]
579pub enum Axis3Spec {
580 X,
581 Y,
582 Z,
583}
584
585/// Rigid diaphragm: row `i` ties every node listed against it in
586/// `constrained` to `retained[i]` — in 2D the `ux` dof (`core::Domain::
587/// rigid_diaphragm`'s doc comment); in 3D the two in-plane translational dofs
588/// (perpendicular to `normal[i]`) plus the lever-arm rotation term
589/// (`core::Domain3::rigid_diaphragm_about`). `constrained` is sparse per row —
590/// `(row, node index)` pairs — since a diaphragm's node count varies.
591/// `normal` is 3D only: omitted or empty means every row's normal is Y; a
592/// 2D model must leave it empty.
593#[derive(Debug, Clone, Default, Serialize, Deserialize, tsify::Tsify)]
594#[serde(rename_all = "camelCase")]
595pub struct RigidDiaphragmTable {
596 pub retained: Vec<u32>,
597 #[serde(default)]
598 #[tsify(optional)]
599 pub normal: Vec<Axis3Spec>,
600 pub constrained: Vec<(u32, u32)>,
601}
602
603/// Rigid links (`core::Domain::rigid_link`'s doc comment): `slave` moves with
604/// `master` as a rigid body, small-rotation lever arm taken from the node
605/// coordinates.
606#[derive(Debug, Clone, Default, Serialize, Deserialize, tsify::Tsify)]
607#[serde(rename_all = "camelCase")]
608pub struct RigidLinkTable {
609 pub master: Vec<u32>,
610 pub slave: Vec<u32>,
611}
612
613/// General linear multi-point constraints (`core::Domain::add_constraint`):
614/// row `i` defines `u[slaveNode[i], slaveDof[i]] = sum coeff * u[node, dof]`
615/// over the terms `termOffsets[i]..termOffsets[i + 1]` of the flattened sparse
616/// `termNode`/`termDof`/`termCoeff` arrays. `termOffsets` has `rows + 1`
617/// entries (or none at all for an empty table).
618#[derive(Debug, Clone, Default, Serialize, Deserialize, tsify::Tsify)]
619#[serde(rename_all = "camelCase")]
620pub struct LinearConstraintTable {
621 pub slave_node: Vec<u32>,
622 pub slave_dof: Vec<u8>,
623 pub term_offsets: Vec<u32>,
624 pub term_node: Vec<u32>,
625 pub term_dof: Vec<u8>,
626 pub term_coeff: Vec<f64>,
627}
628
629#[derive(Debug, Clone, Default, Serialize, Deserialize, tsify::Tsify)]
630#[serde(rename_all = "camelCase")]
631pub struct ElementLoadTable {
632 pub pattern: Vec<u32>,
633 pub element_kind: Vec<ElementKind>,
634 /// Row index into the table named by the parallel `element_kind` entry.
635 pub element_index: Vec<u32>,
636 pub load: Vec<ElementLoadSpec>,
637 /// See [`NodalLoadTable::stage`] — same per-stage registration timing.
638 pub stage: Vec<u32>,
639}