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
// JEOD_INV: TS.01 — `<SelfRef>` / `<SelfPlanet>` are runtime-resolved storage-boundary wildcards; see `docs/JEOD_invariants.md` row TS.01 and the lint at `tests/self_ref_self_planet_discipline.rs`.
//! Bevy `Component` and `Message` newtypes for the mass tree —
//! per-body mass identifier, the parent ↔ child mass-tree edge, the
//! mass-point back-pointer, the kinematic-child gating marker, and the
//! attach / detach messages consumed by the staging system.
use ;
use *;
// intra-doc-link resolution
use MassPropertiesC;
// ── Mass Tree (Staging) ──
/// Maps this entity to a node in the shared [`MassTreeR`](crate::MassTreeR) resource.
///
/// Entities with this component participate in the mass tree. After
/// attach/detach events are processed, the entity's [`MassPropertiesC`]
/// is synced from the tree's composite properties.
;
/// ECS-native mass-tree relation: marks `Entity` carrying this component
/// as a sub-mass attached to the referenced parent entity in the **mass
/// tree** (deliberately distinct from Bevy's frame-tree `ChildOf`).
///
/// Mirrors JEOD's separation of `RefFrame` and `MassBody` trees (see
/// [Frame-Tree-ECS-Native § 15.2](https://github.com/simnaut/astrodyn/wiki/Frame-Tree-ECS-Native#152-mass--inertia-composition)
/// and Appendix A.3): the kinematic frame tree and the inertial mass
/// tree evolve under independent attach/detach paths, coupled only by
/// the explicit [`MassPointRef`] back-pointer. Keeping the two
/// relations as separate `Component`s makes Bevy's hierarchy +
/// observer plumbing one-to-one with JEOD's "two trees + named
/// coupling" architecture.
///
/// The component carries the **parent reference** plus the
/// attach-edge geometry (`offset` + `t_parent_child`), matching the
/// arena's per-body `MassBody::structure_point` (`offset` is the
/// child's structural origin in the *parent's* structural frame;
/// `t_parent_child` is the rotation from the parent's structural
/// frame into this body's structural frame). Edge geometry lives on
/// the child because every child has exactly one parent — which
/// matches both JEOD's `MassBody` layout and the natural ECS
/// component-per-entity grain.
///
/// The carrier entity must also have [`MassPropertiesC`]; the
/// `composite_mass_system` walks `MassChildOf` edges bottom-up via
/// the [`astrodyn::MassStorage`] trait and writes the recomputed
/// composite properties back into [`MassPropertiesC`] on every node
/// in the affected subtree.
///
/// # JEOD precedent
///
/// `MassBody` nodes form a tree via `MassBodyLinks` (see
/// `models/dynamics/mass/include/mass.hh`); `MassBody::structure_point`
/// (`MassPointState`) carries the per-attach offset + rotation that
/// `MassChildOf` mirrors here. `BodyRefFrame::mass_point`
/// (`models/dynamics/dyn_body/include/body_ref_frame.hh`) is the
/// frame-side back-pointer connecting a kinematic frame to its
/// mass-tree origin — see [`MassPointRef`] for the Bevy port.
// JEOD_INV: MA.08 — no cycle in mass tree (composite_mass_system asserts via post-order walk)
// JEOD_INV: MA.19 — no same-tree attachment (cycle prevention)
/// Frame-side back-pointer linking a body's frame entity to the
/// mass-tree node that supplies the body's **mass-point origin**
/// (CoM offset + struct→body rotation).
///
/// Mirrors JEOD's `BodyRefFrame::mass_point` (a `MassPoint *`) defined
/// in `models/dynamics/dyn_body/include/body_ref_frame.hh`. JEOD uses
/// this back-pointer to route kinematic state queries on a body frame
/// (which knows the mass-side point) without forcing the frame and
/// mass trees to share their hierarchy, which is the same separation
/// the Bevy adapter mirrors via [`MassChildOf`] vs Bevy's `ChildOf`.
///
/// **Optional by design.** Per [Frame-Tree-ECS-Native § 15.2](https://github.com/simnaut/astrodyn/wiki/Frame-Tree-ECS-Native#152-mass--inertia-composition)
/// the back-pointer is *absent for kinematic-only attaches* — i.e.
/// frame entities whose kinematics ride a parent without contributing
/// to that parent's mass (sensor mounts, station-keeping vehicles
/// attached only via `attach_to_frame`). Mission code attaches it
/// only when the frame entity also participates in the mass tree.
;
/// Marker: this entity is a kinematic non-root node in a
/// [`MassChildOf`] chain and must NOT be advanced by
/// [`integration_system`](crate::systems::integration_system).
///
/// JEOD's composite-rigid-body model integrates only the root of every
/// mass tree (`dyn_body_collect.cc:138` — every `dyn_parent != nullptr`
/// branch transmits forces upstream and computes no per-child
/// accelerations). The Bevy port mirrors this by:
///
/// 1. [`wrench_aggregation_system`](crate::wrench::wrench_aggregation_system)
/// walks every `MassChildOf` chain and folds each non-root child's
/// `(force, torque)` (with parallel-axis arm) into the root's
/// `TotalForceC`, then zeroes the children's
/// `TotalForceC` / `FrameDerivativesC`.
/// 2. The same system inserts `KinematicChildC` on every non-root
/// node and removes it from any node that becomes a root (mass tree
/// rewired or torn down).
/// 3. [`integration_system`](crate::systems::integration_system) filters
/// its body query with `Without<KinematicChildC>` so the kinematic
/// children's translational / rotational state never advances under
/// gravity (or any other contributor `integration_system` reads
/// directly). Without the marker, zeroing `TotalForceC` is not
/// enough — `integration_system` recomputes gravity at every RK
/// sub-stage from `GravityControlsC` and would still drift the
/// child's state.
///
/// This marker is purely a **gating hint** for the integrator. The
/// kinematic propagation that derives child poses *from* the root
/// each step lives at
/// [`crate::kinematic_propagation::propagate_state_from_root_system`]
/// (design-doc Section 15.3) and runs earlier in
/// `AstrodynSet::ForceCollection` so the wrench walk reads live
/// attitudes; non-root children's `TranslationalStateC` /
/// `RotationalStateC` are overwritten each step with the derived
/// value.
///
/// Mission code MUST NOT manage this marker manually — the
/// wrench-aggregation system owns its lifecycle. Inserting it on a
/// root-level body would freeze that body's state; removing it from a
/// non-root body would let the integrator double-count the wrench
/// (once via the aggregated root total, once via per-stage gravity on
/// the now-self-integrated child).
// JEOD_INV: DB.17 — only the root's TotalForce/FrameDerivatives drive the
// integrator (children are kinematic, gated by this marker)
;
/// Message: attach a child body to a parent in the mass tree.
///
/// Both entities must have [`MassBodyIdC`]. Processed by `staging_system`
/// before integration each step.
///
/// # Vehicle phantoms
///
/// `AttachEvent` is parameterized by **two** vehicle phantoms:
/// `VParent` names the parent body's vehicle identity and `VChild`
/// names the child body's. The split lets the type system distinguish
/// the parent's structural frame from the child's, which is necessary
/// to type the rotation slot as
/// `FrameTransform<StructuralFrame<VParent>, StructuralFrame<VChild>>`
/// — a single-phantom shape would collapse `From == To` and lose the
/// directional guarantee at the type level.
///
/// Mission code that pins both vehicles (via
/// [`define_vehicle!`](astrodyn::define_vehicle)) gets a compile-time
/// guard against confusing one attach pair with another — e.g.
/// `AttachEvent<Iss, Soyuz>` cannot be confused with
/// `AttachEvent<Iss, Cygnus>`, and a `t_parent_child` constructed for
/// the wrong pair fails to typecheck. The compile-time guard layered
/// on top of the existing frame-kind check (structural-vs-inertial)
/// is the parent-and-child vehicle identity.
///
/// # Runtime-resolved boundary
///
/// The canonical Bevy adapter registers and consumes
/// `AttachEvent<SelfRef, SelfRef>` because per-entity storage decides
/// both parent and child vehicle identity at runtime via the entity
/// hierarchy — the message bus does not statically know which vehicle
/// pair is involved. `<SelfRef, SelfRef>` is the documented
/// runtime-resolved instantiation; mission code that mints concrete
/// pairs may register the matching `add_message::<AttachEvent<P, C>>()`
/// itself.
///
/// # Direction convention
///
/// `t_parent_child` rotates vectors expressed in the **parent's**
/// structural frame into the **child's** structural frame, matching
/// JEOD's `T_pstr_cstr` (see
/// `models/dynamics/mass/src/mass_attach.cc:151` —
/// "Transformation matrix from the new parent body's structural
/// frame to this body's structural frame"). The offset is the child's
/// structural origin expressed in the parent's structural frame
/// coordinates (JEOD `offset_pstr_cstr_pstr`).
///
/// # Cross-pair compile-time guard
///
/// Constructing an `AttachEvent<Iss, Soyuz>` whose `t_parent_child`
/// was built for a different pair (e.g. `<Iss, Iss>` — a same-vehicle
/// "self attach" rotation that happens to typecheck without the
/// split phantom) is rejected at compile time:
///
/// ```compile_fail
/// use astrodyn_bevy::AttachEvent;
/// use bevy::prelude::Entity;
/// use astrodyn::{define_vehicle, FrameTransform, StructuralFrame, Vec3Ext};
/// use glam::DVec3;
///
/// define_vehicle!(Iss);
/// define_vehicle!(Soyuz);
///
/// let _ = AttachEvent::<Iss, Soyuz> {
/// child: Entity::PLACEHOLDER,
/// parent: Entity::PLACEHOLDER,
/// offset: Vec3Ext::m_at::<StructuralFrame<Iss>>(DVec3::ZERO),
/// // Wrong pair: `<Iss, Iss>` does not match the slot's expected
/// // `<Iss, Soyuz>` — typecheck failure.
/// t_parent_child: FrameTransform::<StructuralFrame<Iss>, StructuralFrame<Iss>>::identity(),
/// };
/// ```
/// Message: detach a child body from its parent in the mass tree.
///
/// The entity must have [`MassBodyIdC`] and be attached to a parent.
/// Processed by `staging_system` before integration each step.