chore: resolve SonarCloud maintainability issues (#1255)

* chore: resolve SonarCloud maintainability issues

* fix: address shellcheck SC2059 and fix generic cast ambiguity

* style: apply rustfmt formatting

* fix: restore compile and behaviour in Sonar refactors

- fonts: char::is_alphanumeric takes &self, so filter needs a closure
- h264: keep the mvd accumulator as i32 (prefix is usize)
- model: annotate the non-stacked range accumulators as f64
- engine: chart.options still clears the raw chart XML for upDownBars
  and quartiles; smartart goes back to the glob import (cmd! macro)
- web: keep the original viewport meta (behaviour change)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Assign positional parameters to locals in sha256 verification

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
This commit is contained in:
Dylan M. Taylor
2026-10-23 23:28:09 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 Dylan Taylor
parent b913c4f9d0
commit 5fadda5ecf
28 changed files with 364 additions and 293 deletions
+1 -1
View File
@@ -49,7 +49,7 @@ fn tokenize(s: &str) -> Vec<Tok> {
n1.is_ascii_digit() || ((n1 == '-' || n1 == '+') && n2.is_ascii_digit()) n1.is_ascii_digit() || ((n1 == '-' || n1 == '+') && n2.is_ascii_digit())
} { } {
i += 2; i += 2;
while cs.get(i).is_some_and(|d| d.is_ascii_digit()) { while cs.get(i).is_some_and(char::is_ascii_digit) {
i += 1; i += 1;
} }
break; break;
+2 -4
View File
@@ -2222,18 +2222,16 @@ fn neg_deinterleave(diff: i32, r: i32, max: i32) -> i32 {
if diff <= 2 * r { if diff <= 2 * r {
if diff & 1 == 1 { if diff & 1 == 1 {
return r + ((diff + 1) >> 1); return r + ((diff + 1) >> 1);
} else {
return r - (diff >> 1);
} }
return r - (diff >> 1);
} }
diff diff
} else { } else {
if diff <= 2 * (max - r - 1) { if diff <= 2 * (max - r - 1) {
if diff & 1 == 1 { if diff & 1 == 1 {
return r + ((diff + 1) >> 1); return r + ((diff + 1) >> 1);
} else {
return r - (diff >> 1);
} }
return r - (diff >> 1);
} }
max - (diff + 1) max - (diff + 1)
} }
+35 -27
View File
@@ -1761,6 +1761,39 @@ fn chart_data(s: &mut Session, p: &Value) -> Result<Value> {
}) })
} }
fn apply_bin_options(c: &mut deckcraft_model::chart::Chart, bin_count: Option<Option<u32>>, bin_size: Option<Option<f64>>) {
if let Some(n) = bin_count {
c.bin_count = n;
if n.is_some() {
c.bin_size = None;
}
}
if let Some(w) = bin_size {
c.bin_size = w;
if w.is_some() {
c.bin_count = None;
}
}
}
fn apply_subtotals_and_bars(c: &mut deckcraft_model::chart::Chart, p: &Value, subtotals: Option<&[usize]>) {
if let Some(t) = subtotals {
let n = c.series.first().map_or(0, |s| s.values.len());
let mut t: Vec<usize> = t.iter().copied().filter(|i| *i < n).collect();
t.sort_unstable();
t.dedup();
c.subtotals = t;
}
if let Some(b) = p.get("upDownBars").and_then(Value::as_bool) {
c.up_down_bars = b;
c.raw = None;
}
if let Some(q) = p.get("quartiles").and_then(Value::as_str) {
c.inclusive_quartiles = q.eq_ignore_ascii_case("inclusive");
c.raw = None;
}
}
fn chart_options(s: &mut Session, p: &Value) -> Result<Value> { fn chart_options(s: &mut Session, p: &Value) -> Result<Value> {
// Histogram bins: a count or a width (setting one clears the other; null for automatic). // Histogram bins: a count or a width (setting one clears the other; null for automatic).
let bin_count = p.get("binCount").map(|v| v.as_u64().map(|n| n.clamp(1, deckcraft_model::chart::MAX_BINS as u64) as u32)); let bin_count = p.get("binCount").map(|v| v.as_u64().map(|n| n.clamp(1, deckcraft_model::chart::MAX_BINS as u64) as u32));
@@ -1772,33 +1805,8 @@ fn chart_options(s: &mut Session, p: &Value) -> Result<Value> {
let subtotals: Option<Vec<usize>> = let subtotals: Option<Vec<usize>> =
p.get("subtotals").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_u64).map(|i| i as usize).collect()); p.get("subtotals").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_u64).map(|i| i as usize).collect());
with_chart(s, p, "chart.options", |c| { with_chart(s, p, "chart.options", |c| {
if let Some(n) = bin_count { apply_bin_options(c, bin_count, bin_size);
c.bin_count = n; apply_subtotals_and_bars(c, p, subtotals.as_deref());
if n.is_some() {
c.bin_size = None;
}
}
if let Some(w) = bin_size {
c.bin_size = w;
if w.is_some() {
c.bin_count = None;
}
}
if let Some(t) = &subtotals {
let n = c.series.first().map_or(0, |s| s.values.len());
let mut t: Vec<usize> = t.iter().copied().filter(|i| *i < n).collect();
t.sort_unstable();
t.dedup();
c.subtotals = t;
}
if let Some(b) = p.get("upDownBars").and_then(Value::as_bool) {
c.up_down_bars = b;
c.raw = None;
}
if let Some(q) = p.get("quartiles").and_then(Value::as_str) {
c.inclusive_quartiles = q.eq_ignore_ascii_case("inclusive");
c.raw = None;
}
if bin_count.is_some() || bin_size.is_some() || subtotals.is_some() { if bin_count.is_some() || bin_size.is_some() || subtotals.is_some() {
c.raw = None; c.raw = None;
} }
+2 -2
View File
@@ -539,9 +539,9 @@ fn section_add(s: &mut Session, p: &Value) -> Result<Value> {
if doc.sections.is_empty() { if doc.sections.is_empty() {
// The slides before `at` go into a "Default Section". // The slides before `at` go into a "Default Section".
if at > 0 { if at > 0 {
doc.sections.push(Section { name: "Default Section".into(), slides: ids.get(..at).map(|x| x.to_vec()).unwrap_or_default() }); doc.sections.push(Section { name: "Default Section".into(), slides: ids.get(..at).map(<[_]>::to_vec).unwrap_or_default() });
} }
doc.sections.push(Section { name, slides: ids.get(at..).map(|x| x.to_vec()).unwrap_or_default() }); doc.sections.push(Section { name, slides: ids.get(at..).map(<[_]>::to_vec).unwrap_or_default() });
} else { } else {
let Some(start) = ids.get(at).copied() else { return Err(bad("section.add", "no slide")) }; let Some(start) = ids.get(at).copied() else { return Err(bad("section.add", "no slide")) };
let si = doc.section_of(start).unwrap_or(0); let si = doc.section_of(start).unwrap_or(0);
+1 -1
View File
@@ -13,7 +13,7 @@ use crate::{EngineError, Result, Session};
pub fn panic_message(payload: &(dyn std::any::Any + Send)) -> String { pub fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
payload payload
.downcast_ref::<&str>() .downcast_ref::<&str>()
.map(|s| s.to_string()) .map(ToString::to_string)
.or_else(|| payload.downcast_ref::<String>().cloned()) .or_else(|| payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "unknown panic".into()) .unwrap_or_else(|| "unknown panic".into())
} }
+1 -1
View File
@@ -60,7 +60,7 @@ pub fn stroke_width(icon: &Icon, w: f64, h: f64) -> f64 {
} }
fn parsed() -> &'static HashMap<&'static str, BezPath> { fn parsed() -> &'static HashMap<&'static str, BezPath> {
static CACHE: OnceLock<HashMap<&'static str, BezPath>> = OnceLock::new(); static CACHE: OnceLock<HashMap<&str, BezPath>> = OnceLock::new();
CACHE.get_or_init(|| { CACHE.get_or_init(|| {
let mut m = HashMap::new(); let mut m = HashMap::new();
for icon in ICONS { for icon in ICONS {
+2 -2
View File
@@ -152,13 +152,13 @@ pub fn list(dir: &Path) -> Vec<(u64, Value)> {
let Ok(rd) = std::fs::read_dir(dir) else { return out }; let Ok(rd) = std::fs::read_dir(dir) else { return out };
for e in rd.flatten() { for e in rd.flatten() {
let p = e.path(); let p = e.path();
if p.extension().and_then(|x| x.to_str()) != Some("deckcraft") { if p.extension().and_then(std::ffi::OsStr::to_str) != Some("deckcraft") {
continue; continue;
} }
if !std::fs::metadata(&p).is_ok_and(|m| m.is_file()) { if !std::fs::metadata(&p).is_ok_and(|m| m.is_file()) {
continue; continue;
} }
let Some(uid) = p.file_stem().and_then(|s| s.to_str()).and_then(|s| s.parse::<u64>().ok()) else { continue }; let Some(uid) = p.file_stem().and_then(std::ffi::OsStr::to_str).and_then(|s| s.parse::<u64>().ok()) else { continue };
// Metadata is a file on disk: anything but an object (corrupt, hand-edited) counts as none. // Metadata is a file on disk: anything but an object (corrupt, hand-edited) counts as none.
let meta = read_regular(&p.with_extension("json"), MAX_META_BYTES) let meta = read_regular(&p.with_extension("json"), MAX_META_BYTES)
.and_then(|t| serde_json::from_slice::<Value>(&t).ok()) .and_then(|t| serde_json::from_slice::<Value>(&t).ok())
+28 -27
View File
@@ -161,12 +161,12 @@ pub struct FaceRef(&'static FontFace);
impl FaceRef { impl FaceRef {
/// The handle for a loaded face (memoised per face). /// The handle for a loaded face (memoised per face).
pub fn of(face: &Arc<FontFace>) -> FaceRef { pub fn of(face: &Arc<FontFace>) -> FaceRef {
static LEAKED: RwLock<Vec<Option<&'static FontFace>>> = RwLock::new(Vec::new()); static LEAKED: RwLock<Vec<Option<&FontFace>>> = RwLock::new(Vec::new());
let id = face.id as usize; let id = face.id as usize;
if let Some(Some(f)) = LEAKED.read().unwrap_or_else(|e| e.into_inner()).get(id) { if let Some(Some(f)) = LEAKED.read().unwrap_or_else(std::sync::PoisonError::into_inner).get(id) {
return FaceRef(f); return FaceRef(f);
} }
let mut w = LEAKED.write().unwrap_or_else(|e| e.into_inner()); let mut w = LEAKED.write().unwrap_or_else(std::sync::PoisonError::into_inner);
if w.len() <= id { if w.len() <= id {
w.resize(id + 1, None); w.resize(id + 1, None);
} }
@@ -743,7 +743,7 @@ pub fn base_style(style: &str) -> &str {
} }
fn norm(s: &str) -> String { fn norm(s: &str) -> String {
s.chars().filter(|c| c.is_alphanumeric()).flat_map(|c| c.to_lowercase()).collect() s.chars().filter(|c| c.is_alphanumeric()).flat_map(char::to_lowercase).collect()
} }
/// Weight implied by a style name. /// Weight implied by a style name.
@@ -807,7 +807,7 @@ impl FontDb {
pub fn set_system_fallback(&self, on: bool) { pub fn set_system_fallback(&self, on: bool) {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
{ {
self.sys.lock().unwrap_or_else(|e| e.into_inner()).enabled = on; self.sys.lock().unwrap_or_else(std::sync::PoisonError::into_inner).enabled = on;
} }
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
let _ = on; let _ = on;
@@ -822,14 +822,14 @@ impl FontDb {
} }
pub(crate) fn read_faces(&self) -> std::sync::RwLockReadGuard<'_, Vec<Arc<FontFace>>> { pub(crate) fn read_faces(&self) -> std::sync::RwLockReadGuard<'_, Vec<Arc<FontFace>>> {
self.faces.read().unwrap_or_else(|e| e.into_inner()) self.faces.read().unwrap_or_else(std::sync::PoisonError::into_inner)
} }
/// The system font catalog, scanned first if it hasn't been yet. /// The system font catalog, scanned first if it hasn't been yet.
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
fn read_catalog(&self) -> std::sync::RwLockReadGuard<'_, Vec<CatalogEntry>> { fn read_catalog(&self) -> std::sync::RwLockReadGuard<'_, Vec<CatalogEntry>> {
self.ensure_catalog(); self.ensure_catalog();
self.catalog.read().unwrap_or_else(|e| e.into_inner()) self.catalog.read().unwrap_or_else(std::sync::PoisonError::into_inner)
} }
/// Family names available (loaded plus cataloged system fonts), sorted and deduplicated. /// Family names available (loaded plus cataloged system fonts), sorted and deduplicated.
@@ -885,7 +885,7 @@ impl FontDb {
} }
} }
} }
let mut faces = self.faces.write().unwrap_or_else(|e| e.into_inner()); let mut faces = self.faces.write().unwrap_or_else(std::sync::PoisonError::into_inner);
let mut added = 0; let mut added = 0;
for f in new { for f in new {
// Checked again under the lock: another thread may have added the same face. // Checked again under the lock: another thread may have added the same face.
@@ -923,7 +923,7 @@ impl FontDb {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pub fn load_system_fonts(&self) -> usize { pub fn load_system_fonts(&self) -> usize {
// Families load again from the new catalog when next looked up. // Families load again from the new catalog when next looked up.
self.loaded_families.lock().unwrap_or_else(|e| e.into_inner()).clear(); self.loaded_families.lock().unwrap_or_else(std::sync::PoisonError::into_inner).clear();
// The first scan, or a rescan once it is done (never both at once). // The first scan, or a rescan once it is done (never both at once).
let mut first = None; let mut first = None;
self.cataloged.get_or_init(|| first = Some(self.scan_font_dirs())); self.cataloged.get_or_init(|| first = Some(self.scan_font_dirs()));
@@ -959,7 +959,7 @@ impl FontDb {
} }
continue; continue;
} }
let ext = p.extension().and_then(|e| e.to_str()).map(|e| e.to_ascii_lowercase()); let ext = p.extension().and_then(std::ffi::OsStr::to_str).map(str::to_ascii_lowercase);
if !matches!(ext.as_deref(), Some("ttf" | "otf" | "ttc" | "otc")) { if !matches!(ext.as_deref(), Some("ttf" | "otf" | "ttc" | "otc")) {
continue; continue;
} }
@@ -975,7 +975,7 @@ impl FontDb {
} }
let n = found.len(); let n = found.len();
log::debug!("cataloged {n} system font faces"); log::debug!("cataloged {n} system font faces");
*self.catalog.write().unwrap_or_else(|e| e.into_inner()) = found; *self.catalog.write().unwrap_or_else(std::sync::PoisonError::into_inner) = found;
n n
} }
@@ -983,7 +983,7 @@ impl FontDb {
/// it is loaded. /// it is loaded.
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
fn load_cataloged(&self, family: &str) -> bool { fn load_cataloged(&self, family: &str) -> bool {
let loading = self.loading.lock().unwrap_or_else(|e| e.into_inner()); let loading = self.loading.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
self.load_family(family, &loading) self.load_family(family, &loading)
} }
@@ -1047,7 +1047,7 @@ impl FontDb {
// them rather than finding none and falling back to another family. // them rather than finding none and falling back to another family.
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
{ {
let mut loaded = self.loaded_families.lock().unwrap_or_else(|e| e.into_inner()); let mut loaded = self.loaded_families.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let key = family.to_lowercase(); let key = family.to_lowercase();
if !loaded.contains(&key) { if !loaded.contains(&key) {
self.load_cataloged(family); self.load_cataloged(family);
@@ -1112,7 +1112,7 @@ impl FontDb {
} }
let f = make_face(base.bytes.clone(), base.index, base.family.clone(), style.to_string(), coords)?; let f = make_face(base.bytes.clone(), base.index, base.family.clone(), style.to_string(), coords)?;
let f = Arc::new(f); let f = Arc::new(f);
self.faces.write().unwrap_or_else(|e| e.into_inner()).push(f.clone()); self.faces.write().unwrap_or_else(std::sync::PoisonError::into_inner).push(f.clone());
Some(f) Some(f)
} }
@@ -1214,20 +1214,20 @@ impl FontDb {
} }
let seen = self.read_faces().len(); let seen = self.read_faces().len();
{ {
let sys = self.sys.lock().unwrap_or_else(|e| e.into_inner()); let sys = self.sys.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
if !sys.enabled || sys.misses.contains(&c) { if !sys.enabled || sys.misses.contains(&c) {
return false; return false;
} }
} }
let covered = |db: &FontDb| db.read_faces().iter().any(|f| f.covers(c) && accept(f)); let covered = |db: &FontDb| db.read_faces().iter().any(|f| f.covers(c) && accept(f));
let cataloged: Vec<String> = self.read_catalog().iter().map(|e| e.family.clone()).collect(); let cataloged: Vec<String> = self.read_catalog().iter().map(|e| e.family.clone()).collect();
let loading = self.loading.lock().unwrap_or_else(|e| e.into_inner()); let loading = self.loading.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
// Another thread may have loaded a face for `c` (faces are only ever added), or found // Another thread may have loaded a face for `c` (faces are only ever added), or found
// there is none, while this one waited. // there is none, while this one waited.
if self.read_faces().len() != seen && covered(self) { if self.read_faces().len() != seen && covered(self) {
return true; return true;
} }
if self.sys.lock().unwrap_or_else(|e| e.into_inner()).misses.contains(&c) { if self.sys.lock().unwrap_or_else(std::sync::PoisonError::into_inner).misses.contains(&c) {
return false; return false;
} }
for fam in SYSTEM_FALLBACKS { for fam in SYSTEM_FALLBACKS {
@@ -1238,7 +1238,8 @@ impl FontDb {
} }
} }
} }
let mut paths: Vec<std::path::PathBuf> = self.catalog.read().unwrap_or_else(|e| e.into_inner()).iter().map(|e| e.path.clone()).collect(); let mut paths: Vec<std::path::PathBuf> =
self.catalog.read().unwrap_or_else(std::sync::PoisonError::into_inner).iter().map(|e| e.path.clone()).collect();
paths.sort(); paths.sort();
paths.dedup(); paths.dedup();
for p in paths { for p in paths {
@@ -1253,7 +1254,7 @@ impl FontDb {
} }
} }
if remember_miss { if remember_miss {
self.sys.lock().unwrap_or_else(|e| e.into_inner()).misses.insert(c); self.sys.lock().unwrap_or_else(std::sync::PoisonError::into_inner).misses.insert(c);
} }
false false
} }
@@ -1326,10 +1327,10 @@ impl FontDb {
if c.is_control() || c.is_whitespace() { if c.is_control() || c.is_whitespace() {
return false; return false;
} }
if !self.sys.lock().unwrap_or_else(|e| e.into_inner()).enabled { if !self.sys.lock().unwrap_or_else(std::sync::PoisonError::into_inner).enabled {
return false; return false;
} }
if MISSES.lock().unwrap_or_else(|e| e.into_inner()).get_or_insert_with(Default::default).contains(&c) { if MISSES.lock().unwrap_or_else(std::sync::PoisonError::into_inner).get_or_insert_with(Default::default).contains(&c) {
return false; return false;
} }
let mut paths: Vec<std::path::PathBuf> = self.read_catalog().iter().map(|e| e.path.clone()).collect(); let mut paths: Vec<std::path::PathBuf> = self.read_catalog().iter().map(|e| e.path.clone()).collect();
@@ -1347,7 +1348,7 @@ impl FontDb {
return true; return true;
} }
} }
MISSES.lock().unwrap_or_else(|e| e.into_inner()).get_or_insert_with(Default::default).insert(c); MISSES.lock().unwrap_or_else(std::sync::PoisonError::into_inner).get_or_insert_with(Default::default).insert(c);
false false
} }
@@ -1359,12 +1360,12 @@ impl FontDb {
return false; return false;
} }
{ {
let sys = self.sys.lock().unwrap_or_else(|e| e.into_inner()); let sys = self.sys.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
if !sys.enabled { if !sys.enabled {
return false; return false;
} }
} }
if MISSES.lock().unwrap_or_else(|e| e.into_inner()).get_or_insert_with(Default::default).contains(&c) { if MISSES.lock().unwrap_or_else(std::sync::PoisonError::into_inner).get_or_insert_with(Default::default).contains(&c) {
return false; return false;
} }
let mut paths: Vec<std::path::PathBuf> = self.read_catalog().iter().map(|e| e.path.clone()).collect(); let mut paths: Vec<std::path::PathBuf> = self.read_catalog().iter().map(|e| e.path.clone()).collect();
@@ -1382,14 +1383,14 @@ impl FontDb {
return true; return true;
} }
} }
MISSES.lock().unwrap_or_else(|e| e.into_inner()).get_or_insert_with(Default::default).insert(c); MISSES.lock().unwrap_or_else(std::sync::PoisonError::into_inner).get_or_insert_with(Default::default).insert(c);
false false
} }
/// Glyph outline in font units, y-down (flipped), cached per (face, glyph). /// Glyph outline in font units, y-down (flipped), cached per (face, glyph).
pub fn outline(&self, face: &FontFace, gid: u32) -> Arc<BezPath> { pub fn outline(&self, face: &FontFace, gid: u32) -> Arc<BezPath> {
let key = (face.id, gid); let key = (face.id, gid);
if let Some(p) = self.outlines.lock().unwrap_or_else(|e| e.into_inner()).get(&key) { if let Some(p) = self.outlines.lock().unwrap_or_else(std::sync::PoisonError::into_inner).get(&key) {
return p.clone(); return p.clone();
} }
let mut pen = FlipPen(BezPath::new()); let mut pen = FlipPen(BezPath::new());
@@ -1399,7 +1400,7 @@ impl FontDb {
let _ = g.draw(DrawSettings::unhinted(Size::unscaled(), face.location()), &mut pen); let _ = g.draw(DrawSettings::unhinted(Size::unscaled(), face.location()), &mut pen);
} }
let p = Arc::new(pen.0); let p = Arc::new(pen.0);
let mut cache = self.outlines.lock().unwrap_or_else(|e| e.into_inner()); let mut cache = self.outlines.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
if cache.len() >= OUTLINE_CACHE_MAX { if cache.len() >= OUTLINE_CACHE_MAX {
cache.clear(); cache.clear();
} }
+26 -26
View File
@@ -1070,33 +1070,33 @@ mod tests {
assert_eq!(md_list_item(not), None, "{not:?}"); assert_eq!(md_list_item(not), None, "{not:?}");
} }
} }
}
/// #814: slides_to_outline includes table cell text, which it used to skip. /// #814: slides_to_outline includes table cell text, which it used to skip.
#[test] #[test]
fn outline_includes_table_text() { fn outline_includes_table_text() {
use deckcraft_model::table::{Cell, Row, Table}; use deckcraft_model::table::{Cell, Row, Table};
use deckcraft_model::{Shape, ShapeKind, Xfrm}; use deckcraft_model::{Shape, ShapeKind, Xfrm};
let mut p = defaults::new_presentation(None); let mut p = defaults::new_presentation(None);
let layout = defaults::layout_of_kind(&p, LayoutType::TitleAndContent).unwrap(); let layout = defaults::layout_of_kind(&p, LayoutType::TitleAndContent).unwrap();
let mut s = defaults::new_slide(&mut p, layout).expect("slide"); let mut s = defaults::new_slide(&mut p, layout).expect("slide");
s.shapes.retain(|sh| !sh.ph_type().is_some_and(|k| k.is_title())); s.shapes.retain(|sh| !sh.ph_type().is_some_and(|k| k.is_title()));
let mut body = Shape { xfrm: Some(Xfrm::new(0.0, 0.0, 600.0, 100.0)), ..Default::default() }; let mut body = Shape { xfrm: Some(Xfrm::new(0.0, 0.0, 600.0, 100.0)), ..Default::default() };
body.text = Some(TextBody::from_text("Text box body")); body.text = Some(TextBody::from_text("Text box body"));
s.shapes.push(body); s.shapes.push(body);
let mut table = Table { cols: vec![200.0, 200.0], ..Default::default() }; let mut table = Table { cols: vec![200.0, 200.0], ..Default::default() };
for texts in [["Cell A1", "Cell B1"], ["Cell A2", "Cell B2"]] { for texts in [["Cell A1", "Cell B1"], ["Cell A2", "Cell B2"]] {
let cells: Vec<Cell> = texts.iter().map(|t| Cell { text: TextBody::from_text(t), ..Default::default() }).collect(); let cells: Vec<Cell> = texts.iter().map(|t| Cell { text: TextBody::from_text(t), ..Default::default() }).collect();
table.rows.push(Row { height: 30.0, cells }); table.rows.push(Row { height: 30.0, cells });
}
let mut tbl = Shape { xfrm: Some(Xfrm::new(0.0, 100.0, 400.0, 60.0)), ..Default::default() };
tbl.kind = ShapeKind::Table(table);
s.shapes.push(tbl);
p.slides = vec![std::sync::Arc::new(s)];
let out = slides_to_outline(&p);
assert!(out.contains("Text box body"), "{out}");
assert!(out.contains("Cell A1\tCell B1"), "{out}");
assert!(out.contains("Cell A2\tCell B2"), "{out}");
} }
let mut tbl = Shape { xfrm: Some(Xfrm::new(0.0, 100.0, 400.0, 60.0)), ..Default::default() };
tbl.kind = ShapeKind::Table(table);
s.shapes.push(tbl);
p.slides = vec![std::sync::Arc::new(s)];
let out = slides_to_outline(&p);
assert!(out.contains("Text box body"), "{out}");
assert!(out.contains("Cell A1\tCell B1"), "{out}");
assert!(out.contains("Cell A2\tCell B2"), "{out}");
} }
+1 -1
View File
@@ -254,7 +254,7 @@ impl Player {
} }
fn with<R>(&self, f: impl FnOnce(&mut Mixer) -> R) -> R { fn with<R>(&self, f: impl FnOnce(&mut Mixer) -> R) -> R {
let mut g = self.mixer.lock().unwrap_or_else(|e| e.into_inner()); let mut g = self.mixer.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
f(&mut g) f(&mut g)
} }
+1 -1
View File
@@ -1187,7 +1187,7 @@ fn image(r: Bytes) -> Option<ImageData> {
fn metafile(r: Bytes) -> Option<Picture> { fn metafile(r: Bytes) -> Option<Picture> {
let size = r.u32(12)? as usize; let size = r.u32(12)? as usize;
let data = r.slice(16, size).or_else(|| r.tail(16).map(|t| t.0))?; let data = r.slice(16, size).or_else(|| r.tail(16).map(|t| t.0))?;
let depth = NESTING.with(|n| n.get()); let depth = NESTING.with(std::cell::Cell::get);
if depth >= MAX_NESTING { if depth >= MAX_NESTING {
return None; return None;
} }
+1 -1
View File
@@ -1716,7 +1716,7 @@ fn plus_pic(recs: Vec<Vec<u8>>) -> Picture {
} }
fn u16s(s: &str) -> Vec<u8> { fn u16s(s: &str) -> Vec<u8> {
s.encode_utf16().flat_map(|u| u.to_le_bytes()).collect() s.encode_utf16().flat_map(u16::to_le_bytes).collect()
} }
/// A 2 x 2 32bpp ARGB bitmap image object: red, green / blue, white. /// A 2 x 2 32bpp ARGB bitmap image object: red, green / blue, white.
+86 -56
View File
@@ -491,50 +491,66 @@ impl Default for Chart {
} }
impl Chart { impl Chart {
fn sample_treemap_sunburst(kind: ChartType) -> Self {
let mut c = Chart {
kind,
categories: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"].iter().map(ToString::to_string).collect(),
category_levels: vec![["Q1", "Q1", "Q1", "Q2", "Q2", "Q2", "Q3", "Q3"].iter().map(ToString::to_string).collect()],
vary_colors: true,
..Default::default()
};
let v = [4.2, 2.8, 3.6, 4.7, 2.6, 4.1, 1.9, 3.0];
c.series.push(Series { name: "Series 1".into(), values: v.iter().map(|x| Some(*x)).collect(), ..Default::default() });
c
}
fn sample_observation_chart(kind: ChartType) -> Self {
let obs = [12.0, 15.0, 17.0, 18.0, 21.0, 22.0, 22.0, 24.0, 25.0, 27.0, 28.0, 31.0, 33.0, 36.0, 41.0, 48.0];
let n_series = if kind == ChartType::BoxWhisker { 2 } else { 1 };
let categories = if kind == ChartType::BoxWhisker {
(0..obs.len()).map(|i| if i % 2 == 0 { "Group A" } else { "Group B" }.to_string()).collect()
} else {
vec![]
};
let mut c =
Chart { kind, categories, gridlines: true, legend: (kind == ChartType::BoxWhisker).then(|| "b".to_string()), ..Default::default() };
for s in 0..n_series {
c.series.push(Series {
name: format!("Series {}", s + 1),
values: obs.iter().map(|v| Some(v + s as f64 * 6.0)).collect(),
..Default::default()
});
}
c
}
fn sample_stock() -> Self {
let mut c = Chart {
kind: ChartType::Stock,
categories: ["Mon", "Tue", "Wed", "Thu", "Fri"].iter().map(ToString::to_string).collect(),
..Default::default()
};
let hlc = [("High", [55.0, 57.0, 57.0, 58.0, 58.0]), ("Low", [11.0, 12.0, 13.0, 11.0, 25.0]), ("Close", [25.0, 38.0, 50.0, 35.0, 43.0])];
for (name, v) in hlc {
c.series.push(Series { name: name.into(), values: v.iter().map(|x| Some(*x)).collect(), ..Default::default() });
}
c
}
/// The sample data a new chart starts with (our own numbers). /// The sample data a new chart starts with (our own numbers).
pub fn sample(kind: ChartType) -> Self { pub fn sample(kind: ChartType) -> Self {
let cats = ["Q1", "Q2", "Q3", "Q4"];
let data = [[4.2, 2.6, 2.1], [2.8, 4.1, 2.4], [3.6, 1.9, 3.3], [4.7, 3.0, 5.1]];
let mut c = Chart { kind, categories: cats.iter().map(|s| s.to_string()).collect(), ..Default::default() };
if matches!(kind, ChartType::Treemap | ChartType::Sunburst) { if matches!(kind, ChartType::Treemap | ChartType::Sunburst) {
// A two-level hierarchy: months inside quarters. return Self::sample_treemap_sunburst(kind);
c.categories = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"].iter().map(|s| s.to_string()).collect();
c.category_levels = vec![["Q1", "Q1", "Q1", "Q2", "Q2", "Q2", "Q3", "Q3"].iter().map(|s| s.to_string()).collect()];
let v = [4.2, 2.8, 3.6, 4.7, 2.6, 4.1, 1.9, 3.0];
c.series.push(Series { name: "Series 1".into(), values: v.iter().map(|x| Some(*x)).collect(), ..Default::default() });
c.vary_colors = true;
return c;
} }
if matches!(kind, ChartType::Histogram | ChartType::Pareto | ChartType::BoxWhisker) { if matches!(kind, ChartType::Histogram | ChartType::Pareto | ChartType::BoxWhisker) {
// These plot raw observations: a histogram bins one series, a box chart summarises return Self::sample_observation_chart(kind);
// each category's points.
let obs = [12.0, 15.0, 17.0, 18.0, 21.0, 22.0, 22.0, 24.0, 25.0, 27.0, 28.0, 31.0, 33.0, 36.0, 41.0, 48.0];
let n_series = if kind == ChartType::BoxWhisker { 2 } else { 1 };
c.categories = if kind == ChartType::BoxWhisker {
(0..obs.len()).map(|i| if i % 2 == 0 { "Group A" } else { "Group B" }.to_string()).collect()
} else {
vec![]
};
for s in 0..n_series {
c.series.push(Series {
name: format!("Series {}", s + 1),
values: obs.iter().map(|v| Some(v + s as f64 * 6.0)).collect(),
..Default::default()
});
}
c.gridlines = true;
c.legend = (kind == ChartType::BoxWhisker).then(|| "b".to_string());
return c;
} }
if kind == ChartType::Stock { if kind == ChartType::Stock {
// High-low-close over five days, the series in the order PowerPoint expects. return Self::sample_stock();
c.categories = ["Mon", "Tue", "Wed", "Thu", "Fri"].iter().map(|s| s.to_string()).collect();
let hlc = [("High", [55.0, 57.0, 57.0, 58.0, 58.0]), ("Low", [11.0, 12.0, 13.0, 11.0, 25.0]), ("Close", [25.0, 38.0, 50.0, 35.0, 43.0])];
for (name, v) in hlc {
c.series.push(Series { name: name.into(), values: v.iter().map(|x| Some(*x)).collect(), ..Default::default() });
}
return c;
} }
let cats = ["Q1", "Q2", "Q3", "Q4"];
let data = [[4.2, 2.6, 2.1], [2.8, 4.1, 2.4], [3.6, 1.9, 3.3], [4.7, 3.0, 5.1]];
let mut c = Chart { kind, categories: cats.iter().map(ToString::to_string).collect(), ..Default::default() };
let n_series = let n_series =
if matches!(kind, ChartType::Pie | ChartType::Doughnut | ChartType::Funnel | ChartType::Waterfall | ChartType::Treemap) { 1 } else { 3 }; if matches!(kind, ChartType::Pie | ChartType::Doughnut | ChartType::Funnel | ChartType::Waterfall | ChartType::Treemap) { 1 } else { 3 };
for s in 0..n_series { for s in 0..n_series {
@@ -699,38 +715,52 @@ impl Chart {
crate::numfmt::format(code.map_or("General", String::as_str), v, self.lang.as_deref()) crate::numfmt::format(code.map_or("General", String::as_str), v, self.lang.as_deref())
} }
/// Value range across series (stacked sums for stacked types). fn waterfall_value_range(&self) -> (f64, f64) {
pub fn value_range(&self) -> (f64, f64) {
let mut lo: f64 = 0.0; let mut lo: f64 = 0.0;
let mut hi: f64 = 0.0; let mut hi: f64 = 0.0;
if self.kind == ChartType::Waterfall { for (a, b) in self.waterfall_bars() {
for (a, b) in self.waterfall_bars() { lo = lo.min(a.min(b));
lo = lo.min(a.min(b)); hi = hi.max(a.max(b));
hi = hi.max(a.max(b)); }
(lo, hi)
}
fn stacked_value_range(&self, n: usize) -> (f64, f64) {
let mut lo: f64 = 0.0;
let mut hi: f64 = 0.0;
for i in 0..n {
let (mut p, mut m) = (0.0, 0.0);
for s in &self.series {
let v = s.values.get(i).copied().flatten().unwrap_or(0.0);
if v >= 0.0 { p += v } else { m += v }
} }
return (lo, hi); hi = hi.max(p);
lo = lo.min(m);
}
(lo, hi)
}
/// Value range across series (stacked sums for stacked types).
pub fn value_range(&self) -> (f64, f64) {
if self.kind == ChartType::Waterfall {
return self.waterfall_value_range();
} }
let n = self.categories.len().max(self.series.iter().map(|s| s.values.len()).max().unwrap_or(0)); let n = self.categories.len().max(self.series.iter().map(|s| s.values.len()).max().unwrap_or(0));
if self.kind.stacked() { let (mut lo, mut hi) = if self.kind.stacked() {
for i in 0..n { self.stacked_value_range(n)
let (mut p, mut m) = (0.0, 0.0);
for s in &self.series {
let v = s.values.get(i).copied().flatten().unwrap_or(0.0);
if v >= 0.0 { p += v } else { m += v }
}
hi = hi.max(p);
lo = lo.min(m);
}
} else { } else {
let mut l: f64 = 0.0;
let mut h: f64 = 0.0;
for s in &self.series { for s in &self.series {
for v in s.values.iter().flatten() { for v in s.values.iter().flatten() {
if v.is_finite() { if v.is_finite() {
hi = hi.max(*v); h = h.max(*v);
lo = lo.min(*v); l = l.min(*v);
} }
} }
} }
} (l, h)
};
if self.kind.percent() { if self.kind.percent() {
return (0.0, 1.0); return (0.0, 1.0);
} }
+3 -3
View File
@@ -1066,13 +1066,13 @@ impl Presentation {
self.slides.iter().position(|s| s.id == id) self.slides.iter().position(|s| s.id == id)
} }
pub fn slide(&self, id: SlideId) -> Option<&Slide> { pub fn slide(&self, id: SlideId) -> Option<&Slide> {
self.slides.iter().find(|s| s.id == id).map(|s| s.as_ref()) self.slides.iter().find(|s| s.id == id).map(AsRef::as_ref)
} }
pub fn slide_mut(&mut self, id: SlideId) -> Option<&mut Slide> { pub fn slide_mut(&mut self, id: SlideId) -> Option<&mut Slide> {
self.slides.iter_mut().find(|s| s.id == id).map(Arc::make_mut) self.slides.iter_mut().find(|s| s.id == id).map(Arc::make_mut)
} }
pub fn slide_at(&self, i: usize) -> Option<&Slide> { pub fn slide_at(&self, i: usize) -> Option<&Slide> {
self.slides.get(i).map(|s| s.as_ref()) self.slides.get(i).map(AsRef::as_ref)
} }
/// Master and layout for a layout id. /// Master and layout for a layout id.
pub fn layout(&self, id: LayoutId) -> Option<(&Master, &Layout)> { pub fn layout(&self, id: LayoutId) -> Option<(&Master, &Layout)> {
@@ -1086,7 +1086,7 @@ impl Presentation {
self.masters.first().map(|m| (m.as_ref(), m.layouts.first())) self.masters.first().map(|m| (m.as_ref(), m.layouts.first()))
} }
pub fn first_master(&self) -> Option<&Master> { pub fn first_master(&self) -> Option<&Master> {
self.masters.first().map(|m| m.as_ref()) self.masters.first().map(AsRef::as_ref)
} }
pub fn media(&self, id: MediaId) -> Option<&MediaItem> { pub fn media(&self, id: MediaId) -> Option<&MediaItem> {
self.media.iter().find(|m| m.id == id) self.media.iter().find(|m| m.id == id)
+1 -1
View File
@@ -275,7 +275,7 @@ pub enum Dash {
} }
impl Dash { impl Dash {
pub const PRESETS: [&'static str; 11] = pub const PRESETS: [&str; 11] =
["solid", "sysDot", "sysDash", "dash", "dashDot", "lgDash", "lgDashDot", "lgDashDotDot", "dot", "sysDashDot", "sysDashDotDot"]; ["solid", "sysDot", "sysDash", "dash", "dashDot", "lgDash", "lgDashDot", "lgDashDotDot", "dot", "sysDashDot", "sysDashDotDot"];
/// Dash/space lengths in multiples of the line width. /// Dash/space lengths in multiples of the line width.
pub fn pattern(&self) -> Vec<f64> { pub fn pattern(&self) -> Vec<f64> {
+2 -2
View File
@@ -530,7 +530,7 @@ impl Paragraph {
true true
} }
pub fn text(&self) -> String { pub fn text(&self) -> String {
self.runs.iter().map(|r| r.content()).collect() self.runs.iter().map(Run::content).collect()
} }
pub fn char_len(&self) -> usize { pub fn char_len(&self) -> usize {
self.runs.iter().map(Run::char_len).sum() self.runs.iter().map(Run::char_len).sum()
@@ -576,7 +576,7 @@ pub struct ListStyle {
impl ListStyle { impl ListStyle {
pub fn level(&self, l: u8) -> Option<&LevelStyle> { pub fn level(&self, l: u8) -> Option<&LevelStyle> {
self.levels.get(l as usize).and_then(|x| x.as_ref()) self.levels.get(l as usize).and_then(Option::as_ref)
} }
pub fn set(&mut self, l: u8, s: LevelStyle) { pub fn set(&mut self, l: u8, s: LevelStyle) {
let i = (l as usize).min(8); let i = (l as usize).min(8);
+87 -74
View File
@@ -57,22 +57,7 @@ impl Trendline {
let yhat: Vec<f64> = xs.iter().map(|x| poly_y(&coeff, *x)).collect(); let yhat: Vec<f64> = xs.iter().map(|x| poly_y(&coeff, *x)).collect();
(Model::Poly(coeff), ys, yhat) (Model::Poly(coeff), ys, yhat)
} }
TrendlineType::Exp => { TrendlineType::Exp => fit_exp(&pairs, intercept)?,
let pairs: Vec<(f64, f64)> = pairs.into_iter().filter(|(_, y)| *y > 0.0).collect();
if pairs.len() < 2 {
return None;
}
let (xs, ln_y): (Vec<f64>, Vec<f64>) = pairs.iter().map(|(x, y)| (*x, y.ln())).unzip();
let fixed = intercept.filter(|c| *c > 0.0).map(|c| c.ln());
let (ln_c, b) = linreg(&xs, &ln_y, fixed)?;
let c = ln_c.exp();
if !c.is_finite() || c <= 0.0 {
return None;
}
let ys: Vec<f64> = pairs.iter().map(|(_, y)| *y).collect();
let yhat: Vec<f64> = xs.iter().map(|x| c * (b * x).exp()).collect();
(Model::Exp { c, b }, ys, yhat)
}
TrendlineType::Log => { TrendlineType::Log => {
let pairs: Vec<(f64, f64)> = pairs.into_iter().filter(|(x, _)| *x > 0.0).collect(); let pairs: Vec<(f64, f64)> = pairs.into_iter().filter(|(x, _)| *x > 0.0).collect();
if pairs.len() < 2 { if pairs.len() < 2 {
@@ -83,43 +68,10 @@ impl Trendline {
let yhat: Vec<f64> = ln_x.iter().map(|u| b + c * u).collect(); let yhat: Vec<f64> = ln_x.iter().map(|u| b + c * u).collect();
(Model::Log { b, c }, ys, yhat) (Model::Log { b, c }, ys, yhat)
} }
TrendlineType::Power => { TrendlineType::Power => fit_power(&pairs, intercept)?,
let pairs: Vec<(f64, f64)> = pairs.into_iter().filter(|(x, y)| *x > 0.0 && *y > 0.0).collect();
if pairs.len() < 2 {
return None;
}
let (ln_x, ln_y): (Vec<f64>, Vec<f64>) = pairs.iter().map(|(x, y)| (x.ln(), y.ln())).unzip();
let fixed = intercept.filter(|c| *c > 0.0).map(|c| c.ln());
let (ln_c, b) = linreg(&ln_x, &ln_y, fixed)?;
let c = ln_c.exp();
if !c.is_finite() || c <= 0.0 {
return None;
}
let ys: Vec<f64> = pairs.iter().map(|(_, y)| *y).collect();
let yhat: Vec<f64> = pairs.iter().map(|(x, _)| c * x.powf(b)).collect();
(Model::Power { c, b }, ys, yhat)
}
TrendlineType::MovingAvg => { TrendlineType::MovingAvg => {
let period = self.period.unwrap_or(2).clamp(2, 255) as usize; let period = self.period.unwrap_or(2).clamp(2, 255) as usize;
if pairs.len() <= period { fit_moving_avg(&pairs, period)?
return None;
}
let mut pts = Vec::new();
let mut ys = Vec::new();
let mut yhat = Vec::new();
for i in period - 1..pairs.len() {
let window = &pairs[i + 1 - period..=i];
let avg = window.iter().map(|(_, y)| y).sum::<f64>() / period as f64;
if avg.is_finite() {
pts.push((pairs[i].0, avg));
ys.push(pairs[i].1);
yhat.push(avg);
}
}
if pts.len() < 2 {
return None;
}
(Model::Moving(pts), ys, yhat)
} }
}; };
if yhat.iter().any(|v| !v.is_finite()) { if yhat.iter().any(|v| !v.is_finite()) {
@@ -325,6 +277,89 @@ fn poly_y(coeff: &[f64], x: f64) -> f64 {
acc acc
} }
fn fit_exp(pairs: &[(f64, f64)], intercept: Option<f64>) -> Option<(Model, Vec<f64>, Vec<f64>)> {
let pairs: Vec<(f64, f64)> = pairs.iter().copied().filter(|(_, y)| *y > 0.0).collect();
if pairs.len() < 2 {
return None;
}
let (xs, ln_y): (Vec<f64>, Vec<f64>) = pairs.iter().map(|(x, y)| (*x, y.ln())).unzip();
let fixed = intercept.filter(|c| *c > 0.0).map(f64::ln);
let (ln_c, b) = linreg(&xs, &ln_y, fixed)?;
let c = ln_c.exp();
if !c.is_finite() || c <= 0.0 {
return None;
}
let ys: Vec<f64> = pairs.iter().map(|(_, y)| *y).collect();
let yhat: Vec<f64> = xs.iter().map(|x| c * (b * x).exp()).collect();
Some((Model::Exp { c, b }, ys, yhat))
}
fn fit_power(pairs: &[(f64, f64)], intercept: Option<f64>) -> Option<(Model, Vec<f64>, Vec<f64>)> {
let pairs: Vec<(f64, f64)> = pairs.iter().copied().filter(|(x, y)| *x > 0.0 && *y > 0.0).collect();
if pairs.len() < 2 {
return None;
}
let (ln_x, ln_y): (Vec<f64>, Vec<f64>) = pairs.iter().map(|(x, y)| (x.ln(), y.ln())).unzip();
let fixed = intercept.filter(|c| *c > 0.0).map(f64::ln);
let (ln_c, b) = linreg(&ln_x, &ln_y, fixed)?;
let c = ln_c.exp();
if !c.is_finite() || c <= 0.0 {
return None;
}
let ys: Vec<f64> = pairs.iter().map(|(_, y)| *y).collect();
let yhat: Vec<f64> = pairs.iter().map(|(x, _)| c * x.powf(b)).collect();
Some((Model::Power { c, b }, ys, yhat))
}
fn fit_moving_avg(pairs: &[(f64, f64)], period: usize) -> Option<(Model, Vec<f64>, Vec<f64>)> {
if pairs.len() <= period {
return None;
}
let mut pts = Vec::new();
let mut ys = Vec::new();
let mut yhat = Vec::new();
for i in period - 1..pairs.len() {
let window = &pairs[i + 1 - period..=i];
let avg = window.iter().map(|(_, y)| y).sum::<f64>() / period as f64;
if avg.is_finite() {
pts.push((pairs[i].0, avg));
ys.push(pairs[i].1);
yhat.push(avg);
}
}
if pts.len() < 2 {
return None;
}
Some((Model::Moving(pts), ys, yhat))
}
#[allow(clippy::needless_range_loop)]
fn polyfit_with_intercept(xs: &[f64], ys: &[f64], order: usize, b0: f64) -> Option<Vec<f64>> {
let m = order;
let mut ata = vec![vec![0.0; m]; m];
let mut atb = vec![0.0; m];
for (x, y) in xs.iter().zip(ys) {
let dy = y - b0;
for i in 0..m {
let xi = x.powi((i + 1) as i32);
atb[i] += dy * xi;
for j in i..m {
ata[i][j] += xi * x.powi((j + 1) as i32);
}
}
}
for i in 0..m {
for j in 0..i {
ata[i][j] = ata[j][i];
}
}
let solved = solve(ata, atb)?;
let mut coeff = Vec::with_capacity(order + 1);
coeff.push(b0);
coeff.extend(solved);
coeff.iter().all(|v| v.is_finite()).then_some(coeff)
}
/// Polynomial through the normal equations. `x` is centred when the intercept is free, then /// Polynomial through the normal equations. `x` is centred when the intercept is free, then
/// expanded back to powers of `x`, so order 6 on category numbers stays solvable. /// expanded back to powers of `x`, so order 6 on category numbers stays solvable.
#[allow(clippy::needless_range_loop)] // the index is the power in the normal equations #[allow(clippy::needless_range_loop)] // the index is the power in the normal equations
@@ -334,29 +369,7 @@ fn polyfit(xs: &[f64], ys: &[f64], order: usize, intercept: Option<f64>) -> Opti
return None; return None;
} }
if let Some(b0) = intercept { if let Some(b0) = intercept {
let m = order; return polyfit_with_intercept(xs, ys, order, b0);
let mut ata = vec![vec![0.0; m]; m];
let mut atb = vec![0.0; m];
for (x, y) in xs.iter().zip(ys) {
let dy = y - b0;
for i in 0..m {
let xi = x.powi((i + 1) as i32);
atb[i] += dy * xi;
for j in i..m {
ata[i][j] += xi * x.powi((j + 1) as i32);
}
}
}
for i in 0..m {
for j in 0..i {
ata[i][j] = ata[j][i];
}
}
let solved = solve(ata, atb)?;
let mut coeff = Vec::with_capacity(order + 1);
coeff.push(b0);
coeff.extend(solved);
return coeff.iter().all(|v| v.is_finite()).then_some(coeff);
} }
let xm = xs.iter().sum::<f64>() / n as f64; let xm = xs.iter().sum::<f64>() / n as f64;
let us: Vec<f64> = xs.iter().map(|x| x - xm).collect(); let us: Vec<f64> = xs.iter().map(|x| x - xm).collect();
+1 -1
View File
@@ -71,7 +71,7 @@ fn exp_rotation1(x: &mut [f32], len: usize, stride: usize, c: f32, s: f32) {
x[i + stride] = c * x2 + s * x1; x[i + stride] = c * x2 + s * x1;
x[i] = c * x1 + ms * x2; x[i] = c * x1 + ms * x2;
} }
if len >= 2 * stride + 1 { if len > 2 * stride {
for i in (0..=len - 2 * stride - 1).rev() { for i in (0..=len - 2 * stride - 1).rev() {
let x1 = x[i]; let x1 = x[i];
let x2 = x[i + stride]; let x2 = x[i + stride];
+1 -1
View File
@@ -578,7 +578,7 @@ fn tf_decode(start: usize, end: usize, is_transient: bool, tf_res: &mut [i32; NB
let mut budget = dec.storage() as i32 * 8; let mut budget = dec.storage() as i32 * 8;
let mut tell = dec.tell(); let mut tell = dec.tell();
let mut logp: u32 = if is_transient { 2 } else { 4 }; let mut logp: u32 = if is_transient { 2 } else { 4 };
let tf_select_rsv = lm > 0 && tell + logp as i32 + 1 <= budget; let tf_select_rsv = lm > 0 && tell + (logp as i32) < budget;
budget -= tf_select_rsv as i32; budget -= tf_select_rsv as i32;
let mut tf_changed = 0; let mut tf_changed = 0;
let mut curr = 0; let mut curr = 0;
+1 -1
View File
@@ -217,7 +217,7 @@ impl StreamDecoder {
} }
} }
} }
let data_len = data.map_or(0, |d| d.len()); let data_len = data.map_or(0, <[_]>::len);
let mut dec_storage; let mut dec_storage;
let mut dec: Option<&mut RangeDecoder> = match data { let mut dec: Option<&mut RangeDecoder> = match data {
Some(d) => { Some(d) => {
+29 -21
View File
@@ -131,6 +131,33 @@ pub fn export(pres: &Presentation, opts: &PdfOptions) -> Result<Vec<u8>> {
write::guard(|| export_inner(pres, opts)) write::guard(|| export_inner(pres, opts))
} }
fn plan_pages(pres: &Presentation, opts: &PdfOptions, slides: &[usize], sw: f64, sh: f64) -> Vec<Vec<(usize, Rect)>> {
match opts.layout {
PageLayout::Slides => slides.iter().map(|&i| vec![(i, Rect::new(0.0, 0.0, sw, sh))]).collect(),
PageLayout::Notes => {
let b = notes_frames(pres, opts.paper).0;
slides.iter().map(|&i| vec![(i, b)]).collect()
}
PageLayout::Handouts { per_page } => {
let boxes = handout_boxes(per_page, opts.paper, sw / sh);
slides.chunks(boxes.len()).map(|c| c.iter().zip(&boxes).map(|(i, b)| (*i, *b)).collect()).collect()
}
}
}
fn build_outline(pres: &Presentation, pages: &[Vec<(usize, Rect)>]) -> Outline {
let mut outline = Outline::new();
for (p, v) in pages.iter().enumerate() {
for (i, _) in v {
let t = pres.slides[*i].title();
let t = t.lines().next().unwrap_or("").trim().to_string();
let label = if t.is_empty() { format!("Slide {}", i + 1) } else { format!("{}. {t}", i + 1) };
outline.push_child(OutlineNode::new(label, XyzDestination::new(p, Point::from_xy(0.0, 0.0))));
}
}
outline
}
fn export_inner(pres: &Presentation, opts: &PdfOptions) -> Result<Vec<u8>> { fn export_inner(pres: &Presentation, opts: &PdfOptions) -> Result<Vec<u8>> {
let slides: Vec<usize> = match &opts.slides { let slides: Vec<usize> = match &opts.slides {
Some(v) => v.iter().copied().filter(|i| *i < pres.slides.len()).collect(), Some(v) => v.iter().copied().filter(|i| *i < pres.slides.len()).collect(),
@@ -153,29 +180,10 @@ fn export_inner(pres: &Presentation, opts: &PdfOptions) -> Result<Vec<u8>> {
let (sw, sh) = (pres.slide_size.width.max(1.0), pres.slide_size.height.max(1.0)); let (sw, sh) = (pres.slide_size.width.max(1.0), pres.slide_size.height.max(1.0));
let mut ex = Exporter { pres, opts, fonts: HashMap::new() }; let mut ex = Exporter { pres, opts, fonts: HashMap::new() };
// Page plan: per page, the slides on it with their boxes (page points). // Page plan: per page, the slides on it with their boxes (page points).
let pages: Vec<Vec<(usize, Rect)>> = match opts.layout { let pages = plan_pages(pres, opts, &slides, sw, sh);
PageLayout::Slides => slides.iter().map(|&i| vec![(i, Rect::new(0.0, 0.0, sw, sh))]).collect(),
PageLayout::Notes => {
let b = notes_frames(pres, opts.paper).0;
slides.iter().map(|&i| vec![(i, b)]).collect()
}
PageLayout::Handouts { per_page } => {
let boxes = handout_boxes(per_page, opts.paper, sw / sh);
slides.chunks(boxes.len()).map(|c| c.iter().zip(&boxes).map(|(i, b)| (*i, *b)).collect()).collect()
}
};
// Bookmarks and slide → page lookup for internal links. // Bookmarks and slide → page lookup for internal links.
let page_of: HashMap<usize, usize> = pages.iter().enumerate().flat_map(|(p, v)| v.iter().map(move |(i, _)| (*i, p))).collect(); let page_of: HashMap<usize, usize> = pages.iter().enumerate().flat_map(|(p, v)| v.iter().map(move |(i, _)| (*i, p))).collect();
let mut outline = Outline::new(); pdf.set_outline(build_outline(pres, &pages));
for (p, v) in pages.iter().enumerate() {
for (i, _) in v {
let t = pres.slides[*i].title();
let t = t.lines().next().unwrap_or("").trim().to_string();
let label = if t.is_empty() { format!("Slide {}", i + 1) } else { format!("{}. {t}", i + 1) };
outline.push_child(OutlineNode::new(label, XyzDestination::new(p, Point::from_xy(0.0, 0.0))));
}
}
pdf.set_outline(outline);
for v in &pages { for v in &pages {
let (pw, ph) = match opts.layout { let (pw, ph) = match opts.layout {
+1 -1
View File
@@ -76,7 +76,7 @@ impl Anim {
static CACHE: Mutex<Option<Cache>> = Mutex::new(None); static CACHE: Mutex<Option<Cache>> = Mutex::new(None);
fn with<R>(f: impl FnOnce(&mut Cache) -> R) -> R { fn with<R>(f: impl FnOnce(&mut Cache) -> R) -> R {
let mut g = CACHE.lock().unwrap_or_else(|e| e.into_inner()); let mut g = CACHE.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
f(g.get_or_insert_with(Cache::default)) f(g.get_or_insert_with(Cache::default))
} }
+2 -2
View File
@@ -1090,7 +1090,7 @@ fn shape_item(
for g in &glyphs { for g in &glyphs {
let Some(&h) = char_at_byte.get(&g.cluster) else { continue }; let Some(&h) = char_at_byte.get(&g.cluster) else { continue };
let Some(cell) = cells.get_mut(h) else { continue }; let Some(cell) = cells.get_mut(h) else { continue };
if matches!(cell.ch, '\t' | '\u{b}' | '\n') { if matches!(cell.ch, '\t'..='\u{b}') {
continue; continue;
} }
let s = scale_of(cell); let s = scale_of(cell);
@@ -1738,7 +1738,7 @@ fn tab_segment(cells: &[Cell], i: usize) -> (f64, f64) {
let mut w = 0.0; let mut w = 0.0;
let mut dec = None; let mut dec = None;
for c in cells.iter().skip(i + 1) { for c in cells.iter().skip(i + 1) {
if matches!(c.ch, '\t' | '\u{b}' | '\n') { if matches!(c.ch, '\t'..='\u{b}') {
break; break;
} }
if c.ch == '.' && dec.is_none() { if c.ch == '.' && dec.is_none() {
+3 -4
View File
@@ -2027,7 +2027,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool {
}); });
ui.horizontal(|ui| { ui.horizontal(|ui| {
if ui.button("+ Row").clicked() { if ui.button("+ Row").clicked() {
let n = grid.first().map(|r| r.len()).unwrap_or(2); let n = grid.first().map(Vec::len).unwrap_or(2);
grid.push(vec![String::new(); n]); grid.push(vec![String::new(); n]);
} }
if ui.button("+ Series").clicked() { if ui.button("+ Series").clicked() {
@@ -2943,7 +2943,7 @@ fn paragraph_return_params(d: &mut Dialog) -> Value {
let line_value = finite_pt(&d.get("lineValue", "1")).unwrap_or(1.0); let line_value = finite_pt(&d.get("lineValue", "1")).unwrap_or(1.0);
let before_same = d.get("spaceBefore", "") == d.get("spaceBeforeSeed", ""); let before_same = d.get("spaceBefore", "") == d.get("spaceBeforeSeed", "");
let after_same = d.get("spaceAfter", "") == d.get("spaceAfterSeed", ""); let after_same = d.get("spaceAfter", "") == d.get("spaceAfterSeed", "");
let v = json!({ json!({
"align": d.get("align", "left"), "align": d.get("align", "left"),
"indentLeft": before, "indentLeft": before,
"indentRight": after, "indentRight": after,
@@ -2957,8 +2957,7 @@ fn paragraph_return_params(d: &mut Dialog) -> Value {
"lineValue": line_value, "lineValue": line_value,
"tabs": d.params.get("tabs").cloned().unwrap_or_else(|| json!([])), "tabs": d.params.get("tabs").cloned().unwrap_or_else(|| json!([])),
"defaultTab": d.params.get("defaultTab").cloned().unwrap_or_else(|| json!(72.0)), "defaultTab": d.params.get("defaultTab").cloned().unwrap_or_else(|| json!(72.0)),
}); })
v
} }
fn line_preset_label(kind: &str, value: &str) -> &'static str { fn line_preset_label(kind: &str, value: &str) -> &'static str {
+34 -26
View File
@@ -1349,6 +1349,38 @@ pub fn table_grid_picker(ui: &mut Ui) -> Option<(usize, usize)> {
if resp.clicked() && hover.0 > 0 { Some(hover) } else { None } if resp.clicked() && hover.0 > 0 { Some(hover) } else { None }
} }
fn render_icon_categories(app: &mut SlideApp, ui: &mut Ui, icons: &[deckcraft_engine::icons::Icon], color: Color32, hover: Color32) {
let mut cats: Vec<&str> = Vec::new();
for icon in icons {
if !cats.contains(&icon.category) {
cats.push(icon.category);
}
}
for cat in cats {
ui.label(egui::RichText::new(cat).small().strong());
ui.horizontal_wrapped(|ui| {
for icon in icons.iter().filter(|i| i.category == cat) {
icon_cell(app, ui, icon, color, hover);
}
});
}
}
fn render_filtered_icons(app: &mut SlideApp, ui: &mut Ui, icons: &[deckcraft_engine::icons::Icon], q: &str, color: Color32, hover: Color32) {
let mut n = 0;
ui.horizontal_wrapped(|ui| {
for icon in icons {
if icon.name.contains(q) || icon.label.to_lowercase().contains(q) {
icon_cell(app, ui, icon, color, hover);
n += 1;
}
}
});
if n == 0 {
ui.label("No icons match.");
}
}
/// Lucide stroke icons (Insert › Icons). Search filters the gallery; a click inserts the outline. /// Lucide stroke icons (Insert › Icons). Search filters the gallery; a click inserts the outline.
fn icon_library(app: &mut SlideApp, ui: &mut Ui) { fn icon_library(app: &mut SlideApp, ui: &mut Ui) {
let t = Tokens::get(ui.ctx()); let t = Tokens::get(ui.ctx());
@@ -1361,33 +1393,9 @@ fn icon_library(app: &mut SlideApp, ui: &mut Ui) {
let icons = deckcraft_engine::icons::icons(); let icons = deckcraft_engine::icons::icons();
egui::ScrollArea::vertical().id_salt("icon-scroll").max_height(420.0).show(ui, |ui| { egui::ScrollArea::vertical().id_salt("icon-scroll").max_height(420.0).show(ui, |ui| {
if q.is_empty() { if q.is_empty() {
let mut cats: Vec<&str> = Vec::new(); render_icon_categories(app, ui, icons, color, hover);
for icon in icons {
if !cats.contains(&icon.category) {
cats.push(icon.category);
}
}
for cat in cats {
ui.label(egui::RichText::new(cat).small().strong());
ui.horizontal_wrapped(|ui| {
for icon in icons.iter().filter(|i| i.category == cat) {
icon_cell(app, ui, icon, color, hover);
}
});
}
} else { } else {
let mut n = 0; render_filtered_icons(app, ui, icons, &q, color, hover);
ui.horizontal_wrapped(|ui| {
for icon in icons {
if icon.name.contains(&q) || icon.label.to_lowercase().contains(&q) {
icon_cell(app, ui, icon, color, hover);
n += 1;
}
}
});
if n == 0 {
ui.label("No icons match.");
}
} }
}); });
} }
+10 -4
View File
@@ -19,6 +19,12 @@ set -euo pipefail
HERE="$ROOT/packaging/linux" HERE="$ROOT/packaging/linux"
APP_ID=ai.storyteller.deckcraft APP_ID=ai.storyteller.deckcraft
verify_sha256() {
local expected="$1"
local file="$2"
printf '%s %s\n' "$expected" "$file" | sha256sum -c -
}
SKIP_BUILD=0 SKIP_BUILD=0
FORMATS="appimage deb rpm tar" FORMATS="appimage deb rpm tar"
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
@@ -137,21 +143,21 @@ if has appimage; then
download="$TOOL.download" download="$TOOL.download"
curl --proto '=https' -fsSL -o "$download" \ curl --proto '=https' -fsSL -o "$download" \
"https://github.com/AppImage/appimagetool/releases/download/$APPIMAGETOOL_VERSION/appimagetool-$ARCH.AppImage" "https://github.com/AppImage/appimagetool/releases/download/$APPIMAGETOOL_VERSION/appimagetool-$ARCH.AppImage"
printf '%s %s\n' "$APPIMAGETOOL_SHA256" "$download" | sha256sum -c - verify_sha256 "$APPIMAGETOOL_SHA256" "$download"
chmod 755 "$download" chmod 755 "$download"
mv "$download" "$TOOL" mv "$download" "$TOOL"
fi fi
printf '%s %s\n' "$APPIMAGETOOL_SHA256" "$TOOL" | sha256sum -c - verify_sha256 "$APPIMAGETOOL_SHA256" "$TOOL"
fi fi
RUNTIME="$CARGO_TARGET_DIR/appimage-runtime-$APPIMAGE_RUNTIME_VERSION-$ARCH" RUNTIME="$CARGO_TARGET_DIR/appimage-runtime-$APPIMAGE_RUNTIME_VERSION-$ARCH"
if [[ ! -f "$RUNTIME" ]]; then if [[ ! -f "$RUNTIME" ]]; then
download="$RUNTIME.download" download="$RUNTIME.download"
curl --proto '=https' -fsSL -o "$download" \ curl --proto '=https' -fsSL -o "$download" \
"https://github.com/AppImage/type2-runtime/releases/download/$APPIMAGE_RUNTIME_VERSION/runtime-$ARCH" "https://github.com/AppImage/type2-runtime/releases/download/$APPIMAGE_RUNTIME_VERSION/runtime-$ARCH"
printf '%s %s\n' "$APPIMAGE_RUNTIME_SHA256" "$download" | sha256sum -c - verify_sha256 "$APPIMAGE_RUNTIME_SHA256" "$download"
mv "$download" "$RUNTIME" mv "$download" "$RUNTIME"
fi fi
printf '%s %s\n' "$APPIMAGE_RUNTIME_SHA256" "$RUNTIME" | sha256sum -c - verify_sha256 "$APPIMAGE_RUNTIME_SHA256" "$RUNTIME"
# Absolute, because appimagetool runs in $DIST below (CARGO_TARGET_DIR or APPIMAGETOOL may be # Absolute, because appimagetool runs in $DIST below (CARGO_TARGET_DIR or APPIMAGETOOL may be
# relative). # relative).
OUT="$(cd "$DIST" && pwd)/$BASENAME.AppImage" OUT="$(cd "$DIST" && pwd)/$BASENAME.AppImage"
+1 -1
View File
@@ -51,7 +51,7 @@ const ASSET_EXT: &[&str] = &[
const ASSET_DIRS: &[&str] = &["assets/", "docs/images/", "docs/brand/", "examples/"]; const ASSET_DIRS: &[&str] = &["assets/", "docs/images/", "docs/brand/", "examples/"];
pub fn is_asset(path: &str) -> bool { pub fn is_asset(path: &str) -> bool {
let ext = Path::new(path).extension().and_then(|e| e.to_str()).map(str::to_ascii_lowercase).unwrap_or_default(); let ext = Path::new(path).extension().and_then(std::ffi::OsStr::to_str).map(str::to_ascii_lowercase).unwrap_or_default();
ASSET_DIRS.iter().any(|d| path.starts_with(d)) || ASSET_EXT.contains(&ext.as_str()) ASSET_DIRS.iter().any(|d| path.starts_with(d)) || ASSET_EXT.contains(&ext.as_str())
} }
+1 -1
View File
@@ -207,7 +207,7 @@ pub fn check(crates: &[Crate]) -> Vec<Violation> {
} }
} }
} }
out.sort_by_key(|v| v.to_string()); out.sort_by_key(ToString::to_string);
out.dedup(); out.dedup();
out out
} }