mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-28 20:47:15 -06:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54e3e4fb18 | ||
|
|
c7b986f2db |
@@ -5,7 +5,7 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use deckcraft_color::{ColorTransform, Rgba, SchemeSlot};
|
use deckcraft_color::{ColorTransform, Rgba, SchemeSlot};
|
||||||
use deckcraft_model::style::{Dash, Glow, Gradient, GradientShape, GradientStop, LineEnd, PatternFill, PictureFill, PictureMode, Reflection, Shadow};
|
use deckcraft_model::style::{Dash, Glow, Gradient, GradientShape, GradientStop, LineEnd, PatternFill, PictureFill, PictureMode, Reflection, Shadow};
|
||||||
use deckcraft_model::{ColorRef, Effects, Fill, Geom, Line, ShapeStyle};
|
use deckcraft_model::{ColorRef, Effects, Fill, Geom, Line, ShapeId, ShapeStyle};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@@ -556,81 +556,102 @@ fn edit_group_appearance(s: &mut Session, p: &Value, cmd: &str, f: impl Fn(&mut
|
|||||||
edit_shapes(s, p, cmd, |sh| apply(sh, 0, &f))
|
edit_shapes(s, p, cmd, |sh| apply(sh, 0, &f))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fill(s: &mut Session, p: &Value) -> Result<Value> {
|
/// The fill a `shape.fill` request sets: `Some(None)` clears it, `Some(Some(_))` sets it, and `None`
|
||||||
let new_fill: Option<Option<Fill>> = if bool_or(p, "reset", false) {
|
/// leaves it as it is.
|
||||||
Some(None)
|
fn requested_fill(s: &mut Session, p: &Value) -> Result<Option<Option<Fill>>> {
|
||||||
} else if bool_or(p, "none", false) {
|
if bool_or(p, "reset", false) {
|
||||||
Some(Some(Fill::None))
|
return Ok(Some(None));
|
||||||
} else if let Some(c) = color_param(p, "color") {
|
}
|
||||||
Some(Some(Fill::solid(c)))
|
if bool_or(p, "none", false) {
|
||||||
} else if let Some(g) = p.get("gradient") {
|
return Ok(Some(Some(Fill::None)));
|
||||||
Some(Some(Fill::Gradient(
|
}
|
||||||
gradient_param(g).ok_or_else(|| bad("shape.fill", "gradient needs at least two `stops`: [[0, \"#fff\"], [1, \"#000\"]]"))?,
|
if let Some(c) = color_param(p, "color") {
|
||||||
)))
|
return Ok(Some(Some(Fill::solid(c))));
|
||||||
} else if let Some(pt) = p.get("pattern") {
|
}
|
||||||
Some(Some(Fill::Pattern(PatternFill {
|
if let Some(g) = p.get("gradient") {
|
||||||
|
let gradient = gradient_param(g).ok_or_else(|| bad("shape.fill", "gradient needs at least two `stops`: [[0, \"#fff\"], [1, \"#000\"]]"))?;
|
||||||
|
return Ok(Some(Some(Fill::Gradient(gradient))));
|
||||||
|
}
|
||||||
|
if let Some(pt) = p.get("pattern") {
|
||||||
|
return Ok(Some(Some(Fill::Pattern(PatternFill {
|
||||||
preset: pt.get("preset").and_then(Value::as_str).unwrap_or("pct50").to_string(),
|
preset: pt.get("preset").and_then(Value::as_str).unwrap_or("pct50").to_string(),
|
||||||
fg: pt.get("fg").and_then(color_value).unwrap_or(ColorRef::scheme(SchemeSlot::Accent1)),
|
fg: pt.get("fg").and_then(color_value).unwrap_or(ColorRef::scheme(SchemeSlot::Accent1)),
|
||||||
bg: pt.get("bg").and_then(color_value).unwrap_or(ColorRef::scheme(SchemeSlot::Bg1)),
|
bg: pt.get("bg").and_then(color_value).unwrap_or(ColorRef::scheme(SchemeSlot::Bg1)),
|
||||||
})))
|
}))));
|
||||||
} else if p.get("picture").is_some() {
|
}
|
||||||
|
if p.get("picture").is_some() {
|
||||||
let (name, bytes) = super::insert::media_bytes(&with_param(p, "data", p.get("picture").cloned().unwrap_or_default()), "shape.fill")?;
|
let (name, bytes) = super::insert::media_bytes(&with_param(p, "data", p.get("picture").cloned().unwrap_or_default()), "shape.fill")?;
|
||||||
let ct = super::insert::content_type(&name, &bytes);
|
let ct = super::insert::content_type(&name, &bytes);
|
||||||
let media = s.edit(|doc, _| doc.add_media(&name, ct, bytes).ok_or_else(|| bad("shape.fill", NO_ID_SPACE)))?;
|
let media = s.edit(|doc, _| doc.add_media(&name, ct, bytes).ok_or_else(|| bad("shape.fill", NO_ID_SPACE)))?;
|
||||||
Some(Some(Fill::Picture(PictureFill { media, ..Default::default() })))
|
return Ok(Some(Some(Fill::Picture(PictureFill { media, ..Default::default() }))));
|
||||||
} else {
|
}
|
||||||
None
|
Ok(None)
|
||||||
};
|
}
|
||||||
|
|
||||||
|
/// The effective fill of each targeted shape, for a transparency change on an inherited (style) fill.
|
||||||
|
fn inherited_fills(s: &Session, p: &Value) -> Result<HashMap<ShapeId, Option<Fill>>> {
|
||||||
|
let st = s.doc()?;
|
||||||
|
let ctx = super::text::ctx_for(&st.doc, &st.selection);
|
||||||
|
let mut fills = HashMap::new();
|
||||||
|
for id in targets(s, p)? {
|
||||||
|
if let Some(sh) = st.shape(id) {
|
||||||
|
deckcraft_model::walk(std::slice::from_ref(sh), &mut |sh, _| {
|
||||||
|
let fill = ctx.as_ref().and_then(|c| {
|
||||||
|
let (f, ph) = deckcraft_model::resolve::fill(c, sh);
|
||||||
|
f.map(|f| match f {
|
||||||
|
Fill::Solid { color } => Fill::solid(ColorRef::rgb(c.color(&color, ph))),
|
||||||
|
other => other,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
fills.insert(sh.id, fill);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(fills)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies a `shape.fill` request to one shape: the fill, then its picture options, then transparency.
|
||||||
|
fn apply_fill(
|
||||||
|
sh: &mut deckcraft_model::Shape,
|
||||||
|
new_fill: Option<&Option<Fill>>,
|
||||||
|
pic_props: Option<&Value>,
|
||||||
|
transparency: Option<f64>,
|
||||||
|
inherited: &HashMap<ShapeId, Option<Fill>>,
|
||||||
|
) -> Result<()> {
|
||||||
|
if let Some(f) = new_fill {
|
||||||
|
sh.fill.clone_from(f);
|
||||||
|
}
|
||||||
|
if let (Some(pp), Some(Fill::Picture(pf))) = (pic_props, sh.fill.as_mut()) {
|
||||||
|
picture_props(pf, pp);
|
||||||
|
}
|
||||||
|
if let (Some(t), Some(Fill::Picture(pf))) = (transparency, sh.fill.as_mut()) {
|
||||||
|
let a = 1.0 - t.clamp(0.0, 1.0);
|
||||||
|
pf.alpha = (a < 1.0).then_some(a);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if let Some(t) = transparency {
|
||||||
|
let base = sh.fill.clone().or_else(|| inherited.get(&sh.id).cloned().flatten());
|
||||||
|
if let Some(Fill::Solid { mut color }) = base {
|
||||||
|
color.mods.retain(|m| !matches!(m, ColorTransform::Alpha(_)));
|
||||||
|
if t > 0.0 {
|
||||||
|
color.mods.push(ColorTransform::Alpha(((1.0 - t.clamp(0.0, 1.0)) * 100_000.0).round() as i32));
|
||||||
|
}
|
||||||
|
sh.fill = Some(Fill::Solid { color });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fill(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
|
let new_fill = requested_fill(s, p)?;
|
||||||
let transparency = f64_param(p, "transparency");
|
let transparency = f64_param(p, "transparency");
|
||||||
let pic_props = p.get("pictureProps").filter(|v| v.is_object());
|
let pic_props = p.get("pictureProps").filter(|v| v.is_object());
|
||||||
if new_fill.is_none() && transparency.is_none() && pic_props.is_none() {
|
if new_fill.is_none() && transparency.is_none() && pic_props.is_none() {
|
||||||
return Err(bad("shape.fill", "give `color`, `none`, `gradient`, `pattern`, `picture`, `pictureProps`, `transparency` or `reset`"));
|
return Err(bad("shape.fill", "give `color`, `none`, `gradient`, `pattern`, `picture`, `pictureProps`, `transparency` or `reset`"));
|
||||||
}
|
}
|
||||||
// Transparency on an inherited (style) fill needs the effective colour.
|
// Transparency on an inherited (style) fill needs the effective colour.
|
||||||
let st = s.doc()?;
|
let inherited = inherited_fills(s, p)?;
|
||||||
let ctx_fill = {
|
edit_group_appearance(s, p, "shape.fill", |sh| apply_fill(sh, new_fill.as_ref(), pic_props, transparency, &inherited))
|
||||||
let ctx = super::text::ctx_for(&st.doc, &st.selection);
|
|
||||||
let mut fills = HashMap::new();
|
|
||||||
for id in targets(s, p)? {
|
|
||||||
if let Some(sh) = st.shape(id) {
|
|
||||||
deckcraft_model::walk(std::slice::from_ref(sh), &mut |sh, _| {
|
|
||||||
let fill = ctx.as_ref().and_then(|c| {
|
|
||||||
let (f, ph) = deckcraft_model::resolve::fill(c, sh);
|
|
||||||
f.map(|f| match f {
|
|
||||||
Fill::Solid { color } => Fill::solid(ColorRef::rgb(c.color(&color, ph))),
|
|
||||||
other => other,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
fills.insert(sh.id, fill);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fills
|
|
||||||
};
|
|
||||||
edit_group_appearance(s, p, "shape.fill", |sh| {
|
|
||||||
if let Some(f) = &new_fill {
|
|
||||||
sh.fill.clone_from(f);
|
|
||||||
}
|
|
||||||
if let (Some(pp), Some(Fill::Picture(pf))) = (pic_props, sh.fill.as_mut()) {
|
|
||||||
picture_props(pf, pp);
|
|
||||||
}
|
|
||||||
if let (Some(t), Some(Fill::Picture(pf))) = (transparency, sh.fill.as_mut()) {
|
|
||||||
let a = 1.0 - t.clamp(0.0, 1.0);
|
|
||||||
pf.alpha = (a < 1.0).then_some(a);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
if let Some(t) = transparency {
|
|
||||||
let base = sh.fill.clone().or_else(|| ctx_fill.get(&sh.id).cloned().flatten());
|
|
||||||
if let Some(Fill::Solid { mut color }) = base {
|
|
||||||
color.mods.retain(|m| !matches!(m, ColorTransform::Alpha(_)));
|
|
||||||
if t > 0.0 {
|
|
||||||
color.mods.push(ColorTransform::Alpha(((1.0 - t.clamp(0.0, 1.0)) * 100_000.0).round() as i32));
|
|
||||||
}
|
|
||||||
sh.fill = Some(Fill::Solid { color });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Applies Format Shape ▸ Picture or texture fill options to a picture fill.
|
/// Applies Format Shape ▸ Picture or texture fill options to a picture fill.
|
||||||
|
|||||||
+44
-31
@@ -204,6 +204,48 @@ impl Dpb {
|
|||||||
entry_lists
|
entry_lists
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// picNumLXNoWrap (8-34, 8-35) of a short-term modification from the predictor `pred`, and the
|
||||||
|
/// picNum it selects.
|
||||||
|
fn short_term_pic_nums(pred: i32, idc: u32, value: u32, curr_pic_num: i32, max_pic_num: i32) -> (i32, i32) {
|
||||||
|
let d = value as i32 + 1;
|
||||||
|
let no_wrap = if idc == 0 {
|
||||||
|
if pred - d < 0 { pred - d + max_pic_num } else { pred - d }
|
||||||
|
} else if pred + d >= max_pic_num {
|
||||||
|
pred + d - max_pic_num
|
||||||
|
} else {
|
||||||
|
pred + d
|
||||||
|
};
|
||||||
|
let pn = if no_wrap > curr_pic_num { no_wrap - max_pic_num } else { no_wrap };
|
||||||
|
(no_wrap, pn)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Puts `target` at `ref_idx` and shifts the rest of the list up, then drops the later entries
|
||||||
|
/// that repeat it (8.2.4.3.1, 8.2.4.3.2) and keeps the list `n` long.
|
||||||
|
fn insert_ref<'a>(list: &mut Vec<Option<&'a DpbEntry>>, ref_idx: usize, n: usize, target: &'a DpbEntry, is_long: bool) {
|
||||||
|
for c in (ref_idx + 1..=n).rev() {
|
||||||
|
list[c] = list[c - 1];
|
||||||
|
}
|
||||||
|
list[ref_idx] = Some(target);
|
||||||
|
let mut nidx = ref_idx + 1;
|
||||||
|
for c in ref_idx + 1..=n {
|
||||||
|
let keep = match list[c] {
|
||||||
|
None => true,
|
||||||
|
Some(e) => {
|
||||||
|
if is_long {
|
||||||
|
!(e.mark == RefMark::Long && e.long_term_frame_idx == target.long_term_frame_idx)
|
||||||
|
} else {
|
||||||
|
!(e.mark == RefMark::Short && std::ptr::eq(e, target))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if keep {
|
||||||
|
list[nidx] = list[c];
|
||||||
|
nidx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list.truncate(n);
|
||||||
|
}
|
||||||
|
|
||||||
/// Reference picture list modification (8.2.4.3) of one list; `n` is num_ref_idx_active.
|
/// Reference picture list modification (8.2.4.3) of one list; `n` is num_ref_idx_active.
|
||||||
fn modify_ref_list<'a>(
|
fn modify_ref_list<'a>(
|
||||||
list: &mut Vec<Option<&'a DpbEntry>>,
|
list: &mut Vec<Option<&'a DpbEntry>>,
|
||||||
@@ -223,16 +265,8 @@ impl Dpb {
|
|||||||
list.push(None); // temporarily one longer
|
list.push(None); // temporarily one longer
|
||||||
let (target, is_long): (Option<&DpbEntry>, bool) = match m.idc {
|
let (target, is_long): (Option<&DpbEntry>, bool) = match m.idc {
|
||||||
0 | 1 => {
|
0 | 1 => {
|
||||||
let d = m.value as i32 + 1;
|
let (no_wrap, pn) = Self::short_term_pic_nums(pred, m.idc, m.value, curr_pic_num, max_pic_num);
|
||||||
let no_wrap = if m.idc == 0 {
|
|
||||||
if pred - d < 0 { pred - d + max_pic_num } else { pred - d }
|
|
||||||
} else if pred + d >= max_pic_num {
|
|
||||||
pred + d - max_pic_num
|
|
||||||
} else {
|
|
||||||
pred + d
|
|
||||||
};
|
|
||||||
pred = no_wrap;
|
pred = no_wrap;
|
||||||
let pn = if no_wrap > curr_pic_num { no_wrap - max_pic_num } else { no_wrap };
|
|
||||||
(short.iter().copied().find(|e| Self::frame_num_wrap(e, cur_fn, max_frame_num) == pn), false)
|
(short.iter().copied().find(|e| Self::frame_num_wrap(e, cur_fn, max_frame_num) == pn), false)
|
||||||
}
|
}
|
||||||
2 => (long.iter().copied().find(|e| e.long_term_frame_idx == m.value), true),
|
2 => (long.iter().copied().find(|e| e.long_term_frame_idx == m.value), true),
|
||||||
@@ -244,29 +278,8 @@ impl Dpb {
|
|||||||
ref_idx += 1;
|
ref_idx += 1;
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
for c in (ref_idx + 1..=n).rev() {
|
Self::insert_ref(list, ref_idx, n, target, is_long);
|
||||||
list[c] = list[c - 1];
|
|
||||||
}
|
|
||||||
list[ref_idx] = Some(target);
|
|
||||||
ref_idx += 1;
|
ref_idx += 1;
|
||||||
let mut nidx = ref_idx;
|
|
||||||
for c in ref_idx..=n {
|
|
||||||
let keep = match list[c] {
|
|
||||||
None => true,
|
|
||||||
Some(e) => {
|
|
||||||
if is_long {
|
|
||||||
!(e.mark == RefMark::Long && e.long_term_frame_idx == target.long_term_frame_idx)
|
|
||||||
} else {
|
|
||||||
!(e.mark == RefMark::Short && std::ptr::eq(e, target))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if keep {
|
|
||||||
list[nidx] = list[c];
|
|
||||||
nidx += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
list.truncate(n);
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-15
@@ -277,6 +277,30 @@ pub struct SliceDecoder<'a> {
|
|||||||
pub ctb_y: i32,
|
pub ctb_y: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Scaling process (8.6.3): dequantises the top-left `max_x` × `max_y` coefficients of the `n` × `n`
|
||||||
|
/// block `coeffs` with scaling matrix `m` (flat when `None`).
|
||||||
|
fn dequantize(coeffs: &mut [i32], n: usize, (max_x, max_y): (usize, usize), m: Option<&[u8]>, scale: i64, round: i64, bd_shift: u32) {
|
||||||
|
for y in 0..=max_y {
|
||||||
|
for x in 0..=max_x {
|
||||||
|
let v = coeffs[y * n + x];
|
||||||
|
if v != 0 {
|
||||||
|
let f = m.map_or(16, |m| m[y * n + x] as i64);
|
||||||
|
coeffs[y * n + x] = ((v as i64 * f * scale + round) >> bd_shift).clamp(-32768, 32767) as i32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds the `n` × `n` residual `coeffs` to the samples of `plane` at (x0, y0), clipped to 0..=`max`.
|
||||||
|
fn add_residual(plane: &mut [u16], stride: usize, (x0, y0): (usize, usize), n: usize, coeffs: &[i32], max: i32) {
|
||||||
|
for y in 0..n {
|
||||||
|
let row = &mut plane[(y0 + y) * stride + x0..][..n];
|
||||||
|
for (x, s) in row.iter_mut().enumerate() {
|
||||||
|
*s = (*s as i32 + coeffs[y * n + x]).clamp(0, max) as u16;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> SliceDecoder<'a> {
|
impl<'a> SliceDecoder<'a> {
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
fn dd(&mut self, ctx: usize) -> u32 {
|
fn dd(&mut self, ctx: usize) -> u32 {
|
||||||
@@ -1331,15 +1355,7 @@ impl<'a> SliceDecoder<'a> {
|
|||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
for y in 0..=max_y {
|
dequantize(coeffs, n, (max_x, max_y), m, scale, round, bd_shift);
|
||||||
for x in 0..=max_x {
|
|
||||||
let v = coeffs[y * n + x];
|
|
||||||
if v != 0 {
|
|
||||||
let f = m.map_or(16, |m| m[y * n + x] as i64);
|
|
||||||
coeffs[y * n + x] = ((v as i64 * f * scale + round) >> bd_shift).clamp(-32768, 32767) as i32;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let bd_shift = 20 - bd;
|
let bd_shift = 20 - bd;
|
||||||
if ts {
|
if ts {
|
||||||
transform::transform_skip(coeffs, n, bd_shift);
|
transform::transform_skip(coeffs, n, bd_shift);
|
||||||
@@ -1355,12 +1371,7 @@ impl<'a> SliceDecoder<'a> {
|
|||||||
let _ = dc_only;
|
let _ = dc_only;
|
||||||
let (stride, max) = (if c == 0 { self.pic.width } else { self.pic.cwidth }, (1i32 << bd) - 1);
|
let (stride, max) = (if c == 0 { self.pic.width } else { self.pic.cwidth }, (1i32 << bd) - 1);
|
||||||
let plane = &mut self.pic.planes[c];
|
let plane = &mut self.pic.planes[c];
|
||||||
for y in 0..n {
|
add_residual(plane, stride, (x0 as usize, y0 as usize), n, coeffs, max);
|
||||||
let row = &mut plane[(y0 as usize + y) * stride + x0 as usize..][..n];
|
|
||||||
for (x, s) in row.iter_mut().enumerate() {
|
|
||||||
*s = (*s as i32 + coeffs[y * n + x]).clamp(0, max) as u16;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+51
-31
@@ -802,6 +802,7 @@ impl Chart {
|
|||||||
/// The (min, max, major unit) of an axis over the data range [lo, hi]: `fmin`/`fmax`/`major`
|
/// The (min, max, major unit) of an axis over the data range [lo, hi]: `fmin`/`fmax`/`major`
|
||||||
/// where the chart fixes them; otherwise Office's automatic limits, rounded out to the smallest
|
/// where the chart fixes them; otherwise Office's automatic limits, rounded out to the smallest
|
||||||
/// 1-2-5 unit giving at most `max_intervals` intervals:
|
/// 1-2-5 unit giving at most `max_intervals` intervals:
|
||||||
|
/// The data range [lo, hi] an axis covers, clamped to ±1e100; (0, 1) without finite values.
|
||||||
///
|
///
|
||||||
/// - **Maximum:** `hi + (hi − lo)/20` (with `headroom`), rounded up to the unit.
|
/// - **Maximum:** `hi + (hi − lo)/20` (with `headroom`), rounded up to the unit.
|
||||||
/// - **Minimum:** zero when every value is ≥ 0 and `lo` is below 5/6 of `hi`; otherwise
|
/// - **Minimum:** zero when every value is ≥ 0 and `lo` is below 5/6 of `hi`; otherwise
|
||||||
@@ -809,22 +810,14 @@ impl Chart {
|
|||||||
/// near them.
|
/// near them.
|
||||||
/// - All-negative values mirror this: the axis ends at zero when `hi` is above 5/6 of `lo`.
|
/// - All-negative values mirror this: the axis ends at zero when `hi` is above 5/6 of `lo`.
|
||||||
/// Mixed signs get headroom at both ends.
|
/// Mixed signs get headroom at both ends.
|
||||||
pub fn axis_scale(
|
fn axis_data_range(lo: f64, hi: f64) -> (f64, f64) {
|
||||||
lo: f64,
|
if lo.is_finite() && hi.is_finite() { (lo.min(hi).clamp(-1e100, 1e100), lo.max(hi).clamp(-1e100, 1e100)) } else { (0.0, 1.0) }
|
||||||
hi: f64,
|
}
|
||||||
fmin: Option<f64>,
|
|
||||||
fmax: Option<f64>,
|
/// Office's automatic (min, max) of the data range [lo, hi]. "How chart axis limits are determined",
|
||||||
major: Option<f64>,
|
/// as Jon Peltier summarises it: 5 % of the data range past the data, and zero when the values don't
|
||||||
max_intervals: usize,
|
/// sit within the top sixth of their largest magnitude. A single value is measured from zero.
|
||||||
headroom: bool,
|
fn auto_limits(lo: f64, hi: f64, headroom: bool) -> (f64, f64) {
|
||||||
) -> (f64, f64, f64) {
|
|
||||||
// Values past ±1e100 aren't axis values; clamping keeps the arithmetic finite.
|
|
||||||
let fin = |v: Option<f64>| v.filter(|v| v.is_finite()).map(|v| v.clamp(-1e100, 1e100));
|
|
||||||
let (fmin, fmax) = (fin(fmin), fin(fmax));
|
|
||||||
let (lo, hi) = if lo.is_finite() && hi.is_finite() { (lo.min(hi).clamp(-1e100, 1e100), lo.max(hi).clamp(-1e100, 1e100)) } else { (0.0, 1.0) };
|
|
||||||
// Office's automatic limits ("How chart axis limits are determined", as Jon Peltier
|
|
||||||
// summarises it): 5 % of the data range past the data, and zero when the values don't
|
|
||||||
// sit within the top sixth of their largest magnitude. A single value is measured from zero.
|
|
||||||
let flat = hi - lo <= 1e-12 * lo.abs().max(hi.abs());
|
let flat = hi - lo <= 1e-12 * lo.abs().max(hi.abs());
|
||||||
let pad = if !headroom {
|
let pad = if !headroom {
|
||||||
0.0
|
0.0
|
||||||
@@ -835,21 +828,23 @@ pub fn axis_scale(
|
|||||||
};
|
};
|
||||||
let auto_lo = if lo >= 0.0 && (flat || lo < hi * 5.0 / 6.0) { 0.0 } else { lo - pad };
|
let auto_lo = if lo >= 0.0 && (flat || lo < hi * 5.0 / 6.0) { 0.0 } else { lo - pad };
|
||||||
let auto_hi = if hi <= 0.0 && (flat || hi > lo * 5.0 / 6.0) { 0.0 } else { hi + pad };
|
let auto_hi = if hi <= 0.0 && (flat || hi > lo * 5.0 / 6.0) { 0.0 } else { hi + pad };
|
||||||
let mut lo = fmin.unwrap_or(auto_lo);
|
(auto_lo, auto_hi)
|
||||||
let mut hi = fmax.unwrap_or(auto_hi);
|
}
|
||||||
let mag = lo.abs().max(hi.abs());
|
|
||||||
// Relative to the data's magnitude, so tiny values don't count as flat; all-zero falls back to 1.
|
/// The ends of a span too small to show: with one fixed limit on the far side of the data, the
|
||||||
let unit = if mag == 0.0 { 1.0 } else { mag };
|
/// automatic end gives way; otherwise the span is one `unit` wide.
|
||||||
if hi - lo <= 1e-12 * mag {
|
fn flat_span_ends(fmin: Option<f64>, fmax: Option<f64>, lo: f64, hi: f64, unit: f64) -> (f64, f64) {
|
||||||
// A fixed limit on the far side of the data: the automatic end gives way.
|
match (fmin, fmax) {
|
||||||
match (fmin, fmax) {
|
(None, Some(_)) => (if hi > 0.0 { 0.0 } else { hi - hi.abs().max(unit) }, hi),
|
||||||
(None, Some(_)) => lo = if hi > 0.0 { 0.0 } else { hi - hi.abs().max(unit) },
|
(Some(_), None) if lo < 0.0 => (lo, 0.0),
|
||||||
(Some(_), None) if lo < 0.0 => hi = 0.0,
|
_ => (lo, lo + unit),
|
||||||
_ => hi = lo + unit,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let n = max_intervals.clamp(1, 50) as f64;
|
}
|
||||||
let step = match fin(major).filter(|m| *m > 0.0 && (hi - lo) / m <= 1000.0) {
|
|
||||||
|
/// The major step: `major` when it gives at most 1000 intervals, else the smallest 1-2-5 step
|
||||||
|
/// that fits `n` intervals over [lo, hi].
|
||||||
|
fn axis_step(major: Option<f64>, lo: f64, hi: f64, n: f64, unit: f64) -> f64 {
|
||||||
|
match major.filter(|m| *m > 0.0 && (hi - lo) / m <= 1000.0) {
|
||||||
Some(m) => m,
|
Some(m) => m,
|
||||||
None => {
|
None => {
|
||||||
let mut step = 10f64.powf(((hi - lo) / n).log10().floor());
|
let mut step = 10f64.powf(((hi - lo) / n).log10().floor());
|
||||||
@@ -865,7 +860,32 @@ pub fn axis_scale(
|
|||||||
}
|
}
|
||||||
step
|
step
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn axis_scale(
|
||||||
|
lo: f64,
|
||||||
|
hi: f64,
|
||||||
|
fmin: Option<f64>,
|
||||||
|
fmax: Option<f64>,
|
||||||
|
major: Option<f64>,
|
||||||
|
max_intervals: usize,
|
||||||
|
headroom: bool,
|
||||||
|
) -> (f64, f64, f64) {
|
||||||
|
// Values past ±1e100 aren't axis values; clamping keeps the arithmetic finite.
|
||||||
|
let fin = |v: Option<f64>| v.filter(|v| v.is_finite()).map(|v| v.clamp(-1e100, 1e100));
|
||||||
|
let (fmin, fmax) = (fin(fmin), fin(fmax));
|
||||||
|
let (lo, hi) = axis_data_range(lo, hi);
|
||||||
|
let (auto_lo, auto_hi) = auto_limits(lo, hi, headroom);
|
||||||
|
let (mut lo, mut hi) = (fmin.unwrap_or(auto_lo), fmax.unwrap_or(auto_hi));
|
||||||
|
let mag = lo.abs().max(hi.abs());
|
||||||
|
// Relative to the data's magnitude, so tiny values don't count as flat; all-zero falls back to 1.
|
||||||
|
let unit = if mag == 0.0 { 1.0 } else { mag };
|
||||||
|
if hi - lo <= 1e-12 * mag {
|
||||||
|
(lo, hi) = flat_span_ends(fmin, fmax, lo, hi, unit);
|
||||||
|
}
|
||||||
|
let n = max_intervals.clamp(1, 50) as f64;
|
||||||
|
let step = axis_step(fin(major), lo, hi, n, unit);
|
||||||
let min = fmin.unwrap_or((lo / step + 1e-9).floor() * step);
|
let min = fmin.unwrap_or((lo / step + 1e-9).floor() * step);
|
||||||
let max = fmax.unwrap_or((hi / step - 1e-9).ceil() * step).max(min + step);
|
let max = fmax.unwrap_or((hi / step - 1e-9).ceil() * step).max(min + step);
|
||||||
(min, max, step)
|
(min, max, step)
|
||||||
|
|||||||
+43
-31
@@ -576,6 +576,44 @@ pub fn text_body(imp: &mut Imp, part: &Part, tx: &El) -> TextBody {
|
|||||||
|
|
||||||
const MAX_PARAGRAPH_RUNS: usize = 10_000;
|
const MAX_PARAGRAPH_RUNS: usize = 10_000;
|
||||||
|
|
||||||
|
/// The run properties of `e`'s `rPr` child, or the defaults.
|
||||||
|
fn run_props(imp: &mut Imp, part: &Part, e: &El) -> RunProps {
|
||||||
|
e.child("rPr").map(|r| rpr(imp, part, r)).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Appends the runs of an `a:r` text, where a vertical tab or line feed is a line break, as it
|
||||||
|
/// lays out and as export writes it.
|
||||||
|
fn push_text_runs(para: &mut Paragraph, props: RunProps, text: &str) {
|
||||||
|
for (i, piece) in text.split(['\u{b}', '\n']).enumerate() {
|
||||||
|
if para.runs.len() >= MAX_PARAGRAPH_RUNS {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if i > 0 {
|
||||||
|
para.runs.push(Run { text: String::new(), props: props.clone(), kind: RunKind::Break });
|
||||||
|
}
|
||||||
|
if !piece.is_empty() {
|
||||||
|
if para.runs.len() >= MAX_PARAGRAPH_RUNS {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
para.runs.push(Run { text: piece.to_string(), props: props.clone(), kind: RunKind::Text });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Appends an Office Math (a14:m) run: its plain text and its OMML.
|
||||||
|
fn push_math_run(para: &mut Paragraph, part: &Part, e: &El) {
|
||||||
|
if para.runs.len() < MAX_PARAGRAPH_RUNS {
|
||||||
|
let mut text = String::new();
|
||||||
|
let mut ts = vec![];
|
||||||
|
e.find_all("t", &mut ts);
|
||||||
|
for t in ts {
|
||||||
|
text.push_str(&t.text());
|
||||||
|
}
|
||||||
|
let omml = e.elements().map(|c| part.keep_inner(c)).collect::<String>();
|
||||||
|
para.runs.push(Run { text, props: RunProps::default(), kind: RunKind::Math { omml } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn paragraph(imp: &mut Imp, part: &Part, p: &El) -> Paragraph {
|
pub fn paragraph(imp: &mut Imp, part: &Part, p: &El) -> Paragraph {
|
||||||
let mut para = Paragraph::default();
|
let mut para = Paragraph::default();
|
||||||
for e in p.elements() {
|
for e in p.elements() {
|
||||||
@@ -589,50 +627,24 @@ pub fn paragraph(imp: &mut Imp, part: &Part, p: &El) -> Paragraph {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"r" => {
|
"r" => {
|
||||||
let props = e.child("rPr").map(|r| rpr(imp, part, r)).unwrap_or_default();
|
let props = run_props(imp, part, e);
|
||||||
let text: String = e.child("t").map(|t| t.text()).unwrap_or_default();
|
let text: String = e.child("t").map(|t| t.text()).unwrap_or_default();
|
||||||
// A vertical tab or line feed inside text is a line break, as it lays out and as export writes it.
|
push_text_runs(&mut para, props, &text);
|
||||||
for (i, piece) in text.split(['\u{b}', '\n']).enumerate() {
|
|
||||||
if para.runs.len() >= MAX_PARAGRAPH_RUNS {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if i > 0 {
|
|
||||||
para.runs.push(Run { text: String::new(), props: props.clone(), kind: RunKind::Break });
|
|
||||||
}
|
|
||||||
if !piece.is_empty() {
|
|
||||||
if para.runs.len() >= MAX_PARAGRAPH_RUNS {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
para.runs.push(Run { text: piece.to_string(), props: props.clone(), kind: RunKind::Text });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"br" => {
|
"br" => {
|
||||||
if para.runs.len() < MAX_PARAGRAPH_RUNS {
|
if para.runs.len() < MAX_PARAGRAPH_RUNS {
|
||||||
let props = e.child("rPr").map(|r| rpr(imp, part, r)).unwrap_or_default();
|
let props = run_props(imp, part, e);
|
||||||
para.runs.push(Run { text: String::new(), props, kind: RunKind::Break });
|
para.runs.push(Run { text: String::new(), props, kind: RunKind::Break });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"fld" => {
|
"fld" => {
|
||||||
if para.runs.len() < MAX_PARAGRAPH_RUNS {
|
if para.runs.len() < MAX_PARAGRAPH_RUNS {
|
||||||
let props = e.child("rPr").map(|r| rpr(imp, part, r)).unwrap_or_default();
|
let props = run_props(imp, part, e);
|
||||||
let text = e.child("t").map(|t| t.text()).unwrap_or_default();
|
let text = e.child("t").map(|t| t.text()).unwrap_or_default();
|
||||||
para.runs.push(Run { text, props, kind: RunKind::Field { field: e.attr("type").unwrap_or("").to_string() } });
|
para.runs.push(Run { text, props, kind: RunKind::Field { field: e.attr("type").unwrap_or("").to_string() } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"m" => {
|
"m" => push_math_run(&mut para, part, e),
|
||||||
if para.runs.len() < MAX_PARAGRAPH_RUNS {
|
|
||||||
// Office Math (a14:m) — keep the OMML and its plain text.
|
|
||||||
let mut text = String::new();
|
|
||||||
let mut ts = vec![];
|
|
||||||
e.find_all("t", &mut ts);
|
|
||||||
for t in ts {
|
|
||||||
text.push_str(&t.text());
|
|
||||||
}
|
|
||||||
let omml = e.elements().map(|c| part.keep_inner(c)).collect::<String>();
|
|
||||||
para.runs.push(Run { text, props: RunProps::default(), kind: RunKind::Math { omml } });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"endParaRPr" => para.end_props = rpr(imp, part, e),
|
"endParaRPr" => para.end_props = rpr(imp, part, e),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user