avian3d/dynamics/joints/joint_graph/
plugin.rs1use core::marker::PhantomData;
2
3use super::{JointGraph, JointGraphEdge, JointId};
4#[cfg(feature = "xpbd_joints")]
5use crate::dynamics::solver::xpbd::XpbdVelocityProjection;
6use crate::{
7 collision::contact_types::ContactId,
8 data_structures::pair_key::PairKey,
9 dynamics::joints::EntityConstraint,
10 prelude::{
11 ContactGraph, ContactStatusChange, ContactStatusChangeQueue, JointCollisionDisabled,
12 JointDisabled, PhysicsSchedule, PhysicsStepSystems, RigidBodyColliders,
13 },
14};
15use bevy::{
16 ecs::{
17 component::ComponentId, entity_disabling::Disabled, lifecycle::HookContext,
18 query::QueryFilter, world::DeferredWorld,
19 },
20 prelude::*,
21};
22
23pub struct JointGraphPlugin<T: Component + EntityConstraint<2>>(PhantomData<T>);
27
28impl<T: Component + EntityConstraint<2>> Default for JointGraphPlugin<T> {
29 fn default() -> Self {
30 Self(PhantomData)
31 }
32}
33
34#[derive(Resource, Default)]
35struct JointGraphPluginInitialized;
36
37impl<T: Component + EntityConstraint<2>> Plugin for JointGraphPlugin<T> {
38 fn build(&self, app: &mut App) {
39 let already_initialized = app
40 .world()
41 .is_resource_added::<JointGraphPluginInitialized>();
42
43 app.init_resource::<JointGraph>();
44 app.add_message::<JointGraphChange>();
45 app.init_resource::<JointGraphPluginInitialized>();
46
47 app.register_required_components::<T, JointComponentId>();
49
50 app.world_mut()
52 .register_component_hooks::<T>()
53 .on_add(on_add_joint)
54 .on_remove(on_remove_joint);
55
56 app.add_observer(
58 add_joint_to_graph::<T, Add<T>, (With<JointComponentId>, Without<JointDisabled>)>,
59 );
60
61 app.add_observer(remove_joint_from_graph::<Remove<T>>);
63
64 if !already_initialized {
65 app.add_observer(remove_joint_from_graph::<Add<(Disabled, JointDisabled)>>);
67
68 app.add_observer(on_disable_joint_collision);
70 }
71
72 app.add_observer(
74 add_joint_to_graph::<
75 T,
76 Remove<Disabled>,
77 (
78 With<JointComponentId>,
79 Or<(With<Disabled>, Without<Disabled>)>,
80 Without<JointDisabled>,
81 ),
82 >,
83 );
84
85 app.add_observer(add_joint_to_graph::<T, Remove<JointDisabled>, With<JointComponentId>>);
87
88 app.add_systems(
89 PhysicsSchedule,
90 on_change_joint_entities::<T>
91 .in_set(PhysicsStepSystems::First)
92 .ambiguous_with(PhysicsStepSystems::First),
93 );
94 }
95}
96
97#[derive(Message, Clone, Copy, Debug, PartialEq, Eq)]
102pub enum JointGraphChange {
103 Added(JointId),
105 Removed(JointId),
107}
108
109#[derive(Component, Clone, Debug, Default, PartialEq, Reflect)]
113pub struct JointComponentId(Option<ComponentId>);
114
115impl JointComponentId {
116 pub fn new() -> Self {
118 Self(None)
119 }
120
121 pub fn id(&self) -> Option<ComponentId> {
123 self.0
124 }
125}
126
127fn add_joint_to_graph<
128 T: Component + EntityConstraint<2>,
129 E: EventPattern<Event: EntityEvent>,
130 F: QueryFilter,
131>(
132 trigger: On<E>,
133 query: Query<(&T, Has<JointCollisionDisabled>), F>,
134 mut joint_graph: ResMut<JointGraph>,
135 mut joint_graph_changes: MessageWriter<JointGraphChange>,
136 #[cfg(feature = "xpbd_joints")] mut commands: Commands,
137) {
138 let entity = trigger.event_target();
139
140 let Ok((joint, collision_disabled)) = query.get(entity) else {
141 return;
142 };
143
144 let [body1, body2] = joint.entities();
145
146 let joint_edge = JointGraphEdge::new(entity, body1, body2, collision_disabled);
148 let joint_id = joint_graph.add_joint(joint_edge);
149
150 #[cfg(feature = "xpbd_joints")]
152 for body in [body1, body2] {
153 commands.entity(body).try_insert(XpbdVelocityProjection);
154 }
155
156 joint_graph_changes.write(JointGraphChange::Added(joint_id));
158}
159
160fn remove_joint_from_graph<E: EventPattern<Event: EntityEvent>>(
161 trigger: On<E>,
162 mut joint_graph: ResMut<JointGraph>,
163 mut joint_graph_changes: MessageWriter<JointGraphChange>,
164 #[cfg(feature = "xpbd_joints")] mut commands: Commands,
165) {
166 let entity = trigger.event_target();
167
168 let Some(joint) = joint_graph.get(entity) else {
169 return;
170 };
171
172 joint_graph_changes.write(JointGraphChange::Removed(joint.id));
174 #[cfg(feature = "xpbd_joints")]
175 let bodies = [joint.body1, joint.body2];
176
177 joint_graph.remove_joint(entity);
179
180 #[cfg(feature = "xpbd_joints")]
182 for body in bodies {
183 if joint_graph.joints_of(body).next().is_none() {
184 commands.entity(body).try_remove::<XpbdVelocityProjection>();
185 }
186 }
187}
188
189fn on_add_joint(mut world: DeferredWorld, ctx: HookContext) {
190 let entity = ctx.entity;
191 let component_id = ctx.component_id;
192
193 let mut joint = world.get_mut::<JointComponentId>(entity).unwrap();
194 let old_joint = joint.0;
195
196 joint.0 = Some(component_id);
198
199 if let Some(old_joint) = old_joint {
200 world.commands().entity(entity).remove_by_id(old_joint);
202
203 #[cfg(feature = "validate")]
204 {
205 use disqualified::ShortName;
206
207 let components = world.components();
209 let old_joint_shortname = components.get_info(old_joint).unwrap().name();
210 let old_joint_name = ShortName(&old_joint_shortname);
211 let new_joint_shortname = components.get_info(component_id).unwrap().name();
212 let new_joint_name = ShortName(&new_joint_shortname);
213
214 warn!(
215 "{old_joint_name} was replaced with {new_joint_name} on entity {entity}. An entity can only hold one joint type at a time."
216 );
217 }
218 }
219}
220
221fn on_remove_joint(mut world: DeferredWorld, ctx: HookContext) {
222 let entity = ctx.entity;
223 let component_id = ctx.component_id;
224
225 if let Some(mut joint) = world.get_mut::<JointComponentId>(entity)
228 && joint.0 == Some(component_id)
229 {
230 joint.0 = None;
231
232 world
234 .commands()
235 .entity(entity)
236 .try_remove::<JointComponentId>();
237 }
238}
239
240fn on_disable_joint_collision(
241 trigger: On<Add<JointCollisionDisabled>>,
242 query: Query<&RigidBodyColliders>,
243 joint_graph: Res<JointGraph>,
244 mut contact_graph: ResMut<ContactGraph>,
245 mut contact_status_changes: ResMut<ContactStatusChangeQueue>,
246) {
247 let entity = trigger.entity;
248
249 let Some([body1, body2]) = joint_graph.bodies_of(entity) else {
252 return;
253 };
254 let Ok([colliders1, colliders2]) = query.get_many([body1, body2]) else {
255 return;
256 };
257
258 let (colliders, other_body) = if colliders1.len() < colliders2.len() {
259 (colliders1, body2)
260 } else {
261 (colliders2, body1)
262 };
263
264 let contacts_to_remove: Vec<ContactId> = colliders
265 .iter()
266 .flat_map(|collider| {
267 contact_graph
268 .contact_edges_with(collider)
269 .filter_map(|edge| {
270 if edge.body1 == Some(other_body) || edge.body2 == Some(other_body) {
271 Some(edge.id)
272 } else {
273 None
274 }
275 })
276 })
277 .collect();
278
279 for contact_id in contacts_to_remove {
280 let pair_key = PairKey::new(body1.index_u32(), body2.index_u32());
282 contact_graph.remove_edge_by_id(&pair_key, contact_id);
283
284 contact_status_changes.push(ContactStatusChange::StoppedGeneratingConstraints {
286 contact_id,
287 body1,
288 body2,
289 });
290 }
291}
292
293fn on_change_joint_entities<T: Component + EntityConstraint<2>>(
295 query: Query<(Entity, &T), Changed<T>>,
296 mut joint_graph: ResMut<JointGraph>,
297 mut joint_graph_changes: MessageWriter<JointGraphChange>,
298 #[cfg(feature = "xpbd_joints")] mut commands: Commands,
299) {
300 for (entity, joint) in &query {
301 let [body1, body2] = joint.entities();
302 let Some(old_edge) = joint_graph.get(entity) else {
303 continue;
304 };
305
306 if body1 != old_edge.body1 || body2 != old_edge.body2 {
307 let old_id = old_edge.id;
308 #[cfg(feature = "xpbd_joints")]
309 let old_bodies = [old_edge.body1, old_edge.body2];
310
311 if let Some(mut edge) = joint_graph.remove_joint(entity) {
313 joint_graph_changes.write(JointGraphChange::Removed(old_id));
315
316 edge.body1 = body1;
318 edge.body2 = body2;
319
320 let joint_id = joint_graph.add_joint(edge);
322
323 joint_graph_changes.write(JointGraphChange::Added(joint_id));
325
326 #[cfg(feature = "xpbd_joints")]
328 {
329 for body in [body1, body2] {
330 commands.entity(body).try_insert(XpbdVelocityProjection);
331 }
332 for body in old_bodies {
333 if joint_graph.joints_of(body).next().is_none() {
334 commands.entity(body).try_remove::<XpbdVelocityProjection>();
335 }
336 }
337 }
338 }
339 }
340 }
341}
342
343