mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-25 19:17:06 -06:00
Merge PR #600 from NikitkaRa: render: 3-D contours
This commit is contained in:
@@ -44,22 +44,55 @@ pub fn from_raw(raw: &str) -> Option<Extrusion> {
|
||||
if !(depth.is_finite() && depth > 0.01) {
|
||||
return None;
|
||||
}
|
||||
let color = raw.find("extrusionClr").and_then(|j| {
|
||||
let body = raw.get(j..)?;
|
||||
let body = body.get(..body.find("/a:extrusionClr").or_else(|| body.find("/extrusionClr")).unwrap_or(body.len()))?;
|
||||
if let Some(k) = body.find("srgbClr ") {
|
||||
let v = attr(body.get(k..)?, "val")?;
|
||||
let n = u32::from_str_radix(v, 16).ok()?;
|
||||
Some(ColorRef::rgb(Rgba::rgb((n >> 16) as u8, (n >> 8) as u8, n as u8)))
|
||||
} else if let Some(k) = body.find("schemeClr ") {
|
||||
SchemeSlot::from_xml(attr(body.get(k..)?, "val")?).map(ColorRef::scheme)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let color = color_in(raw, "extrusionClr");
|
||||
Some(Extrusion { depth: depth.min(4000.0), color })
|
||||
}
|
||||
|
||||
/// The colour in the first `<a:{name}>` element of `raw` (an `srgbClr` or a `schemeClr`).
|
||||
fn color_in(raw: &str, name: &str) -> Option<ColorRef> {
|
||||
let j = raw.find(&format!("{name}>"))?;
|
||||
let body = raw.get(j..)?;
|
||||
let end = body.find(&format!("/a:{name}")).or_else(|| body.find(&format!("/{name}"))).unwrap_or(body.len());
|
||||
let body = body.get(..end)?;
|
||||
if let Some(k) = body.find("srgbClr ") {
|
||||
let v = attr(body.get(k..)?, "val")?;
|
||||
let n = u32::from_str_radix(v, 16).ok()?;
|
||||
Some(ColorRef::rgb(Rgba::rgb((n >> 16) as u8, (n >> 8) as u8, n as u8)))
|
||||
} else if let Some(k) = body.find("schemeClr ") {
|
||||
SchemeSlot::from_xml(attr(body.get(k..)?, "val")?).map(ColorRef::scheme)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// A 3-D contour: a band of `width` points around the shape's outline (`a:sp3d contourW`,
|
||||
/// `a:contourClr`), drawn behind the face so only its outer half shows past the outline.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Contour {
|
||||
pub width: f64,
|
||||
pub color: Option<ColorRef>,
|
||||
}
|
||||
|
||||
/// The contour in 3-D XML kept from the file, if it has one.
|
||||
pub fn contour_from_raw(raw: &str) -> Option<Contour> {
|
||||
let i = raw.find("sp3d ")?;
|
||||
let tag = raw.get(i..)?;
|
||||
let tag = tag.get(..tag.find('>')?)?;
|
||||
let width = attr(tag, "contourW")?.parse::<f64>().ok()? / 12700.0;
|
||||
if !(width.is_finite() && width > 0.01) {
|
||||
return None;
|
||||
}
|
||||
Some(Contour { width: width.min(2000.0), color: color_in(raw, "contourClr") })
|
||||
}
|
||||
|
||||
/// Draw `c` around `outline` (shape space, mapped by `m`), lit as a surface facing the viewer.
|
||||
pub fn draw_contour(ctx: &mut RenderContext, outline: &BezPath, m: Affine, width: f64, color: Rgba, lit: &Lighting) {
|
||||
ctx.set_transform(m);
|
||||
ctx.set_paint(crate::color(lit.shade(color, [0.0, 0.0, 1.0]), 1.0));
|
||||
ctx.set_stroke(kurbo::Stroke::new(2.0 * width).with_join(kurbo::Join::Round));
|
||||
ctx.stroke_path(outline);
|
||||
}
|
||||
|
||||
/// The camera's view of the depth axis: where the back face sits relative to the front face, in
|
||||
/// the turned shape's space (the space `camera::affine` maps into).
|
||||
pub fn back_offset(c: &Camera, depth: f64, w: f64, h: f64) -> Vec2 {
|
||||
|
||||
@@ -971,6 +971,17 @@ impl Renderer {
|
||||
let lit = light::Lighting::from_raw(raw);
|
||||
extrude::draw(ctx, &outline, w, h, &cam, ex.depth, pre, turn, extrude::side_color(rctx, &ex, face), &lit);
|
||||
}
|
||||
// A 3-D contour: a band around the outline, behind the face (and in front of any sides).
|
||||
if let Some(raw) = effects.as_ref().and_then(|e| e.raw3d.as_deref())
|
||||
&& let Some(c) = extrude::contour_from_raw(raw)
|
||||
{
|
||||
let face = match &fill {
|
||||
Some(Fill::Solid { color: col }) => Some(rctx.color(col, fill_ph)),
|
||||
_ => None,
|
||||
};
|
||||
let col = c.color.as_ref().map(|k| rctx.color(k, face)).or(face).unwrap_or(Rgba::rgb(0xff, 0xff, 0xff));
|
||||
extrude::draw_contour(ctx, &outline, m, c.width, col, &light::Lighting::from_raw(raw));
|
||||
}
|
||||
// Fill.
|
||||
match &s.kind {
|
||||
ShapeKind::Picture { fill: pf } => {
|
||||
|
||||
@@ -1846,7 +1846,7 @@ fn extruded_deck() -> Presentation {
|
||||
701,
|
||||
280.0,
|
||||
"ellipse",
|
||||
r#"<a:scene3d><a:camera prst="isometricTopUp"/><a:lightRig rig="threePt" dir="t"/></a:scene3d><a:sp3d extrusionH="254000"><a:extrusionClr><a:srgbClr val="C00000"/></a:extrusionClr></a:sp3d>"#,
|
||||
r#"<a:scene3d><a:camera prst="isometricTopUp"/><a:lightRig rig="threePt" dir="t"/></a:scene3d><a:sp3d extrusionH="254000" contourW="63500"><a:extrusionClr><a:srgbClr val="C00000"/></a:extrusionClr><a:contourClr><a:srgbClr val="FFC000"/></a:contourClr></a:sp3d>"#,
|
||||
),
|
||||
mk(
|
||||
702,
|
||||
@@ -1858,11 +1858,31 @@ fn extruded_deck() -> Presentation {
|
||||
703,
|
||||
760.0,
|
||||
"rect",
|
||||
r#"<a:scene3d><a:camera prst="orthographicFront"/><a:lightRig rig="threePt" dir="t"/></a:scene3d><a:sp3d extrusionH="381000"/>"#,
|
||||
r#"<a:scene3d><a:camera prst="orthographicFront"/><a:lightRig rig="threePt" dir="t"/></a:scene3d><a:sp3d extrusionH="381000" contourW="76200" prstMaterial="metal"><a:contourClr><a:srgbClr val="00B050"/></a:contourClr></a:sp3d>"#,
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contours_band_the_outline_in_their_colour() {
|
||||
let img = render_slide(&extruded_deck(), 0, &RenderOpts { scale: 1.0, ..Default::default() });
|
||||
let n = |x0: u32, x1: u32, f: &dyn Fn([u8; 4]) -> bool| {
|
||||
let mut k = 0;
|
||||
for y in 0..img.height {
|
||||
for x in x0..x1.min(img.width) {
|
||||
let i = ((y * img.width + x) * 4) as usize;
|
||||
if img.pixels.get(i..i + 4).and_then(|p| <[u8; 4]>::try_from(p).ok()).is_some_and(f) {
|
||||
k += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
k
|
||||
};
|
||||
// The ellipse's yellow contour and the flat rectangle's green one.
|
||||
assert!(n(260, 480, &|p| p[0] > 200 && (150..230).contains(&p[1]) && p[2] < 80) > 200);
|
||||
assert!(n(740, 960, &|p| p[0] < 80 && p[1] > 120 && p[2] < 120) > 300);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extrusions_show_their_sides_under_a_turned_camera() {
|
||||
let p = extruded_deck();
|
||||
|
||||
+5
-4
@@ -255,10 +255,10 @@ Detail in [animation-parity.md](animation-parity.md). 10–16 h; the pane is a *
|
||||
- **Estimate:** 1 h (edit points: segment types, open/close path), 2–4 h (warp gallery, command and handles), 6–10 h (3-D extrusion,
|
||||
render as approximations and `scene3d`/`sp3d` round-trip as raw XML; extrusions draw their
|
||||
sides under turned cameras, each lit for its own normal; the preset light rigs, rig directions and
|
||||
materials shade bevels and sides approximately (fitted to PowerPoint for three-point/matte only); no contours,
|
||||
and no command or pane to apply 3-D).
|
||||
- **Estimate:** 4–6 h (edit points), 2–4 h (warp gallery, command and handles), 2–4 h (3-D contours and
|
||||
rig/material fidelity), plus the Format Shape page.
|
||||
materials shade bevels and sides approximately (fitted to PowerPoint for three-point/matte only); contours draw as a
|
||||
band round the face outline (not round the extruded silhouette); no command or pane to apply 3-D).
|
||||
- **Estimate:** 4–6 h (edit points), 2–4 h (warp gallery, command and handles), 1–2 h (fitting the other rigs and materials
|
||||
to PowerPoint renders), plus the Format Shape page.
|
||||
|
||||
### Proofing: spelling, thesaurus, AutoCorrect
|
||||
- **Evidence:** `review.spelling` checks a small built-in list of frequent misspellings
|
||||
@@ -355,6 +355,7 @@ Detail in [localization-parity.md](localization-parity.md).
|
||||
| 2026-10-14 | minor | Owned corpus `corpus_media.rs`: 3-D model parts and animation bounce end now round-trip. The real-deck corpus and #70/#74/#77 stay open |
|
||||
| 2026-10-14 | trivial | Corpus slice: themes, masters, effects and links; text Mouse Over actions round-trip |
|
||||
| 2026-10-14 | minor | Chart trendlines round-trip. The real-deck corpus gap stays open; secondary-axis export and drawing trendlines stay in the charts gap |
|
||||
| 2026-10-14 | trivial | 3-D contours |
|
||||
| 2026-10-14 | trivial | 3-D light rigs and materials; extrusion sides lit per side |
|
||||
| 2026-10-14 | trivial | 3-D extrusions draw their sides under turned cameras |
|
||||
| 2026-10-13 | minor | Shape, picture and table rendering checked against PowerPoint on ~150 synthetic edge-case slides; 3-D bevels, cameras and face lighting now render approximately (gap narrowed to extrusion, rigs and the 3-D pane) |
|
||||
|
||||
Reference in New Issue
Block a user