Rename SlideCraft to DeckCraft: crates, apps, binaries, app id, file extension (.deckcraft; .slidecraft files still open), packaging, docs; ROADMAP alpha and parity estimates

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-06 01:46:40 -04:00
co-authored by Claude Opus 5.5
parent 49278565ea
commit ae9d525bce
216 changed files with 2021 additions and 1998 deletions
+13 -13
View File
@@ -1,12 +1,12 @@
//! Charts: title, plot area with axes and gridlines, series marks, legend.
use deckcraft_color::{ColorTransform, Rgba, SchemeSlot};
use deckcraft_model::Shape;
use deckcraft_model::chart::{Chart, ChartType, nice_scale};
use deckcraft_model::resolve::Ctx;
use deckcraft_model::style::{ColorRef, Fill};
use deckcraft_model::text::{Align, Anchor, Bullet, TextBody};
use kurbo::{Affine, BezPath, Point, Rect, Shape as _};
use slidecraft_color::{ColorTransform, Rgba, SchemeSlot};
use slidecraft_model::Shape;
use slidecraft_model::chart::{Chart, ChartType, nice_scale};
use slidecraft_model::resolve::Ctx;
use slidecraft_model::style::{ColorRef, Fill};
use slidecraft_model::text::{Align, Anchor, Bullet, TextBody};
use vello_cpu::RenderContext;
use crate::color;
@@ -23,13 +23,13 @@ pub fn series_color(ctx: &Ctx, c: &Chart, i: usize) -> Rgba {
let round = if mono { i } else { i / 6 };
match round {
0 => base,
1 => slidecraft_color::apply(base, &[ColorTransform::Shade(60000)]),
2 => slidecraft_color::apply(base, &[ColorTransform::Tint(60000)]),
n => slidecraft_color::apply(base, &[ColorTransform::LumMod(100000 - (n as i32 % 5) * 12000)]),
1 => deckcraft_color::apply(base, &[ColorTransform::Shade(60000)]),
2 => deckcraft_color::apply(base, &[ColorTransform::Tint(60000)]),
n => deckcraft_color::apply(base, &[ColorTransform::LumMod(100000 - (n as i32 % 5) * 12000)]),
}
}
fn series_fill(ctx: &Ctx, c: &Chart, i: usize, s: Option<&slidecraft_model::chart::Series>) -> Rgba {
fn series_fill(ctx: &Ctx, c: &Chart, i: usize, s: Option<&deckcraft_model::chart::Series>) -> Rgba {
match s.and_then(|s| s.fill.as_ref()) {
Some(Fill::Solid { color: cc }) => ctx.color(cc, None),
_ => series_color(ctx, c, i),
@@ -56,7 +56,7 @@ fn label(ctx: &mut RenderContext, rctx: &Ctx, m: Affine, text: &str, r: Rect, si
}
}
let tmp = Shape { text: Some(body.clone()), ..Default::default() };
crate::draw_text(ctx, rctx, &tmp, &body, r, m, &slidecraft_text::NoFields, None);
crate::draw_text(ctx, rctx, &tmp, &body, r, m, &deckcraft_text::NoFields, None);
}
fn fmt_num(v: f64) -> String {
@@ -75,7 +75,7 @@ pub fn draw(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, w: f64, h
&Rect::new(0.0, 0.0, w, h).to_path(0.1),
Rect::new(0.0, 0.0, w, h),
m,
slidecraft_geom::preset::FillMode::Norm,
deckcraft_geom::preset::FillMode::Norm,
1.0,
rctx.pres,
);
@@ -173,7 +173,7 @@ pub fn draw(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, w: f64, h
Rect::new(area.x0 + gutter, area.y0, area.x1, area.y1 - base * 1.8)
};
if let Some(f) = &c.plot_fill {
crate::paint::fill_path(ctx, rctx, f, None, &plot.to_path(0.1), plot, m, slidecraft_geom::preset::FillMode::Norm, 1.0, rctx.pres);
crate::paint::fill_path(ctx, rctx, f, None, &plot.to_path(0.1), plot, m, deckcraft_geom::preset::FillMode::Norm, 1.0, rctx.pres);
}
let span = (max - min).max(1e-9);
let to_v = |v: f64| if horiz { plot.x0 + (v - min) / span * plot.width() } else { plot.y1 - (v - min) / span * plot.height() };
+1 -1
View File
@@ -4,7 +4,7 @@
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use slidecraft_model::style::PictureAdjust;
use deckcraft_model::style::PictureAdjust;
use vello_cpu::Pixmap;
const BUDGET: usize = 512 << 20;
+28 -28
View File
@@ -1,4 +1,4 @@
//! The SlideCraft renderer: draws slides, layouts and masters with `vello_cpu`.
//! The DeckCraft renderer: draws slides, layouts and masters with `vello_cpu`.
//!
//! One code path serves the editor canvas, thumbnails, image/PDF-raster export and the slide show:
//! [`render_slide`] draws the background, the master and layout graphics, then the slide's shapes
@@ -16,15 +16,15 @@ mod table;
use std::collections::HashMap;
use std::sync::Arc;
use deckcraft_color::Rgba;
use deckcraft_fonts::FontDb;
use deckcraft_geom::preset::{self, FillMode, Geometry, SubPath};
use deckcraft_model::resolve::{self, Ctx};
use deckcraft_model::style::{Effects, Fill, Line};
use deckcraft_model::text::TextBody;
use deckcraft_model::{Geom, PhType, Presentation, Shape, ShapeId, ShapeKind, Slide};
use deckcraft_text::{Fields, Opts};
use kurbo::{Affine, BezPath, PathEl, Point, Rect, Shape as _, Vec2};
use slidecraft_color::Rgba;
use slidecraft_fonts::FontDb;
use slidecraft_geom::preset::{self, FillMode, Geometry, SubPath};
use slidecraft_model::resolve::{self, Ctx};
use slidecraft_model::style::{Effects, Fill, Line};
use slidecraft_model::text::TextBody;
use slidecraft_model::{Geom, PhType, Presentation, Shape, ShapeId, ShapeKind, Slide};
use slidecraft_text::{Fields, Opts};
use vello_common::filter_effects::{EdgeMode, Filter, FilterPrimitive};
use vello_cpu::{RenderContext, Resources, peniko};
@@ -208,7 +208,7 @@ pub fn needs_filters(pres: &Presentation, slide: &Slide) -> bool {
let Some(rctx) = Ctx::for_slide(pres, slide) else { return false };
let mut any = false;
let mut check = |shapes: &[Shape]| {
slidecraft_model::walk(shapes, &mut |s, _| {
deckcraft_model::walk(shapes, &mut |s, _| {
if any {
return;
}
@@ -498,7 +498,7 @@ impl Renderer {
match &s.kind {
ShapeKind::Group { children, child } => {
let ch = Rect::new(child.x, child.y, child.x + child.w, child.y + child.h);
let gm = parent * slidecraft_geom::group_child_affine(&x, ch);
let gm = parent * deckcraft_geom::group_child_affine(&x, ch);
for c in children {
self.shape(ctx, f, rctx, c, gm, depth + 1);
}
@@ -537,7 +537,7 @@ impl Renderer {
let outline = geo.outline();
// Placeholders in the editor: dashed outline and prompt, nothing else when empty.
let empty_ph = s.ph.is_some() && s.text.as_ref().is_none_or(TextBody::is_empty) && !is_pic && !matches!(s.kind, ShapeKind::Media(_));
let fill = if let Some(t) = st.tint { Some(Fill::solid(slidecraft_model::ColorRef::rgb(t))) } else { fill };
let fill = if let Some(t) = st.tint { Some(Fill::solid(deckcraft_model::ColorRef::rgb(t))) } else { fill };
// Effects below the shape.
if let Some(e) = &effects {
self.effects_below(ctx, rctx, e, fx_ph, &geo, &fill, m, w, h);
@@ -561,7 +561,7 @@ impl Renderer {
ctx.set_transform(m);
let bounds = Rect::new(0.0, 0.0, w, h);
if let Some(p) = mc.poster {
let pf = slidecraft_model::style::PictureFill { media: p, ..Default::default() };
let pf = deckcraft_model::style::PictureFill { media: p, ..Default::default() };
paint::picture(ctx, f.pres, &pf, bounds, m);
} else if mc.video {
ctx.set_paint(peniko::Color::from_rgba8(20, 20, 24, 255));
@@ -640,7 +640,7 @@ impl Renderer {
{
let tr = geo.text_rect;
if !body.is_empty() {
let l = slidecraft_text::layout(rctx, s, body, &Opts { rect: tr, fields: &f.fields, prompt_color: None, no_shrink: false });
let l = deckcraft_text::layout(rctx, s, body, &Opts { rect: tr, fields: &f.fields, prompt_color: None, no_shrink: false });
draw_layout_with(ctx, &l, tr, m, &st.paras);
} else if f.opts.edit && s.ph.is_some() {
let kind = s.ph_type().unwrap_or(PhType::Body);
@@ -747,7 +747,7 @@ fn blur(sigma: f64) -> Filter {
}
fn stroke(ctx: &mut RenderContext, rctx: &Ctx, line: &Line, ph: Option<Rgba>, path: &BezPath, m: Affine, lw: f64) {
use slidecraft_model::style::{LineCap, LineJoin};
use deckcraft_model::style::{LineCap, LineJoin};
let cap = match line.cap.unwrap_or_default() {
LineCap::Flat => kurbo::Cap::Butt,
LineCap::Round => kurbo::Cap::Round,
@@ -896,10 +896,10 @@ fn prompt_body(rctx: &Ctx, s: &Shape, kind: PhType) -> TextBody {
}
let mut t = TextBody::from_text(txt);
if matches!(kind, PhType::Picture | PhType::Chart | PhType::Table | PhType::Media | PhType::Diagram | PhType::ClipArt) {
t.body.anchor = Some(slidecraft_model::text::Anchor::Bottom);
t.body.anchor = Some(deckcraft_model::text::Anchor::Bottom);
if let Some(p) = t.paragraphs.first_mut() {
p.props.align = Some(slidecraft_model::text::Align::Center);
p.props.bullet = Some(slidecraft_model::text::Bullet::None);
p.props.align = Some(deckcraft_model::text::Align::Center);
p.props.bullet = Some(deckcraft_model::text::Bullet::None);
p.props.margin_left = Some(0.0);
p.props.indent = Some(0.0);
for r in &mut p.runs {
@@ -914,7 +914,7 @@ thread_local! {
static GLYPH_CACHE: std::cell::RefCell<HashMap<(u32, u32), Arc<BezPath>>> = std::cell::RefCell::new(HashMap::new());
}
fn glyph(face: &slidecraft_fonts::FontFace, gid: u32) -> Arc<BezPath> {
fn glyph(face: &deckcraft_fonts::FontFace, gid: u32) -> Arc<BezPath> {
let key = (face.id(), gid);
if let Some(p) = GLYPH_CACHE.with(|c| c.borrow().get(&key).cloned()) {
return p;
@@ -932,17 +932,17 @@ fn glyph(face: &slidecraft_fonts::FontFace, gid: u32) -> Arc<BezPath> {
/// Draw a text body into shape space `m` within the text rect.
pub fn draw_text(ctx: &mut RenderContext, rctx: &Ctx, s: &Shape, body: &TextBody, tr: Rect, m: Affine, fields: &dyn Fields, prompt: Option<Rgba>) {
let l = slidecraft_text::layout(rctx, s, body, &Opts { rect: tr, fields, prompt_color: prompt, no_shrink: false });
let l = deckcraft_text::layout(rctx, s, body, &Opts { rect: tr, fields, prompt_color: prompt, no_shrink: false });
draw_layout(ctx, &l, tr, m);
}
/// Draw an already laid-out text block.
pub fn draw_layout(ctx: &mut RenderContext, l: &slidecraft_text::TextLayout, tr: Rect, m: Affine) {
pub fn draw_layout(ctx: &mut RenderContext, l: &deckcraft_text::TextLayout, tr: Rect, m: Affine) {
draw_layout_with(ctx, l, tr, m, &[]);
}
/// Draw a laid-out text block with per-paragraph animation states.
pub fn draw_layout_with(ctx: &mut RenderContext, l: &slidecraft_text::TextLayout, tr: Rect, m: Affine, paras: &[(usize, ParaState)]) {
pub fn draw_layout_with(ctx: &mut RenderContext, l: &deckcraft_text::TextLayout, tr: Rect, m: Affine, paras: &[(usize, ParaState)]) {
let para = |i: usize| paras.iter().find(|(p, _)| *p == i).map(|(_, s)| *s).unwrap_or_default();
let m = if l.rotation != 0.0 {
let c = tr.center().to_vec2();
@@ -950,7 +950,7 @@ pub fn draw_layout_with(ctx: &mut RenderContext, l: &slidecraft_text::TextLayout
} else {
m
};
let deco = |ctx: &mut RenderContext, d: &slidecraft_text::Deco| {
let deco = |ctx: &mut RenderContext, d: &deckcraft_text::Deco| {
let ps = para(d.para);
if !ps.visible || ps.opacity <= 0.001 {
return;
@@ -1031,15 +1031,15 @@ mod mt_tests {
/// Regression: multithreaded rendering of a slide with shadows panicked inside vello_cpu.
#[test]
fn effects_render_with_threads_requested() {
let mut s = slidecraft_model::Presentation::default();
let mut s = deckcraft_model::Presentation::default();
let sl = std::sync::Arc::make_mut(&mut s.slides[0]);
let mut sh = slidecraft_model::Shape {
let mut sh = deckcraft_model::Shape {
id: ShapeId(900),
xfrm: Some(slidecraft_geom::Xfrm::new(10.0, 10.0, 100.0, 100.0)),
style: Some(slidecraft_model::ShapeStyle::accent(slidecraft_color::SchemeSlot::Accent1)),
xfrm: Some(deckcraft_geom::Xfrm::new(10.0, 10.0, 100.0, 100.0)),
style: Some(deckcraft_model::ShapeStyle::accent(deckcraft_color::SchemeSlot::Accent1)),
..Default::default()
};
sh.effects = Some(slidecraft_model::Effects { soft_edge: Some(5.0), ..Default::default() });
sh.effects = Some(deckcraft_model::Effects { soft_edge: Some(5.0), ..Default::default() });
sl.shapes.push(sh);
assert!(needs_filters(&s, &s.slides[0]));
let img = RENDERER.with(|r| r.borrow_mut().slide(&s, &s.slides[0], 0, &RenderOpts { scale: 0.5, threads: 4, ..Default::default() }));
+5 -5
View File
@@ -1,11 +1,11 @@
//! Fills: solid, gradient, picture and pattern paints.
use deckcraft_color::Rgba;
use deckcraft_geom::preset::FillMode;
use deckcraft_model::resolve::Ctx;
use deckcraft_model::style::{Fill, GradientShape, PictureFill, PictureMode};
use deckcraft_model::{PhType, Presentation};
use kurbo::{Affine, BezPath, Point, Rect, Shape as _};
use slidecraft_color::Rgba;
use slidecraft_geom::preset::FillMode;
use slidecraft_model::resolve::Ctx;
use slidecraft_model::style::{Fill, GradientShape, PictureFill, PictureMode};
use slidecraft_model::{PhType, Presentation};
use vello_cpu::{RenderContext, peniko};
use crate::{color, images, shade};
+6 -6
View File
@@ -1,11 +1,11 @@
//! Laid-out text and click areas of a slide, in slide coordinates, without drawing — for exports
//! that put real text over a raster (PDF text layer, links) and for text extraction.
use deckcraft_model::resolve::{self, Ctx};
use deckcraft_model::text::{Hyperlink, TextBody};
use deckcraft_model::{Presentation, Shape, ShapeKind};
use deckcraft_text::{Opts, TextLayout};
use kurbo::{Affine, Rect};
use slidecraft_model::resolve::{self, Ctx};
use slidecraft_model::text::{Hyperlink, TextBody};
use slidecraft_model::{Presentation, Shape, ShapeKind};
use slidecraft_text::{Opts, TextLayout};
use crate::{SlideFields, shape_geometry};
@@ -71,7 +71,7 @@ fn place(out: &mut Placed, rctx: &Ctx, fields: &SlideFields, s: &Shape, parent:
match &s.kind {
ShapeKind::Group { children, child } => {
let ch = Rect::new(child.x, child.y, child.x + child.w, child.y + child.h);
let gm = parent * slidecraft_geom::group_child_affine(&x, ch);
let gm = parent * deckcraft_geom::group_child_affine(&x, ch);
for c in children {
place(out, rctx, fields, c, gm, depth + 1);
}
@@ -80,7 +80,7 @@ fn place(out: &mut Placed, rctx: &Ctx, fields: &SlideFields, s: &Shape, parent:
_ => {
let Some(body) = s.text.as_ref().filter(|b| !b.is_empty()) else { return };
let tr = shape_geometry(s, x.w, x.h).text_rect;
let layout = slidecraft_text::layout(rctx, s, body, &Opts { rect: tr, fields, prompt_color: None, no_shrink: false });
let layout = deckcraft_text::layout(rctx, s, body, &Opts { rect: tr, fields, prompt_color: None, no_shrink: false });
let m = if layout.rotation != 0.0 {
let c = tr.center().to_vec2();
m * Affine::translate(c) * Affine::rotate(layout.rotation.to_radians()) * Affine::translate(-c)
+11 -11
View File
@@ -1,12 +1,12 @@
//! Tables: cell fills from the table style (or overrides), borders, and cell text.
use deckcraft_color::{ColorTransform, Rgba, SchemeSlot};
use deckcraft_model::resolve::Ctx;
use deckcraft_model::style::{ColorRef, Fill};
use deckcraft_model::table::Table;
use deckcraft_model::text::Anchor;
use deckcraft_model::{Presentation, Shape};
use kurbo::{Affine, BezPath, Rect, Shape as _};
use slidecraft_color::{ColorTransform, Rgba, SchemeSlot};
use slidecraft_model::resolve::Ctx;
use slidecraft_model::style::{ColorRef, Fill};
use slidecraft_model::table::Table;
use slidecraft_model::text::Anchor;
use slidecraft_model::{Presentation, Shape};
use vello_cpu::{RenderContext, peniko};
use crate::color;
@@ -42,7 +42,7 @@ pub fn look(ctx: &Ctx, t: &Table, r: usize, c: usize) -> CellLook {
let body_r = if t.first_row { r.wrapping_sub(1) } else { r };
let band = t.band_row && !header && !total && body_r % 2 == 0;
let band_c = t.band_col && c.is_multiple_of(2);
let tint = |k: i32| slidecraft_color::apply(a, &[ColorTransform::Tint(k)]);
let tint = |k: i32| deckcraft_color::apply(a, &[ColorTransform::Tint(k)]);
let white = Rgba::WHITE;
let dark_text = ctx.color(&ColorRef::scheme(SchemeSlot::Tx1), None);
match kind {
@@ -64,9 +64,9 @@ pub fn look(ctx: &Ctx, t: &Table, r: usize, c: usize) -> CellLook {
fill: Some(if header {
Rgba::BLACK
} else if band || band_c {
slidecraft_color::apply(a, &[ColorTransform::Shade(60000)])
deckcraft_color::apply(a, &[ColorTransform::Shade(60000)])
} else {
slidecraft_color::apply(a, &[ColorTransform::Shade(75000)])
deckcraft_color::apply(a, &[ColorTransform::Shade(75000)])
}),
text: Some(white),
bold: header || total || first_col,
@@ -123,7 +123,7 @@ pub fn cell_rects(t: &Table) -> Vec<(usize, usize, Rect)> {
out
}
pub fn draw(ctx: &mut RenderContext, pres: &Presentation, rctx: &Ctx, t: &Table, m: Affine, _w: f64, _h: f64, fields: &dyn slidecraft_text::Fields) {
pub fn draw(ctx: &mut RenderContext, pres: &Presentation, rctx: &Ctx, t: &Table, m: Affine, _w: f64, _h: f64, fields: &dyn deckcraft_text::Fields) {
let _ = pres;
let rects = cell_rects(t);
for (r, c, rect) in &rects {
@@ -132,7 +132,7 @@ pub fn draw(ctx: &mut RenderContext, pres: &Presentation, rctx: &Ctx, t: &Table,
ctx.set_transform(m);
match &cell.fill {
Some(f) => {
crate::paint::fill_path(ctx, rctx, f, None, &rect.to_path(0.1), *rect, m, slidecraft_geom::preset::FillMode::Norm, 1.0, rctx.pres)
crate::paint::fill_path(ctx, rctx, f, None, &rect.to_path(0.1), *rect, m, deckcraft_geom::preset::FillMode::Norm, 1.0, rctx.pres)
}
None => {
if let Some(fc) = lk.fill {
+13 -13
View File
@@ -1,8 +1,8 @@
use super::*;
use slidecraft_model::chart::ChartType;
use slidecraft_model::style::ColorRef;
use slidecraft_model::text::TextBody;
use slidecraft_model::{Chart, Geom, Presentation, Shape, ShapeId, ShapeKind, ShapeStyle, Table, Xfrm};
use deckcraft_model::chart::ChartType;
use deckcraft_model::style::ColorRef;
use deckcraft_model::text::TextBody;
use deckcraft_model::{Chart, Geom, Presentation, Shape, ShapeId, ShapeKind, ShapeStyle, Table, Xfrm};
fn deck_with(shapes: Vec<Shape>) -> Presentation {
let mut p = Presentation::default();
@@ -24,12 +24,12 @@ fn shape_fill_uses_theme_accent() {
let sh = Shape {
id: ShapeId(500),
xfrm: Some(Xfrm::new(100.0, 100.0, 200.0, 100.0)),
style: Some(ShapeStyle::accent(slidecraft_color::SchemeSlot::Accent1)),
style: Some(ShapeStyle::accent(deckcraft_color::SchemeSlot::Accent1)),
..Default::default()
};
let p = deck_with(vec![sh]);
let img = render_slide(&p, 0, &RenderOpts { scale: 1.0, ..Default::default() });
let accent = p.masters[0].theme.colors.get(slidecraft_color::SchemeSlot::Accent1);
let accent = p.masters[0].theme.colors.get(deckcraft_color::SchemeSlot::Accent1);
let px = img.pixel(200, 150);
assert!((px[0] as i32 - accent.r as i32).abs() <= 2 && (px[2] as i32 - accent.b as i32).abs() <= 2, "{px:?} vs {accent:?}");
assert_eq!(img.pixel(50, 50), [255, 255, 255, 255]);
@@ -49,14 +49,14 @@ fn text_draws_ink() {
#[test]
fn every_preset_renders_without_panic() {
let shapes: Vec<Shape> = slidecraft_geom::preset::CATALOG
let shapes: Vec<Shape> = deckcraft_geom::preset::CATALOG
.iter()
.enumerate()
.map(|(i, pr)| Shape {
id: ShapeId(1000 + i as u32),
xfrm: Some(Xfrm::new((i % 16) as f64 * 60.0, (i / 16) as f64 * 55.0, 50.0, 45.0)),
geom: Geom::preset(pr.name),
style: Some(ShapeStyle::accent(slidecraft_color::SchemeSlot::Accent2)),
style: Some(ShapeStyle::accent(deckcraft_color::SchemeSlot::Accent2)),
..Default::default()
})
.collect();
@@ -70,12 +70,12 @@ fn effects_tables_charts_render() {
let mut sh = Shape {
id: ShapeId(600),
xfrm: Some(Xfrm::new(50.0, 50.0, 200.0, 100.0)),
style: Some(ShapeStyle::accent(slidecraft_color::SchemeSlot::Accent1)),
style: Some(ShapeStyle::accent(deckcraft_color::SchemeSlot::Accent1)),
..Default::default()
};
sh.effects = Some(slidecraft_model::Effects {
outer_shadow: Some(slidecraft_model::style::Shadow {
color: ColorRef::rgb(slidecraft_color::Rgba::BLACK),
sh.effects = Some(deckcraft_model::Effects {
outer_shadow: Some(deckcraft_model::style::Shadow {
color: ColorRef::rgb(deckcraft_color::Rgba::BLACK),
blur: 8.0,
dist: 6.0,
dir: 45.0,
@@ -87,7 +87,7 @@ fn effects_tables_charts_render() {
align: String::new(),
rotate_with_shape: false,
}),
glow: Some(slidecraft_model::style::Glow { color: ColorRef::rgb(slidecraft_color::Rgba::rgb(255, 200, 0)), radius: 6.0 }),
glow: Some(deckcraft_model::style::Glow { color: ColorRef::rgb(deckcraft_color::Rgba::rgb(255, 200, 0)), radius: 6.0 }),
soft_edge: Some(4.0),
..Default::default()
});