From 424c8b2d5084e71df716ec96d18cbb0e756c80e8 Mon Sep 17 00:00:00 2001 From: funman300 Date: Thu, 11 Jun 2026 11:05:24 -0700 Subject: [PATCH] =?UTF-8?q?perf(engine):=20route=20remaining=20drag=20card?= =?UTF-8?q?=E2=86=92entity=20lookups=20through=20CardEntityIndex?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace O(n) `Query::iter().find()` card scans with O(1) `CardEntityIndex` lookups in the mouse and touch drag pipelines (`follow_drag`, `end_drag`, `touch_follow_drag`, `touch_end_drag`) and `update_drag_shadow` — 7 sites across 5 systems. Each ran per dragged card per frame during a drag. `InputPlugin` now defensively `init_resource::()` (idempotent; `CardPlugin` still owns and rebuilds it) so the plugin is self-sufficient in tests. The lone remaining card-keyed `.find` is a `#[cfg(test)]` world-query helper, which is the correct pattern there. Completes the CardEntityIndex migration started in ef1efdc. Co-Authored-By: Claude Opus 4.8 (1M context) --- solitaire_engine/src/card_plugin.rs | 7 +++-- solitaire_engine/src/input_plugin.rs | 42 +++++++++++++++------------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/solitaire_engine/src/card_plugin.rs b/solitaire_engine/src/card_plugin.rs index 546e074..2c1573b 100644 --- a/solitaire_engine/src/card_plugin.rs +++ b/solitaire_engine/src/card_plugin.rs @@ -1487,6 +1487,7 @@ fn update_drag_shadow( drag: Res, layout: Option>, card_entities: Query<(&CardEntity, &Transform)>, + card_index: Res, mut shadow: Local>, ) { if drag.is_idle() { @@ -1503,9 +1504,9 @@ fn update_drag_shadow( // Find the world position of the first (top) dragged card. let top_pos = drag.cards.first().and_then(|first_card| { - card_entities - .iter() - .find(|(marker, _)| marker.card == *first_card) + card_index + .get(first_card) + .and_then(|entity| card_entities.get(entity).ok()) .map(|(_, t)| t.translation) }); diff --git a/solitaire_engine/src/input_plugin.rs b/solitaire_engine/src/input_plugin.rs index 5477b2c..e3be4ac 100644 --- a/solitaire_engine/src/input_plugin.rs +++ b/solitaire_engine/src/input_plugin.rs @@ -33,7 +33,9 @@ use solitaire_core::game_state::GameState; use crate::auto_complete_plugin::AutoCompleteState; use crate::card_animation::tuning::AnimationTuning; use crate::card_animation::{CardAnimation, MotionCurve}; -use crate::card_plugin::{CardEntity, HintHighlight, HintHighlightTimer, STACK_FAN_FRAC}; +use crate::card_plugin::{ + CardEntity, CardEntityIndex, HintHighlight, HintHighlightTimer, STACK_FAN_FRAC, +}; use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL; use crate::events::{ DrawRequestEvent, ForfeitRequestEvent, HintVisualEvent, InfoToastEvent, MoveRejectedEvent, @@ -121,6 +123,10 @@ impl Plugin for InputPlugin { .init_resource::() .init_resource::() .init_resource::() + // The drag systems resolve cards via `CardEntityIndex`; `CardPlugin` + // owns and rebuilds it, but init here too so `InputPlugin` is + // self-sufficient in tests (idempotent if already registered). + .init_resource::() .add_message::() .add_message::() .add_message::() @@ -674,6 +680,7 @@ fn follow_drag( layout: Option>, tuning: Res, mut card_transforms: Query<(&CardEntity, &mut Transform, &mut Sprite)>, + card_index: Res, ) { // Skip if idle or if a touch drag is running. if drag.is_idle() || drag.active_touch_id.is_some() { @@ -704,9 +711,8 @@ fn follow_drag( // Elevate cards: push to DRAG_Z and dim slightly so the board // beneath stays readable. for (i, card) in drag.cards.iter().enumerate() { - if let Some((_, mut transform, mut sprite)) = card_transforms - .iter_mut() - .find(|(ce, _, _)| ce.card == *card) + if let Some(entity) = card_index.get(card) + && let Ok((_, mut transform, mut sprite)) = card_transforms.get_mut(entity) { transform.translation.z = dragged_card_z(i); sprite.color.set_alpha(0.85); @@ -719,9 +725,8 @@ fn follow_drag( let fan = -layout.0.card_size.y * layout.0.tableau_fan_frac; for (i, card) in drag.cards.iter().enumerate() { - if let Some((_, mut transform, _)) = card_transforms - .iter_mut() - .find(|(ce, _, _)| ce.card == *card) + if let Some(entity) = card_index.get(card) + && let Ok((_, mut transform, _)) = card_transforms.get_mut(entity) { transform.translation.x = bottom_pos.x; transform.translation.y = bottom_pos.y + fan * i as f32; @@ -743,6 +748,7 @@ fn end_drag( mut changed: MessageWriter, mut commands: Commands, card_entities: Query<(Entity, &CardEntity, &Transform)>, + card_index: Res, ) { if paused.is_some_and(|p| p.0) { drag.clear(); @@ -830,9 +836,8 @@ fn end_drag( continue; }; let target_pos = card_position(&game.0, &layout.0, &origin, stack_index); - if let Some((entity, _, transform)) = card_entities - .iter() - .find(|(_, ce, _)| ce.card == *card) + if let Some(entity) = card_index.get(card) + && let Ok((_, _, transform)) = card_entities.get(entity) { let drag_pos = transform.translation.truncate(); let drag_z = transform.translation.z; @@ -930,6 +935,7 @@ fn touch_follow_drag( layout: Option>, tuning: Res, mut card_transforms: Query<(&CardEntity, &mut Transform, &mut Sprite)>, + card_index: Res, ) { let Some(active_id) = drag.active_touch_id else { return; // Mouse drag or idle. @@ -957,9 +963,8 @@ fn touch_follow_drag( drag.committed = true; for (i, card) in drag.cards.iter().enumerate() { - if let Some((_, mut transform, mut sprite)) = card_transforms - .iter_mut() - .find(|(ce, _, _)| ce.card == *card) + if let Some(entity) = card_index.get(card) + && let Ok((_, mut transform, mut sprite)) = card_transforms.get_mut(entity) { transform.translation.z = dragged_card_z(i); sprite.color.set_alpha(0.85); @@ -971,9 +976,8 @@ fn touch_follow_drag( let fan = -layout.0.card_size.y * layout.0.tableau_fan_frac; for (i, card) in drag.cards.iter().enumerate() { - if let Some((_, mut transform, _)) = card_transforms - .iter_mut() - .find(|(ce, _, _)| ce.card == *card) + if let Some(entity) = card_index.get(card) + && let Ok((_, mut transform, _)) = card_transforms.get_mut(entity) { transform.translation.x = bottom_pos.x; transform.translation.y = bottom_pos.y + fan * i as f32; @@ -998,6 +1002,7 @@ fn touch_end_drag( mut changed: MessageWriter, mut commands: Commands, card_entities: Query<(Entity, &CardEntity, &Transform)>, + card_index: Res, ) { let Some(active_id) = drag.active_touch_id else { return; // Mouse drag or idle. @@ -1070,9 +1075,8 @@ fn touch_end_drag( continue; }; let target_pos = card_position(&game.0, &layout.0, &origin, stack_index); - if let Some((entity, _, transform)) = card_entities - .iter() - .find(|(_, ce, _)| ce.card == *card) + if let Some(entity) = card_index.get(card) + && let Ok((_, _, transform)) = card_entities.get(entity) { let drag_pos = transform.translation.truncate(); let drag_z = transform.translation.z;