diff --git a/crates/render/src/bevel.rs b/crates/render/src/bevel.rs index 85dfb3b..36de1f0 100644 --- a/crates/render/src/bevel.rs +++ b/crates/render/src/bevel.rs @@ -34,10 +34,14 @@ pub struct Bevel { /// The top bevel of 3-D XML kept from the file (`scene3d` + `sp3d`), when it has both a scene /// (without one PowerPoint draws the shape flat) and a `bevelT`. pub fn from_raw(raw: &str) -> Option { - if !raw.contains("scene3d") { + if !raw.contains("scene3d") || !raw.contains("sp3d") { return None; } - let i = raw.find("bevelT")?; + // A 3-D shape without a bevel still has its face lit: a bevel of no width. + let Some(i) = raw.find("bevelT") else { + let b = Bevel { w: 0.0, h: 0.0, preset: "circle".into(), modelled: false }; + return Some(Bevel { modelled: modelled(raw), ..b }); + }; let tag = raw.get(i..)?; let tag = tag.get(..tag.find('>')?)?; let attr = |name: &str| -> Option<&str> { @@ -49,7 +53,12 @@ pub fn from_raw(raw: &str) -> Option { let emu = |name: &str| attr(name).and_then(|v| v.parse::().ok()).map(|v| v / 12700.0); let w = emu("w").unwrap_or(6.0).clamp(0.0, 2000.0); let h = emu("h").unwrap_or(6.0).clamp(0.0, 2000.0); - // The light rig and material, from anywhere in the XML (`a:lightRig rig=`, `prstMaterial=`). + (w > 0.0).then(|| Bevel { w, h, preset: attr("prst").unwrap_or("circle").to_string(), modelled: modelled(raw) }) +} + +/// Whether the light rig and material (from anywhere in the XML: `a:lightRig rig=`, +/// `prstMaterial=`) are the ones measured: the three-point rig, matte. +fn modelled(raw: &str) -> bool { let value_after = |key: &str| -> Option<&str> { let s = raw.find(key)? + key.len(); let rest = raw.get(s..)?; @@ -57,8 +66,7 @@ pub fn from_raw(raw: &str) -> Option { }; let rig = value_after("rig=\"").unwrap_or("threePt"); let material = value_after("prstMaterial=\"").unwrap_or("warmMatte"); - let modelled = rig == "threePt" && matches!(material, "warmMatte" | "matte"); - (w > 0.0).then(|| Bevel { w, h, preset: attr("prst").unwrap_or("circle").to_string(), modelled }) + rig == "threePt" && matches!(material, "warmMatte" | "matte") } /// The light, pointing from the surface towards it (x right, y down, z towards the viewer). @@ -118,6 +126,18 @@ pub fn draw(ctx: &mut RenderContext, bevel: &Bevel, fill: Option, path: &B if !bevel.modelled || bounds.width() <= 0.0 || bounds.height() <= 0.0 || !bounds.is_finite() { return; } + // No bevel: only the face, one lit colour (as PowerPoint lights a 3-D shape's front face, + // whichever way the camera turns it, within about 10 levels). + if bevel.w <= 0.0 { + if let Some(c) = fill { + let f = AMBIENT + DIFFUSE * light()[2]; + let ch = |v: u8| (v as f64 * f + SPECULAR).round().clamp(0.0, 255.0) as u8; + ctx.set_transform(m); + ctx.set_paint(crate::color(Rgba::rgba(ch(c.r), ch(c.g), ch(c.b), c.a), alpha)); + ctx.fill_path(path); + } + return; + } let k = crate::scale_of(m); let k = if k.is_finite() && k > 0.0 { k } else { 1.0 }; let longest = bounds.width().max(bounds.height()) * k; @@ -193,7 +213,9 @@ mod tests { assert!(from_raw(raw).is_some_and(|b| !b.modelled)); // No scene: flat in PowerPoint. assert_eq!(from_raw(r#""#), None); - assert_eq!(from_raw(r#""#), None); + // A 3-D shape with no bevel: a bevel of no width (its face is lit). + assert!(from_raw(r#""#).is_some_and(|b| b.w == 0.0 && b.modelled)); + assert_eq!(from_raw(r#""#), None); } #[test] diff --git a/crates/render/src/tests_wild.rs b/crates/render/src/tests_wild.rs index 4aea690..7e7f0ea 100644 --- a/crates/render/src/tests_wild.rs +++ b/crates/render/src/tests_wild.rs @@ -725,3 +725,22 @@ fn a_picture_reflects_its_pixels() { assert!(below[2] > 200 && below[0] < 200, "{below:?}"); assert!(close(img.pixel(200, 290), WHITE, 10), "faded out by half its height"); } + +#[test] +fn a_3d_shapes_face_is_lit_even_without_a_bevel() { + let base = Rgba::rgb(237, 125, 49); + let shape = |raw: &str| Shape { + id: ShapeId(31), + xfrm: Some(Xfrm::new(100.0, 100.0, 150.0, 100.0)), + fill: solid(base), + line: no_line(), + effects: Some(deckcraft_model::Effects { raw3d: Some(raw.into()), ..Default::default() }), + ..Default::default() + }; + // PowerPoint shows this face as about (251, 135, 56). + let img = render(vec![shape(r#""#)]); + assert!(close(img.pixel(175, 150), Rgba::rgb(248, 136, 60), 8), "{:?}", img.pixel(175, 150)); + // No shape properties in 3-D: flat. + let img = render(vec![shape(r#""#)]); + assert!(close(img.pixel(175, 150), base, 1)); +}