Skip to main content

godot_bevy/plugins/transforms/
custom_sync.rs

1/// Macro for generating transform synchronization systems with compile-time queries.
2///
3/// This macro generates systems that sync transforms between Bevy and Godot for entities
4/// matching specific component queries. It automatically handles both 2D and 3D nodes
5/// using runtime type detection, similar to the default sync systems.
6///
7/// # Usage
8///
9/// ```ignore
10/// use godot_bevy::add_transform_sync_systems;
11/// use bevy_ecs::query::With;
12/// use bevy_ecs::component::Component;
13/// use bevy::prelude::*;
14///
15/// #[derive(Component)]
16/// struct Player;
17/// #[derive(Component)]
18/// struct UIElement;
19/// #[derive(Component)]
20/// struct PhysicsActor;
21///
22/// let mut app = App::new();
23/// // Mixed directional sync in a single call
24/// add_transform_sync_systems! {
25///     app,
26///     UIElements = bevy_to_godot: With<UIElement>,    // ECS → Godot only
27///     PhysicsResults = godot_to_bevy: With<PhysicsActor>, // Godot → ECS only
28///     Player = With<Player>,                          // Bidirectional
29/// }
30/// ```
31#[macro_export]
32macro_rules! add_transform_sync_systems {
33    // Main entry point - handles mixed directional sync
34    ($app:expr, $($tokens:tt)*) => {
35        $crate::add_transform_sync_systems!(@parse_all $app, $($tokens)*);
36    };
37
38    // Parse all items recursively
39    (@parse_all $app:expr, $name:ident = bevy_to_godot: $query:ty, $($rest:tt)*) => {
40        $crate::add_transform_sync_systems!(@generate_post_system $app, $name, $query);
41        $crate::add_transform_sync_systems!(@parse_all $app, $($rest)*);
42    };
43
44    (@parse_all $app:expr, $name:ident = godot_to_bevy: $query:ty, $($rest:tt)*) => {
45        $crate::add_transform_sync_systems!(@generate_pre_system $app, $name, $query);
46        $crate::add_transform_sync_systems!(@parse_all $app, $($rest)*);
47    };
48
49    (@parse_all $app:expr, $name:ident = $query:ty, $($rest:tt)*) => {
50        $crate::add_transform_sync_systems!(@generate_systems $app, $name, $query, $query);
51        $crate::add_transform_sync_systems!(@parse_all $app, $($rest)*);
52    };
53
54    // Handle last item (without trailing comma)
55    (@parse_all $app:expr, $name:ident = bevy_to_godot: $query:ty) => {
56        $crate::add_transform_sync_systems!(@generate_post_system $app, $name, $query);
57    };
58
59    (@parse_all $app:expr, $name:ident = godot_to_bevy: $query:ty) => {
60        $crate::add_transform_sync_systems!(@generate_pre_system $app, $name, $query);
61    };
62
63    (@parse_all $app:expr, $name:ident = $query:ty) => {
64        $crate::add_transform_sync_systems!(@generate_systems $app, $name, $query, $query);
65    };
66
67    // Handle empty case
68    (@parse_all $app:expr,) => {};
69    (@parse_all $app:expr) => {};
70
71    (@generate_systems $app:expr, $name:ident, $bevy_to_godot_query:ty, $godot_to_bevy_query:ty) => {
72        $crate::add_transform_sync_systems!(@generate_post_system $app, $name, $bevy_to_godot_query);
73        $crate::add_transform_sync_systems!(@generate_pre_system $app, $name, $godot_to_bevy_query);
74    };
75
76    (@generate_post_system $app:expr, $name:ident, $bevy_to_godot_query:ty) => {
77        $crate::paste::paste! {
78            #[cfg(debug_assertions)]
79            #[tracing::instrument]
80            pub fn [<post_update_godot_transforms_ $name:lower>](
81                change_tick: $crate::bevy_ecs::system::SystemChangeTick,
82                entities: $crate::bevy_ecs::system::Query<
83                    (
84                        $crate::bevy_ecs::change_detection::Ref<$crate::bevy_transform::components::Transform>,
85                        &$crate::interop::GodotNodeHandle,
86                        &$crate::plugins::transforms::TransformSyncMetadata,
87                        $crate::bevy_ecs::query::AnyOf<(&$crate::interop::node_markers::Node2DMarker, &$crate::interop::node_markers::Node3DMarker)>,
88                    ),
89                    (
90                        $crate::bevy_ecs::query::Changed<$crate::bevy_transform::components::Transform>,
91                        $bevy_to_godot_query,
92                    ),
93                >,
94                mut godot: $crate::interop::GodotAccess,
95                bulk_ops_cache: $crate::bevy_ecs::system::NonSendMut<$crate::interop::BulkOperationsCache>,
96            ) {
97                if let Some(bulk_ops_node) = bulk_ops_cache.get() {
98                    [<post_update_godot_transforms_ $name:lower _bulk>](
99                        change_tick,
100                        entities,
101                        bulk_ops_node,
102                    );
103                    return;
104                }
105                [<post_update_godot_transforms_ $name:lower _individual>](change_tick, entities, &mut godot);
106            }
107
108            #[cfg(not(debug_assertions))]
109            #[tracing::instrument]
110            pub fn [<post_update_godot_transforms_ $name:lower>](
111                change_tick: $crate::bevy_ecs::system::SystemChangeTick,
112                entities: $crate::bevy_ecs::system::Query<
113                    (
114                        $crate::bevy_ecs::change_detection::Ref<$crate::bevy_transform::components::Transform>,
115                        &$crate::interop::GodotNodeHandle,
116                        &$crate::plugins::transforms::TransformSyncMetadata,
117                        $crate::bevy_ecs::query::AnyOf<(&$crate::interop::node_markers::Node2DMarker, &$crate::interop::node_markers::Node3DMarker)>,
118                    ),
119                    (
120                        $crate::bevy_ecs::query::Changed<$crate::bevy_transform::components::Transform>,
121                        $bevy_to_godot_query,
122                    ),
123                >,
124                mut godot: $crate::interop::GodotAccess,
125            ) {
126                [<post_update_godot_transforms_ $name:lower _individual>](change_tick, entities, &mut godot);
127            }
128
129            #[cfg(debug_assertions)]
130            fn [<post_update_godot_transforms_ $name:lower _bulk>](
131                change_tick: $crate::bevy_ecs::system::SystemChangeTick,
132                mut entities: $crate::bevy_ecs::system::Query<
133                    (
134                        $crate::bevy_ecs::change_detection::Ref<$crate::bevy_transform::components::Transform>,
135                        &$crate::interop::GodotNodeHandle,
136                        &$crate::plugins::transforms::TransformSyncMetadata,
137                        $crate::bevy_ecs::query::AnyOf<(&$crate::interop::node_markers::Node2DMarker, &$crate::interop::node_markers::Node3DMarker)>,
138                    ),
139                    (
140                        $crate::bevy_ecs::query::Changed<$crate::bevy_transform::components::Transform>,
141                        $bevy_to_godot_query,
142                    ),
143                >,
144                mut batch_singleton: godot::prelude::Gd<godot::classes::Object>,
145            ) {
146                use $crate::plugins::transforms::{IntoGodotTransform, IntoGodotTransform2D};
147                use $crate::bevy_ecs::change_detection::DetectChanges;
148                use godot::global::godot_print;
149                use godot::prelude::ToGodot;
150
151                let _span = tracing::info_span!("bulk_data_preparation_optimized", system = stringify!($name)).entered();
152
153                // Pre-allocate vectors with estimated capacity to avoid reallocations
154                let entity_count = entities.iter().count();
155                let mut instance_ids_3d = Vec::with_capacity(entity_count);
156                let mut positions_3d = Vec::with_capacity(entity_count);
157                let mut rotations_3d = Vec::with_capacity(entity_count);
158                let mut scales_3d = Vec::with_capacity(entity_count);
159
160                let mut instance_ids_2d = Vec::with_capacity(entity_count);
161                let mut positions_2d = Vec::with_capacity(entity_count);
162                let mut rotations_2d = Vec::with_capacity(entity_count);
163                let mut scales_2d = Vec::with_capacity(entity_count);
164
165                // Collect raw transform data (no FFI allocations)
166                let _collect_span = tracing::info_span!("collect_raw_arrays", system = stringify!($name)).entered();
167                for (transform_ref, reference, metadata, (node2d, node3d)) in entities.iter_mut() {
168                    // Check if we have sync information for this entity
169                    if let Some(sync_tick) = metadata.last_sync_tick {
170                        if !transform_ref
171                            .last_changed()
172                            .is_newer_than(sync_tick, change_tick.this_run())
173                        {
174                            // This change was from our Godot sync, skip it
175                            continue;
176                        }
177                    }
178
179                    let instance_id = reference.instance_id();
180
181                    if node2d.is_some() {
182                        let transform_2d = transform_ref.to_godot_transform_2d();
183                        instance_ids_2d.push(instance_id.to_i64());
184                        positions_2d.push(godot::prelude::Vector2::new(transform_2d.origin.x, transform_2d.origin.y));
185                        rotations_2d.push(transform_2d.rotation());
186                        scales_2d.push(godot::prelude::Vector2::new(transform_2d.scale().x, transform_2d.scale().y));
187                    } else if node3d.is_some() {
188                        // Use Bevy transform components directly (avoid complex conversions)
189                        instance_ids_3d.push(instance_id.to_i64());
190                        positions_3d.push(godot::prelude::Vector3::new(
191                            transform_ref.translation.x,
192                            transform_ref.translation.y,
193                            transform_ref.translation.z
194                        ));
195
196                        rotations_3d.push(godot::prelude::Vector4 {
197                            x: transform_ref.rotation.x,
198                            y: transform_ref.rotation.y,
199                            z: transform_ref.rotation.z,
200                            w: transform_ref.rotation.w,
201                        });
202
203                        scales_3d.push(godot::prelude::Vector3::new(
204                            transform_ref.scale.x,
205                            transform_ref.scale.y,
206                            transform_ref.scale.z
207                        ));
208                    }
209                }
210                drop(_collect_span);
211
212                // Convert to Godot packed arrays (much more efficient than VarDictionary arrays)
213                let _convert_span = tracing::info_span!("convert_to_packed_arrays", system = stringify!($name)).entered();
214                let has_3d_updates = !instance_ids_3d.is_empty();
215                let has_2d_updates = !instance_ids_2d.is_empty();
216                drop(_convert_span);
217
218                // End data preparation phase
219                drop(_span);
220
221                // Make raw array FFI calls if we have updates
222                let total_updates = instance_ids_3d.len() + instance_ids_2d.len();
223                if total_updates > 0 {
224                    static mut BATCH_LOG_COUNTER: u32 = 0;
225                    unsafe {
226                        BATCH_LOG_COUNTER += 1;
227                    }
228
229                    let _ffi_calls_span = tracing::info_span!("raw_array_ffi_calls", total_entities = total_updates, system = stringify!($name)).entered();
230
231                    if has_3d_updates {
232                        let _span = tracing::info_span!("raw_ffi_call_3d", entities = instance_ids_3d.len(), system = stringify!($name)).entered();
233
234                        // Convert to packed arrays
235                        let instance_ids_packed = godot::prelude::PackedInt64Array::from(instance_ids_3d.as_slice());
236                        let positions_packed = godot::prelude::PackedVector3Array::from(positions_3d.as_slice());
237                        let rotations_packed = godot::prelude::PackedVector4Array::from(rotations_3d.as_slice());
238                        let scales_packed = godot::prelude::PackedVector3Array::from(scales_3d.as_slice());
239
240                        batch_singleton.call("bulk_update_transforms_3d", &[
241                            instance_ids_packed.to_variant(),
242                            positions_packed.to_variant(),
243                            rotations_packed.to_variant(),
244                            scales_packed.to_variant()
245                        ]);
246                    }
247                    if has_2d_updates {
248                        let _span = tracing::info_span!("raw_ffi_call_2d", entities = instance_ids_2d.len(), system = stringify!($name)).entered();
249
250                        // Convert to packed arrays
251                        let instance_ids_packed = godot::prelude::PackedInt64Array::from(instance_ids_2d.as_slice());
252                        let positions_packed = godot::prelude::PackedVector2Array::from(positions_2d.as_slice());
253                        let rotations_packed = godot::prelude::PackedFloat32Array::from(rotations_2d.as_slice());
254                        let scales_packed = godot::prelude::PackedVector2Array::from(scales_2d.as_slice());
255
256                        batch_singleton.call("bulk_update_transforms_2d", &[
257                            instance_ids_packed.to_variant(),
258                            positions_packed.to_variant(),
259                            rotations_packed.to_variant(),
260                            scales_packed.to_variant()
261                        ]);
262                    }
263                }
264            }
265
266            fn [<post_update_godot_transforms_ $name:lower _individual>](
267                change_tick: $crate::bevy_ecs::system::SystemChangeTick,
268                mut entities: $crate::bevy_ecs::system::Query<
269                    (
270                        $crate::bevy_ecs::change_detection::Ref<$crate::bevy_transform::components::Transform>,
271                        &$crate::interop::GodotNodeHandle,
272                        &$crate::plugins::transforms::TransformSyncMetadata,
273                        $crate::bevy_ecs::query::AnyOf<(&$crate::interop::node_markers::Node2DMarker, &$crate::interop::node_markers::Node3DMarker)>,
274                    ),
275                    (
276                        $crate::bevy_ecs::query::Changed<$crate::bevy_transform::components::Transform>,
277                        $bevy_to_godot_query,
278                    ),
279                >,
280                godot: &mut $crate::interop::GodotAccess,
281            ) {
282                use $crate::plugins::transforms::{IntoGodotTransform, IntoGodotTransform2D};
283                use $crate::bevy_ecs::change_detection::DetectChanges;
284                use godot::classes::{Node2D, Node3D};
285
286                // Original individual FFI approach
287                for (transform_ref, reference, metadata, (node2d, node3d)) in entities.iter_mut() {
288                    // Check if we have sync information for this entity
289                    if let Some(sync_tick) = metadata.last_sync_tick {
290                        if !transform_ref
291                            .last_changed()
292                            .is_newer_than(sync_tick, change_tick.this_run())
293                        {
294                            // This change was from our Godot sync, skip it
295                            continue;
296                        }
297                    }
298
299                    // Handle both 2D and 3D nodes in a single system
300                    if node2d.is_some() {
301                        let _span = tracing::info_span!("individual_ffi_call_2d", system = stringify!($name)).entered();
302                        let mut obj = godot.get::<Node2D>(*reference);
303                        obj.set_transform(transform_ref.to_godot_transform_2d());
304                    } else if node3d.is_some() {
305                        let _span = tracing::info_span!("individual_ffi_call_3d", system = stringify!($name)).entered();
306                        let mut obj = godot.get::<Node3D>(*reference);
307                        obj.set_transform(transform_ref.to_godot_transform());
308                    }
309                }
310            }
311
312            $app.add_systems($crate::bevy_app::Last, [<post_update_godot_transforms_ $name:lower>]);
313        }
314    };
315
316    (@generate_pre_system $app:expr, $name:ident, $godot_to_bevy_query:ty) => {
317        $crate::paste::paste! {
318            #[cfg(debug_assertions)]
319            #[tracing::instrument]
320            pub fn [<pre_update_godot_transforms_ $name:lower>](
321                entities: $crate::bevy_ecs::system::Query<
322                    (
323                        $crate::bevy_ecs::entity::Entity,
324                        &mut $crate::bevy_transform::components::Transform,
325                        &$crate::interop::GodotNodeHandle,
326                        &mut $crate::plugins::transforms::TransformSyncMetadata,
327                        $crate::bevy_ecs::query::AnyOf<(&$crate::interop::node_markers::Node2DMarker, &$crate::interop::node_markers::Node3DMarker)>,
328                    ),
329                    $godot_to_bevy_query
330                >,
331                mut godot: $crate::interop::GodotAccess,
332                bulk_ops_cache: $crate::bevy_ecs::system::NonSendMut<$crate::interop::BulkOperationsCache>,
333            ) {
334                if let Some(bulk_ops_node) = bulk_ops_cache.get() {
335                    [<pre_update_godot_transforms_ $name:lower _bulk>](
336                        entities,
337                        bulk_ops_node,
338                    );
339                    return;
340                }
341                [<pre_update_godot_transforms_ $name:lower _individual>](entities, &mut godot);
342            }
343
344            #[cfg(not(debug_assertions))]
345            #[tracing::instrument]
346            pub fn [<pre_update_godot_transforms_ $name:lower>](
347                entities: $crate::bevy_ecs::system::Query<
348                    (
349                        $crate::bevy_ecs::entity::Entity,
350                        &mut $crate::bevy_transform::components::Transform,
351                        &$crate::interop::GodotNodeHandle,
352                        &mut $crate::plugins::transforms::TransformSyncMetadata,
353                        $crate::bevy_ecs::query::AnyOf<(&$crate::interop::node_markers::Node2DMarker, &$crate::interop::node_markers::Node3DMarker)>,
354                    ),
355                    $godot_to_bevy_query
356                >,
357                mut godot: $crate::interop::GodotAccess,
358            ) {
359                [<pre_update_godot_transforms_ $name:lower _individual>](entities, &mut godot);
360            }
361
362            #[cfg(debug_assertions)]
363            fn [<pre_update_godot_transforms_ $name:lower _bulk>](
364                mut entities: $crate::bevy_ecs::system::Query<
365                    (
366                        $crate::bevy_ecs::entity::Entity,
367                        &mut $crate::bevy_transform::components::Transform,
368                        &$crate::interop::GodotNodeHandle,
369                        &mut $crate::plugins::transforms::TransformSyncMetadata,
370                        $crate::bevy_ecs::query::AnyOf<(&$crate::interop::node_markers::Node2DMarker, &$crate::interop::node_markers::Node3DMarker)>,
371                    ),
372                    $godot_to_bevy_query
373                >,
374                mut batch_singleton: godot::prelude::Gd<godot::classes::Object>,
375            ) {
376                use $crate::bevy_ecs::change_detection::DetectChanges;
377                use godot::prelude::ToGodot;
378
379                let _span = tracing::info_span!("bulk_read_preparation", system = stringify!($name)).entered();
380
381                // Collect entity info for 3D and 2D nodes separately
382                let mut entities_3d: Vec<($crate::bevy_ecs::entity::Entity, i64)> = Vec::new();
383                let mut entities_2d: Vec<($crate::bevy_ecs::entity::Entity, i64)> = Vec::new();
384
385                for (entity, _, reference, _, (node2d, node3d)) in entities.iter() {
386                    let instance_id = reference.instance_id().to_i64();
387                    if node2d.is_some() {
388                        entities_2d.push((entity, instance_id));
389                    } else if node3d.is_some() {
390                        entities_3d.push((entity, instance_id));
391                    }
392                }
393
394                drop(_span);
395
396                // Process 3D entities
397                if !entities_3d.is_empty() {
398                    let _span = tracing::info_span!("bulk_read_3d", count = entities_3d.len(), system = stringify!($name)).entered();
399
400                    let instance_ids: Vec<i64> = entities_3d.iter().map(|(_, id)| *id).collect();
401                    let ids_packed = godot::prelude::PackedInt64Array::from(instance_ids.as_slice());
402
403                    let result = batch_singleton
404                        .call("bulk_get_transforms_3d", &[ids_packed.to_variant()])
405                        .to::<godot::builtin::VarDictionary>();
406
407                    if let (Some(positions), Some(rotations), Some(scales)) = (
408                        result
409                            .get("positions")
410                            .map(|v| v.to::<godot::builtin::PackedVector3Array>()),
411                        result
412                            .get("rotations")
413                            .map(|v| v.to::<godot::builtin::PackedVector4Array>()),
414                        result
415                            .get("scales")
416                            .map(|v| v.to::<godot::builtin::PackedVector3Array>()),
417                    ) {
418                        for (i, (entity, _)) in entities_3d.iter().enumerate() {
419                            if let Ok((_, mut bevy_transform, _, mut metadata, _)) = entities.get_mut(*entity) {
420                                if let (Some(pos), Some(rot), Some(scale)) =
421                                    (positions.get(i), rotations.get(i), scales.get(i))
422                                {
423                                    let new_bevy_transform = $crate::bevy_transform::components::Transform {
424                                        translation: $crate::bevy_math::Vec3::new(pos.x, pos.y, pos.z),
425                                        rotation: $crate::bevy_math::Quat::from_xyzw(rot.x, rot.y, rot.z, rot.w),
426                                        scale: $crate::bevy_math::Vec3::new(scale.x, scale.y, scale.z),
427                                    };
428
429                                    if *bevy_transform != new_bevy_transform {
430                                        *bevy_transform = new_bevy_transform;
431                                        metadata.last_sync_tick = Some(bevy_transform.last_changed());
432                                    }
433                                }
434                            }
435                        }
436                    }
437                }
438
439                // Process 2D entities
440                if !entities_2d.is_empty() {
441                    let _span = tracing::info_span!("bulk_read_2d", count = entities_2d.len(), system = stringify!($name)).entered();
442
443                    let instance_ids: Vec<i64> = entities_2d.iter().map(|(_, id)| *id).collect();
444                    let ids_packed = godot::prelude::PackedInt64Array::from(instance_ids.as_slice());
445
446                    let result = batch_singleton
447                        .call("bulk_get_transforms_2d", &[ids_packed.to_variant()])
448                        .to::<godot::builtin::VarDictionary>();
449
450                    if let (Some(positions), Some(rotations), Some(scales)) = (
451                        result
452                            .get("positions")
453                            .map(|v| v.to::<godot::builtin::PackedVector2Array>()),
454                        result
455                            .get("rotations")
456                            .map(|v| v.to::<godot::builtin::PackedFloat32Array>()),
457                        result
458                            .get("scales")
459                            .map(|v| v.to::<godot::builtin::PackedVector2Array>()),
460                    ) {
461                        for (i, (entity, _)) in entities_2d.iter().enumerate() {
462                            if let Ok((_, mut bevy_transform, _, mut metadata, _)) = entities.get_mut(*entity) {
463                                if let (Some(pos), Some(rot), Some(scale)) =
464                                    (positions.get(i), rotations.get(i), scales.get(i))
465                                {
466                                    let new_bevy_transform = $crate::bevy_transform::components::Transform {
467                                        translation: $crate::bevy_math::Vec3::new(pos.x, pos.y, 0.0),
468                                        rotation: $crate::bevy_math::Quat::from_rotation_z(rot),
469                                        scale: $crate::bevy_math::Vec3::new(scale.x, scale.y, 1.0),
470                                    };
471
472                                    if *bevy_transform != new_bevy_transform {
473                                        *bevy_transform = new_bevy_transform;
474                                        metadata.last_sync_tick = Some(bevy_transform.last_changed());
475                                    }
476                                }
477                            }
478                        }
479                    }
480                }
481            }
482
483            fn [<pre_update_godot_transforms_ $name:lower _individual>](
484                mut entities: $crate::bevy_ecs::system::Query<
485                    (
486                        $crate::bevy_ecs::entity::Entity,
487                        &mut $crate::bevy_transform::components::Transform,
488                        &$crate::interop::GodotNodeHandle,
489                        &mut $crate::plugins::transforms::TransformSyncMetadata,
490                        $crate::bevy_ecs::query::AnyOf<(&$crate::interop::node_markers::Node2DMarker, &$crate::interop::node_markers::Node3DMarker)>,
491                    ),
492                    $godot_to_bevy_query
493                >,
494                godot: &mut $crate::interop::GodotAccess,
495            ) {
496                use $crate::plugins::transforms::IntoBevyTransform;
497                use $crate::bevy_ecs::change_detection::DetectChanges;
498                use godot::classes::{Node2D, Node3D};
499
500                for (_, mut bevy_transform, reference, mut metadata, (node2d, node3d)) in entities.iter_mut() {
501                    let new_bevy_transform = if node2d.is_some() {
502                        godot
503                            .get::<Node2D>(*reference)
504                            .get_transform()
505                            .to_bevy_transform()
506                    } else if node3d.is_some() {
507                        godot
508                            .get::<Node3D>(*reference)
509                            .get_transform()
510                            .to_bevy_transform()
511                    } else {
512                        panic!("Expected AnyOf to match either a Node2D or a Node3D, is there a bug in bevy?");
513                    };
514
515                    // Only write if actually different - avoids triggering change detection
516                    if *bevy_transform != new_bevy_transform {
517                        *bevy_transform = new_bevy_transform;
518
519                        // Store the last changed tick for this entity, this helps us in the post_ operations
520                        // to disambiguate our change (syncing from Godot to Bevy above) versus changes that
521                        // *user* systems do this frame. It's only the latter that we may need to copy back to
522                        // Godot
523                        metadata.last_sync_tick = Some(bevy_transform.last_changed());
524                    }
525                }
526            }
527
528            $app.add_systems($crate::bevy_app::PreUpdate, [<pre_update_godot_transforms_ $name:lower>]);
529        }
530    };
531
532}
533
534/// Helper trait to easily disable auto sync and configure custom systems
535pub trait GodotTransformSyncPluginExt {
536    /// Disable automatic transform syncing - you must provide your own sync systems via `add_transform_sync_systems` macro
537    fn without_auto_sync(self) -> Self;
538
539    /// Configure the sync mode while keeping auto sync enabled
540    fn with_sync_mode(self, mode: crate::plugins::transforms::TransformSyncMode) -> Self;
541}
542
543impl GodotTransformSyncPluginExt for crate::plugins::transforms::GodotTransformSyncPlugin {
544    fn without_auto_sync(mut self) -> Self {
545        self.auto_sync = false;
546        self
547    }
548
549    fn with_sync_mode(mut self, mode: crate::plugins::transforms::TransformSyncMode) -> Self {
550        self.sync_mode = mode;
551        self
552    }
553}
554
555// Re-export the macro at the crate level
556pub use add_transform_sync_systems;