mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 18:46:57 -06:00
Animations: PowerPoint's Float Down (preset 47) imports as Float, down (#147)
PowerPoint writes Float In / Float Out with the Float Down option as its own preset, 47 (Descend), not as preset 42 with a direction. DeckCraft only knew 42, so a Float Down entrance or exit imported as a plain fade, which is what the issue's video shows: the buildings fade out in place instead of sinking. Preset 47 now imports as float / floatOut with the "d" option and is written back as 47. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
bd61308cf9
commit
aa65bb8af3
@@ -310,6 +310,10 @@ pub fn fallback_effect(class: AnimClass) -> &'static str {
|
||||
|
||||
/// The preset id to write for an animation.
|
||||
pub fn preset_for(a: &deckcraft_model::Animation) -> u32 {
|
||||
// Float Down is PowerPoint's preset 47 (Float Up is 42).
|
||||
if matches!(a.effect.as_str(), "float" | "floatOut") && a.option == "d" {
|
||||
return 47;
|
||||
}
|
||||
if let Some(p) = a.preset_id {
|
||||
let consistent = match effect_for_preset(a.class, p) {
|
||||
Some(e) => e == a.effect,
|
||||
|
||||
@@ -119,6 +119,12 @@ pub fn animations(imp: &mut Imp, part: &Part, ctx: &IdCtx, timing: &El, _shapes:
|
||||
let Some(shape) = ctx.lookup(file_id) else { continue };
|
||||
let paragraph = tgt.path(&["txEl", "pRg"]).and_then(|p| p.u32("st"));
|
||||
let effect = maps::effect_for_preset(class, preset).unwrap_or_else(|| maps::fallback_effect(class));
|
||||
// PowerPoint writes Float Down as its own preset (47), not as Float with a direction.
|
||||
let (effect, option) = match (class, preset) {
|
||||
(AnimClass::Entrance, 47) => ("float", "d".to_string()),
|
||||
(AnimClass::Exit, 47) => ("floatOut", "d".to_string()),
|
||||
_ => (effect, maps::option_from_subtype(effect, sub)),
|
||||
};
|
||||
let start = match c.attr("nodeType") {
|
||||
Some("clickEffect") => AnimStart::OnClick,
|
||||
Some("afterEffect") => AnimStart::AfterPrevious,
|
||||
@@ -128,7 +134,7 @@ pub fn animations(imp: &mut Imp, part: &Part, ctx: &IdCtx, timing: &El, _shapes:
|
||||
shape,
|
||||
class,
|
||||
effect: effect.to_string(),
|
||||
option: maps::option_from_subtype(effect, sub),
|
||||
option,
|
||||
start,
|
||||
duration_ms: max_dur(c, 0).unwrap_or(0),
|
||||
delay_ms: c.path(&["stCondLst", "cond"]).and_then(|d| ms_attr(d, "delay")).unwrap_or(0),
|
||||
|
||||
@@ -430,7 +430,11 @@ fn effect_par(w: &mut W, x: &mut Exp, ids: &mut Ids, e: &Eff, first_in_click: bo
|
||||
};
|
||||
let _ = first_in_click;
|
||||
let preset = crate::maps::preset_for(a);
|
||||
let sub = crate::maps::subtype_from_option(&a.effect, &a.option).or(a.preset_subtype.filter(|_| a.preset_id == Some(preset))).unwrap_or(0);
|
||||
let sub = if preset == 47 {
|
||||
0
|
||||
} else {
|
||||
crate::maps::subtype_from_option(&a.effect, &a.option).or(a.preset_subtype.filter(|_| a.preset_id == Some(preset))).unwrap_or(0)
|
||||
};
|
||||
let repeat = match a.repeat {
|
||||
u32::MAX => Some("indefinite".to_string()),
|
||||
0 | 1 => None,
|
||||
|
||||
@@ -730,3 +730,28 @@ fn date_fields_keep_their_type_and_saved_text() {
|
||||
assert_eq!(by_name(&q.slides[0], name).text, by_name(&p.slides[0], name).text, "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
/// #147: PowerPoint's Float Down is preset 47 (Float Up is 42); it imported as a plain fade.
|
||||
#[test]
|
||||
fn float_down_is_preset_47_both_ways() {
|
||||
let mut p = defaults::new_presentation(None);
|
||||
let mut s = new_slide(&mut p, "Float");
|
||||
let sh = shape(&mut p, "Building", 0.0, 0.0, 100.0, 100.0);
|
||||
let id = sh.id;
|
||||
s.shapes.push(sh);
|
||||
s.animations = vec![
|
||||
Animation { shape: id, class: AnimClass::Entrance, effect: "float".into(), option: "d".into(), ..Default::default() },
|
||||
Animation { shape: id, class: AnimClass::Exit, effect: "floatOut".into(), option: "d".into(), ..Default::default() },
|
||||
Animation { shape: id, class: AnimClass::Exit, effect: "floatOut".into(), option: "u".into(), ..Default::default() },
|
||||
];
|
||||
p.slides.push(Arc::new(s));
|
||||
let bytes = deckcraft_pptx::export(&p).expect("export");
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).expect("zip");
|
||||
let xml = std::io::read_to_string(z.by_name("ppt/slides/slide2.xml").expect("slide")).expect("read");
|
||||
assert!(xml.contains(r#"presetID="47" presetClass="entr" presetSubtype="0""#), "{xml}");
|
||||
assert!(xml.contains(r#"presetID="47" presetClass="exit" presetSubtype="0""#), "{xml}");
|
||||
assert!(xml.contains(r#"presetID="42" presetClass="exit" presetSubtype="0""#), "{xml}");
|
||||
let q = deckcraft_pptx::import(&bytes).expect("import");
|
||||
let got: Vec<(&str, &str)> = q.slides[1].animations.iter().map(|a| (a.effect.as_str(), a.option.as_str())).collect();
|
||||
assert_eq!(got, [("float", "d"), ("floatOut", "d"), ("floatOut", "u")]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user