Set Transparent Color: map the click through the picture's 3-D camera and find grouped pictures that keep no box of their own

This commit is contained in:
Marco Arnulfo
2026-10-23 19:16:09 +02:00
parent de18659978
commit b7ea777be7
3 changed files with 89 additions and 11 deletions
+4 -2
View File
@@ -160,7 +160,9 @@ fn picture_pixel_at(s: &Session, p: &Value, at: &Value) -> Result<(ShapeId, Rgba
for id in ids {
let Some((sh, xf, to_slide)) = placed(&st.doc, &st.selection, shapes, id, Affine::IDENTITY, 0) else { continue };
let ShapeKind::Picture { fill } = &sh.kind else { continue };
let local = to_slide.inverse() * Point::new(x, y);
// The camera turns the picture where it is drawn, so the click is mapped back through it too.
let turn = text::ctx_for(&st.doc, &st.selection).and_then(|c| deckcraft_render::camera_turn(&c, sh, xf.w, xf.h)).unwrap_or(Affine::IDENTITY);
let local = (to_slide * turn).inverse() * Point::new(x, y);
if !(local.is_finite() && (0.0..=xf.w).contains(&local.x) && (0.0..=xf.h).contains(&local.y)) {
continue;
}
@@ -203,7 +205,7 @@ fn placed<'a>(
return None;
}
for sh in shapes {
let Some(x) = (if depth == 0 { Some(xfrm_of(doc, sel, sh)) } else { sh.xfrm }) else { continue };
let x = xfrm_of(doc, sel, sh);
if sh.id == id {
return Some((sh, x, to_slide * x.affine()));
}
+74
View File
@@ -2391,6 +2391,80 @@ fn transparent_color_at_a_point_takes_the_pictures_own_pixel_there() {
s.execute("picture.transparentColor", &json!({"ids": [logo], "at": [450, 500]})).unwrap();
}
/// A picture of `n` one-pixel columns whose red channel is 20 * column, inserted at `rect`.
fn ramp_picture(s: &mut Session, n: u32, rect: [u32; 4]) -> serde_json::Value {
let img = image::RgbaImage::from_fn(n, 1, |x, _| image::Rgba([(20 * x) as u8, 0, 0, 255]));
let mut png = vec![];
image::DynamicImage::ImageRgba8(img).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
s.execute("insert.picture", &json!({"data": cmd::base64_encode(&png), "name": "r.png", "rect": rect})).unwrap()["id"].clone()
}
fn clear_color_of(s: &Session, id: &serde_json::Value) -> Option<deckcraft_color::Rgba> {
fn find(shapes: &[deckcraft_model::Shape], id: &serde_json::Value) -> Option<deckcraft_model::Shape> {
shapes.iter().find_map(|sh| {
if json!(sh.id) == *id {
return Some(sh.clone());
}
match &sh.kind {
deckcraft_model::ShapeKind::Group { children, .. } => find(children, id),
_ => None,
}
})
}
match find(s.doc().unwrap().shapes(), id).unwrap().kind {
deckcraft_model::ShapeKind::Picture { fill } => fill.adjust.clear_color,
_ => panic!("not a picture"),
}
}
#[test]
fn transparent_color_at_a_point_follows_the_pictures_3d_camera() {
let mut s = session();
let id = ramp_picture(&mut s, 10, [100, 100, 200, 100]);
// Turned 60 degrees about the vertical axis the picture is drawn about half as wide, centred
// where it was: 149..251 instead of 100..300.
s.execute("shape.effects", &json!({"ids": [id], "threeD": {"camera": "orthographicFront", "rotX": 60}})).unwrap();
// 175 is a quarter of the way across the drawn picture: column 2, not column 3 (the 75/200
// of the unturned box).
s.execute("picture.transparentColor", &json!({"ids": [id], "at": [175, 150]})).unwrap();
assert_eq!(clear_color_of(&s, &id), Some(deckcraft_color::Rgba::rgb(40, 0, 0)));
// Inside the unturned box but outside the drawn picture: nothing is under the pointer.
assert!(s.execute("picture.transparentColor", &json!({"ids": [id], "at": [120, 150]})).is_err());
}
#[test]
fn transparent_color_at_a_point_finds_a_grouped_picture_without_its_own_box() {
let mut s = session();
let pic = ramp_picture(&mut s, 10, [100, 100, 200, 100]);
let other = s.execute("shape.insert", &json!({"preset": "rect", "rect": [400, 300, 50, 50]})).unwrap()["id"].clone();
s.execute("edit.select", &json!({"ids": [pic, other]})).unwrap();
s.execute("arrange.group", &json!({})).unwrap();
// A grouped picture that keeps no box of its own is drawn where the default box puts it.
let sz = s.doc().unwrap().doc.slide_size;
s.edit(|doc, sel| {
fn clear(shapes: &mut [deckcraft_model::Shape], id: &serde_json::Value) {
for sh in shapes {
if json!(sh.id) == *id {
sh.xfrm = None;
}
if let deckcraft_model::ShapeKind::Group { children, .. } = &mut sh.kind {
clear(children, id);
}
}
}
clear(crate::shapes_mut(doc, sel).unwrap(), &pic);
Ok(())
})
.unwrap();
// Left tenth of the default box (10%, 10%, 80% x 20% of the slide): column 0.
let at = [sz.width * 0.1 + 1.0, sz.height * 0.2];
s.execute("picture.transparentColor", &json!({"ids": [pic], "at": at})).unwrap();
assert_eq!(clear_color_of(&s, &pic), Some(deckcraft_color::Rgba::rgb(0, 0, 0)));
let at = [sz.width * 0.9 - 1.0, sz.height * 0.2];
s.execute("picture.transparentColor", &json!({"ids": [pic], "at": at})).unwrap();
assert_eq!(clear_color_of(&s, &pic), Some(deckcraft_color::Rgba::rgb(180, 0, 0)));
}
#[test]
fn select_all_slides_scope() {
let mut s = session();