mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-24 10:09:01 -06:00
Merge PR #2210
This commit is contained in:
@@ -2245,9 +2245,15 @@ fn mask_overlay(app: &mut LightcraftApp, ui: &mut egui::Ui, resp: &egui::Respons
|
||||
}
|
||||
for m in &d.masks {
|
||||
let sel = Some(m.id) == active;
|
||||
// Ctrl+Shift+O (`mask_overlay_all`) shows every visible mask's region; outline each one
|
||||
// too, not just the selected mask. Handles and the rotation grip stay on the selected
|
||||
// mask so nothing else becomes draggable. The component check keeps this in step with the
|
||||
// painted region (`view_overlay` at detail.rs:1354 only covers masks with components).
|
||||
let show_outline = sel || (app.ui.mask_overlay_all && m.visible && !m.components.is_empty());
|
||||
let stroke_a: u8 = if sel { 230 } else { 170 };
|
||||
for (ci, c) in m.components.iter().enumerate() {
|
||||
match &c.shape {
|
||||
MaskShape::Radial { center, rx, ry, angle, .. } if sel => {
|
||||
MaskShape::Radial { center, rx, ry, angle, .. } if show_outline => {
|
||||
let pts: Vec<Pos2> = (0..64)
|
||||
.map(|i| {
|
||||
let a = i as f64 / 64.0 * std::f64::consts::TAU;
|
||||
@@ -2257,39 +2263,44 @@ fn mask_overlay(app: &mut LightcraftApp, ui: &mut egui::Ui, resp: &egui::Respons
|
||||
map.screen(Point::new(center.x + (x * co - y * s) * l.0, center.y + (x * s + y * co) * l.1))
|
||||
})
|
||||
.collect();
|
||||
p.add(egui::Shape::closed_line(pts, Stroke::new(1.5, Color32::from_white_alpha(230))));
|
||||
p.add(egui::Shape::closed_line(pts, Stroke::new(1.5, Color32::from_white_alpha(stroke_a))));
|
||||
|
||||
if sel {
|
||||
let l = frame_long_norm(map);
|
||||
for (i, a_deg) in [0.0_f64, 90.0_f64, 180.0_f64, 270.0_f64].into_iter().enumerate() {
|
||||
let a_local = a_deg.to_radians();
|
||||
let (s, co) = angle.to_radians().sin_cos();
|
||||
let (x, y) = (rx * a_local.cos(), ry * a_local.sin());
|
||||
let q = map.screen(Point::new(center.x + (x * co - y * s) * l.0, center.y + (x * s + y * co) * l.1));
|
||||
p.circle_stroke(q, 5.0, Stroke::new(1.5, Color32::WHITE));
|
||||
register(ui.ctx(), format!("maskHandle:{}:{ci}:{}", m.id, i + 1), Rect::from_center_size(q, vec2(14.0, 14.0)));
|
||||
grips.push((m.id, ci, i as u8 + 1, q));
|
||||
}
|
||||
|
||||
let l = frame_long_norm(map);
|
||||
for (i, a_deg) in [0.0_f64, 90.0_f64, 180.0_f64, 270.0_f64].into_iter().enumerate() {
|
||||
let a_local = a_deg.to_radians();
|
||||
let (s, co) = angle.to_radians().sin_cos();
|
||||
let (x, y) = (rx * a_local.cos(), ry * a_local.sin());
|
||||
let q = map.screen(Point::new(center.x + (x * co - y * s) * l.0, center.y + (x * s + y * co) * l.1));
|
||||
p.circle_stroke(q, 5.0, Stroke::new(1.5, Color32::WHITE));
|
||||
register(ui.ctx(), format!("maskHandle:{}:{ci}:{}", m.id, i + 1), Rect::from_center_size(q, vec2(14.0, 14.0)));
|
||||
grips.push((m.id, ci, i as u8 + 1, q));
|
||||
let rot_r = rx + 0.08;
|
||||
let rot_q = map.screen(Point::new(center.x + (rot_r * co) * l.0, center.y + (rot_r * s) * l.1));
|
||||
let h1_q = map.screen(Point::new(center.x + (rx * co) * l.0, center.y + (rx * s) * l.1));
|
||||
p.line_segment([h1_q, rot_q], Stroke::new(1.0, Color32::from_white_alpha(200)));
|
||||
p.circle_stroke(rot_q, 4.0, Stroke::new(1.5, Color32::WHITE));
|
||||
register(ui.ctx(), format!("maskHandle:{}:{ci}:5", m.id), Rect::from_center_size(rot_q, vec2(14.0, 14.0)));
|
||||
grips.push((m.id, ci, 5, rot_q));
|
||||
}
|
||||
|
||||
let (s, co) = angle.to_radians().sin_cos();
|
||||
let rot_r = rx + 0.08;
|
||||
let rot_q = map.screen(Point::new(center.x + (rot_r * co) * l.0, center.y + (rot_r * s) * l.1));
|
||||
let h1_q = map.screen(Point::new(center.x + (rx * co) * l.0, center.y + (rx * s) * l.1));
|
||||
p.line_segment([h1_q, rot_q], Stroke::new(1.0, Color32::from_white_alpha(200)));
|
||||
p.circle_stroke(rot_q, 4.0, Stroke::new(1.5, Color32::WHITE));
|
||||
register(ui.ctx(), format!("maskHandle:{}:{ci}:5", m.id), Rect::from_center_size(rot_q, vec2(14.0, 14.0)));
|
||||
grips.push((m.id, ci, 5, rot_q));
|
||||
}
|
||||
MaskShape::Linear { start, end } if sel => {
|
||||
MaskShape::Linear { start, end } if show_outline => {
|
||||
let (a, b) = (map.screen(*start), map.screen(*end));
|
||||
let dir = (b - a).normalized();
|
||||
let perp = vec2(-dir.y, dir.x) * 3000.0;
|
||||
for (q, alpha) in [(a, 230), (b, 110)] {
|
||||
let (a1, a2): (u8, u8) = if sel { (230, 110) } else { (170, 80) };
|
||||
for (q, alpha) in [(a, a1), (b, a2)] {
|
||||
p.line_segment([q - perp, q + perp], Stroke::new(1.0, Color32::from_white_alpha(alpha)));
|
||||
}
|
||||
for (i, q) in [a, b].iter().enumerate() {
|
||||
p.circle_stroke(*q, 5.0, Stroke::new(1.5, Color32::WHITE));
|
||||
register(ui.ctx(), format!("maskHandle:{}:{ci}:{i}", m.id), Rect::from_center_size(*q, vec2(14.0, 14.0)));
|
||||
grips.push((m.id, ci, i as u8 + 1, *q));
|
||||
if sel {
|
||||
for (i, q) in [a, b].iter().enumerate() {
|
||||
p.circle_stroke(*q, 5.0, Stroke::new(1.5, Color32::WHITE));
|
||||
register(ui.ctx(), format!("maskHandle:{}:{ci}:{i}", m.id), Rect::from_center_size(*q, vec2(14.0, 14.0)));
|
||||
grips.push((m.id, ci, i as u8 + 1, *q));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -363,6 +363,63 @@ fn mask_overlay_keys_and_pins() {
|
||||
h.settle(SETTLE);
|
||||
}
|
||||
|
||||
/// #2210: Ctrl+Shift+O (the all-masks overlay) draws the white outline of every visible
|
||||
/// Radial/Linear mask, not just the selected one — but only the selected mask registers
|
||||
/// `maskHandle:` elements, so nothing else becomes draggable.
|
||||
#[test]
|
||||
fn all_masks_overlay_outlines_every_visible_mask_but_only_the_selected_one_is_draggable() {
|
||||
let mut h = detail("panel.masking");
|
||||
// Two visible radial masks; mask 2 ends up selected (mask.add selects the new one).
|
||||
exec(&mut h, "mask.add", json!({"kind": "radial", "center": [0.3, 0.5], "rx": 0.12, "ry": 0.12}));
|
||||
exec(&mut h, "mask.add", json!({"kind": "radial", "center": [0.7, 0.5], "rx": 0.12, "ry": 0.12}));
|
||||
exec(&mut h, "view.maskOverlayAll", json!({}));
|
||||
assert!(h.app.ui.mask_overlay_all);
|
||||
assert_eq!(h.app.session.active_mask, Some(2));
|
||||
// Pins add their own <maskPin:…> elements; keep them out of the handle count.
|
||||
exec(&mut h, "view.maskPins", json!({"show": false}));
|
||||
h.settle(SETTLE);
|
||||
|
||||
let handles_of = |h: &Headless, id: usize| h.app.widgets.iter().filter(|(w, _)| w.starts_with(&format!("maskHandle:{id}:"))).count();
|
||||
// The selected mask keeps its 4 edge handles and its rotation grip.
|
||||
assert_eq!(handles_of(&h, 2), 5, "the selected mask keeps its handles");
|
||||
// The other visible mask draws its outline but registers no handle: it is not draggable.
|
||||
assert_eq!(handles_of(&h, 1), 0, "an unselected mask must not become draggable");
|
||||
// …while a mask with no components has nothing to outline and nothing to grab either.
|
||||
exec(&mut h, "mask.add", json!({"kind": "luminanceRange"}));
|
||||
exec(&mut h, "mask.addComponent", json!({"kind": "radial", "op": "add", "center": [0.5, 0.8], "rx": 0.08, "ry": 0.08}));
|
||||
assert_eq!(h.app.session.active_mask, Some(3));
|
||||
assert_eq!(handles_of(&h, 1), 0);
|
||||
h.settle(SETTLE);
|
||||
}
|
||||
|
||||
/// #2210: the outlines of *unselected* masks are painted, not registered as widgets, so the way to
|
||||
/// observe them is the rendered pixels. With the all-masks overlay on, the unselected mask's
|
||||
/// ellipse edge must carry white stroke pixels the photo alone does not have.
|
||||
#[test]
|
||||
fn all_masks_overlay_paints_the_outline_of_an_unselected_mask() {
|
||||
let mut h = detail("panel.masking");
|
||||
exec(&mut h, "mask.add", json!({"kind": "radial", "center": [0.3, 0.5], "rx": 0.12, "ry": 0.12}));
|
||||
exec(&mut h, "mask.add", json!({"kind": "radial", "center": [0.7, 0.5], "rx": 0.12, "ry": 0.12}));
|
||||
assert_eq!(h.app.session.active_mask, Some(2));
|
||||
exec(&mut h, "view.maskPins", json!({"show": false}));
|
||||
|
||||
// Sample the whole canvas: the outline is white (alpha 170) over the photo, so the overlay adds
|
||||
// near-white pixels along the unselected mask's ellipse edge.
|
||||
let white_pixels = |h: &mut Headless, on: bool| -> usize {
|
||||
let overlay = h.app.ui.mask_overlay_all;
|
||||
if overlay != on {
|
||||
exec(h, "view.maskOverlayAll", json!({}));
|
||||
}
|
||||
assert_eq!(h.app.ui.mask_overlay_all, on);
|
||||
h.settle(SETTLE);
|
||||
let px = h.view.paint(&Default::default());
|
||||
px.pixels.iter().filter(|p| p.r() > 200 && p.g() > 200 && p.b() > 200).count()
|
||||
};
|
||||
let off = white_pixels(&mut h, false);
|
||||
let on = white_pixels(&mut h, true);
|
||||
assert!(on > off, "the unselected mask's outline must add white pixels: off {off}, on {on}");
|
||||
}
|
||||
|
||||
/// #518: holding the brush still, however long, paints the same feathered spot as a quick press.
|
||||
#[test]
|
||||
fn holding_the_brush_still_does_not_harden_the_spot() {
|
||||
|
||||
Reference in New Issue
Block a user