mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-13 14:59:08 +00:00
Render: 3-D shapes' faces are lit even without a bevel
A shape with scene3d and sp3d but no bevelT was drawn in its flat fill colour. PowerPoint lights the front face from the light rig whether or not there is a bevel, so a plain 3-D rotated rectangle is lighter than its fill. Model the face with the same threePt approximation as the bevel (no rim), for solid fills. Wild 3-D slides: s8 6.35 -> 4.26, s9 19.74 -> 17.68. Corpus unchanged except lo tdf114848 s4 2.13 -> 1.94. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
238157d98e
commit
8ee082f257
2 files changed
+47
-6
No files matched your search
@@ -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<Bevel> {
|
||||
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<Bevel> {
|
||||
let emu = |name: &str| attr(name).and_then(|v| v.parse::<f64>().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<Bevel> {
|
||||
};
|
||||
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<Rgba>, 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#"<a:sp3d><a:bevelT w="50800"/></a:sp3d>"#), None);
|
||||
assert_eq!(from_raw(r#"<a:scene3d/><a:sp3d extrusionH="10"/>"#), None);
|
||||
// A 3-D shape with no bevel: a bevel of no width (its face is lit).
|
||||
assert!(from_raw(r#"<a:scene3d/><a:sp3d extrusionH="10"/>"#).is_some_and(|b| b.w == 0.0 && b.modelled));
|
||||
assert_eq!(from_raw(r#"<a:scene3d/>"#), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -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#"<a:scene3d><a:camera prst="orthographicFront"/><a:lightRig rig="threePt" dir="t"/></a:scene3d><a:sp3d/>"#)]);
|
||||
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#"<a:scene3d><a:camera prst="orthographicFront"/><a:lightRig rig="threePt" dir="t"/></a:scene3d>"#)]);
|
||||
assert!(close(img.pixel(175, 150), base, 1));
|
||||
}
|
||||
Reference in new issue
Block a user