diff --git a/crates/ui-egui/src/panels/puppet_tool.rs b/crates/ui-egui/src/panels/puppet_tool.rs index da2c3aad..9fa8ac6a 100644 --- a/crates/ui-egui/src/panels/puppet_tool.rs +++ b/crates/ui-egui/src/panels/puppet_tool.rs @@ -170,9 +170,18 @@ pub fn draw( } } // Pins grow under the pointer. + // Selected pins are filled with their colour and ringed in white; unselected pins are + // hollow (dark centre, coloured outline), like selected and unselected mask vertices. + // Drawing only: hit-testing uses `pos` alone. let r = if sel { 5.5 } else { 4.5 } + if hover.is_some_and(|h| h.distance(pos) < 8.0) { 1.5 } else { 0.0 }; - painter.circle_filled(pos, r, if sel { col } else { col.gamma_multiply(0.85) }); - painter.circle_stroke(pos, r, Stroke::new(1.0, Color32::BLACK)); + if sel { + painter.circle_filled(pos, r, col); + painter.circle_stroke(pos, r, Stroke::new(1.0, Color32::BLACK)); + painter.circle_stroke(pos, r + 2.5, Stroke::new(1.5, Color32::WHITE)); + } else { + painter.circle_filled(pos, r, Color32::from_black_alpha(160)); + painter.circle_stroke(pos, r, Stroke::new(1.5, col)); + } hits.push(PinHit { layer: layer.id, pin: pn.uid, kind: pn.kind, pos, rotation: pn.rotation, scale: pn.scale, handle }); } } diff --git a/crates/ui-egui/tests/ui_viewer.rs b/crates/ui-egui/tests/ui_viewer.rs index 70d2e7a4..124292ef 100644 --- a/crates/ui-egui/tests/ui_viewer.rs +++ b/crates/ui-egui/tests/ui_viewer.rs @@ -543,6 +543,50 @@ fn puppet_pin_click_selects_and_delete_removes_only_the_pins() { assert!(h.state().session.active_comp().unwrap().layer(LayerId(id)).is_some()); } +/// Filled circles painted this frame (flattening nested shape lists). +fn circles(h: &Harness<'_, EffectcraftApp>) -> Vec { + fn walk(s: &egui::Shape, out: &mut Vec) { + match s { + egui::Shape::Circle(c) => out.push(*c), + egui::Shape::Vec(v) => v.iter().for_each(|s| walk(s, out)), + _ => {} + } + } + let mut out = vec![]; + for c in &h.output().shapes { + walk(&c.shape, &mut out); + } + out +} + +/// Issue #96: a selected puppet pin is filled with its colour and ringed; an unselected one is +/// hollow (dark centre, coloured outline), like selected and unselected mask vertices. They used to +/// differ by 1 px and a slight brightness change. +#[test] +fn selected_puppet_pins_are_filled_and_unselected_hollow() { + let (mut h, _id, bend) = puppet_harness(); + let pins: Vec = h.state().auto.previous.iter().filter_map(|e| e.id.strip_prefix("viewer.puppetPin.").and_then(|r| r.parse().ok())).collect(); + let position: Vec = pins.into_iter().filter(|p| *p != bend).collect(); + let (a, b) = (position[0], position[1]); + let pa = rect(&h, &format!("viewer.puppetPin.{a}")).center(); + let pb = rect(&h, &format!("viewer.puppetPin.{b}")).center(); + click(&mut h, pa); + assert_eq!(h.state().session.state.selected_props.iter().map(|(_, u)| *u).collect::>(), vec![a]); + // Pointer away from the pins (hover enlarges a pin), then look at the painted pins. + h.input_mut().events.push(Event::PointerMoved(pos2(5.0, 5.0))); + h.run_steps(2); + let col = effectcraft_ui_egui::panels::puppet_tool::pin_color(effectcraft_engine::effects::puppet::PinKind::Position); + let at = |p: Pos2| circles(&h).into_iter().filter(move |c| c.center.distance(p) < 0.5).collect::>(); + let dark = |c: egui::Color32| c.r().max(c.g()).max(c.b()) < 0x60; + let sel = at(pa); + assert!(sel.iter().any(|c| c.fill == col), "the selected pin is filled with its colour: {sel:?}"); + assert!(sel.iter().any(|c| c.fill == egui::Color32::TRANSPARENT && c.stroke.color == egui::Color32::WHITE), "and ringed in white: {sel:?}"); + let unsel = at(pb); + assert!(!unsel.is_empty(), "the unselected pin is drawn"); + assert!(unsel.iter().all(|c| c.fill == egui::Color32::TRANSPARENT || dark(c.fill)), "the unselected pin is hollow: {unsel:?}"); + assert!(unsel.iter().any(|c| c.stroke.color == col), "with a coloured outline: {unsel:?}"); +} + #[test] fn puppet_marquee_selects_pins_and_alt_drag_works_over_the_art() { let (mut h, _id, bend) = puppet_harness();