pub mod definitions;
pub mod formulas;
pub mod path;
pub mod text_warp_definitions;
use crate::model::CustomGeometry;
pub use definitions::PresetDef;
pub use formulas::{FormulaOp, GuideEnv};
pub use path::{PathFill, ResolvedCommand, resolve_custom_path};
pub struct EvaluatedPath {
pub commands: Vec<ResolvedCommand>,
#[allow(dead_code)]
pub fill: PathFill,
pub stroke: bool,
}
pub struct EvaluatedShape {
pub paths: Vec<EvaluatedPath>,
#[allow(dead_code)]
pub text_rect: Option<(f64, f64, f64, f64)>,
}
const EMU_SCALE: f64 = 12700.0;
fn scaled_env(w: f64, h: f64) -> (GuideEnv, i64, i64) {
let wi = (w * EMU_SCALE) as i64;
let hi = (h * EMU_SCALE) as i64;
(GuideEnv::new(wi, hi), wi, hi)
}
pub fn evaluate_preset(
name: &str,
w: f64,
h: f64,
adj_overrides: &[(String, i64)],
) -> Option<EvaluatedShape> {
let def = definitions::lookup(name)?;
Some(evaluate_def(def, w, h, adj_overrides))
}
pub fn evaluate_def(
def: &PresetDef,
w: f64,
h: f64,
adj_overrides: &[(String, i64)],
) -> EvaluatedShape {
let (mut env, wi, hi) = scaled_env(w, h);
env.set_adjustments(def.adjust_defaults, adj_overrides);
env.evaluate_guides(def.guides);
let paths = def
.paths
.iter()
.map(|p| EvaluatedPath {
commands: path::resolve_path(p, &env, w, h, wi, hi),
fill: p.fill,
stroke: p.stroke,
})
.collect();
let text_rect = def.text_rect.as_ref().map(|tr| {
let l = env.resolve(tr.l) as f64 / EMU_SCALE;
let t = env.resolve(tr.t) as f64 / EMU_SCALE;
let r = env.resolve(tr.r) as f64 / EMU_SCALE;
let b = env.resolve(tr.b) as f64 / EMU_SCALE;
(l, h - b, r - l, b - t)
});
EvaluatedShape { paths, text_rect }
}
pub fn evaluate_custom(
custom: &CustomGeometry,
w: f64,
h: f64,
adj_overrides: &[(String, i64)],
) -> EvaluatedShape {
let (mut env, wi, hi) = scaled_env(w, h);
env.set_adjustments(&[], &custom.adjust_defaults);
env.set_adjustments(&[], adj_overrides);
env.evaluate_custom_guides(&custom.guides);
let paths = custom
.paths
.iter()
.map(|p| EvaluatedPath {
commands: resolve_custom_path(p, &env, w, h, wi, hi),
fill: p.fill,
stroke: p.stroke,
})
.collect();
EvaluatedShape {
paths,
text_rect: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_evaluate_preset_unknown() {
assert!(evaluate_preset("nonexistent_shape", 100.0, 50.0, &[]).is_none());
}
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
#[test]
fn test_rect_matches_handcoded() {
let w = 100.0;
let h = 50.0;
let shape = evaluate_preset("rect", w, h, &[]).unwrap();
let cmds = &shape.paths[0].commands;
assert_eq!(cmds.len(), 5);
assert!(
matches!(&cmds[0], ResolvedCommand::MoveTo(x, y) if approx_eq(*x, 0.0, 0.01) && approx_eq(*y, h, 0.01))
);
assert!(
matches!(&cmds[1], ResolvedCommand::LineTo(x, y) if approx_eq(*x, w, 0.01) && approx_eq(*y, h, 0.01))
);
assert!(
matches!(&cmds[2], ResolvedCommand::LineTo(x, y) if approx_eq(*x, w, 0.01) && approx_eq(*y, 0.0, 0.01))
);
assert!(
matches!(&cmds[3], ResolvedCommand::LineTo(x, y) if approx_eq(*x, 0.0, 0.01) && approx_eq(*y, 0.0, 0.01))
);
assert!(matches!(&cmds[4], ResolvedCommand::Close));
}
#[test]
fn test_ellipse_matches_handcoded() {
let w = 100.0;
let h = 80.0;
let shape = evaluate_preset("ellipse", w, h, &[]).unwrap();
let cmds = &shape.paths[0].commands;
assert!(
matches!(&cmds[0], ResolvedCommand::MoveTo(x, y) if approx_eq(*x, 0.0, 0.1) && approx_eq(*y, h / 2.0, 0.1))
);
let endpoints: Vec<(f64, f64)> = cmds
.iter()
.filter_map(|c| match c {
ResolvedCommand::CubicTo { x, y, .. } => Some((*x, *y)),
_ => None,
})
.collect();
assert_eq!(
endpoints.len(),
4,
"Expected 4 cubic bezier segments for 4 quarter-arcs"
);
let cardinal_points = [
(w / 2.0, h), (w, h / 2.0), (w / 2.0, 0.0), (0.0, h / 2.0), ];
for (i, (ex, ey)) in cardinal_points.iter().enumerate() {
let (gx, gy) = endpoints[i];
assert!(
approx_eq(gx, *ex, 0.5) && approx_eq(gy, *ey, 0.5),
"Arc {i} endpoint ({gx:.2}, {gy:.2}) != expected ({ex:.2}, {ey:.2})"
);
}
let k = 0.5522847498_f64;
let rx = w / 2.0;
let ry = h / 2.0;
if let ResolvedCommand::CubicTo { x1, y1, x2, y2, .. } = &cmds[1] {
let expected_cp1x = 0.0;
let expected_cp1y = h / 2.0 + k * ry;
let expected_cp2x = w / 2.0 - k * rx;
let expected_cp2y = h;
assert!(
approx_eq(*x1, expected_cp1x, 0.5) && approx_eq(*y1, expected_cp1y, 0.5),
"CP1 ({x1:.2}, {y1:.2}) != expected ({expected_cp1x:.2}, {expected_cp1y:.2})"
);
assert!(
approx_eq(*x2, expected_cp2x, 0.5) && approx_eq(*y2, expected_cp2y, 0.5),
"CP2 ({x2:.2}, {y2:.2}) != expected ({expected_cp2x:.2}, {expected_cp2y:.2})"
);
} else {
panic!("Expected CubicTo as first arc segment");
}
}
#[test]
fn test_notched_right_arrow_matches_handcoded() {
let w = 200.0;
let h = 100.0;
let shape = evaluate_preset("notchedRightArrow", w, h, &[]).unwrap();
let cmds = &shape.paths[0].commands;
let expected = [
(0.0, 75.0), (150.0, 75.0), (150.0, 100.0), (200.0, 50.0), (150.0, 0.0), (150.0, 25.0), (0.0, 25.0), (25.0, 50.0), ];
assert_eq!(
cmds.len(),
9,
"Expected 9 commands: MoveTo + 7 LineTo + Close"
);
assert!(
matches!(&cmds[0], ResolvedCommand::MoveTo(x, y) if approx_eq(*x, expected[0].0, 0.01) && approx_eq(*y, expected[0].1, 0.01)),
"MoveTo mismatch: got {:?}, expected {:?}",
cmds[0],
expected[0]
);
for i in 1..8 {
assert!(
matches!(&cmds[i], ResolvedCommand::LineTo(x, y) if approx_eq(*x, expected[i].0, 0.01) && approx_eq(*y, expected[i].1, 0.01)),
"LineTo {i} mismatch: got {:?}, expected {:?}",
cmds[i],
expected[i]
);
}
assert!(matches!(&cmds[8], ResolvedCommand::Close));
}
#[test]
fn test_circular_arrow_crescent_not_blob() {
let adjs = vec![
("adj1".into(), 4688_i64),
("adj2".into(), 299029),
("adj3".into(), 2486671),
("adj4".into(), 15926341),
("adj5".into(), 5469),
];
let w = 177.4;
let h = 177.4;
let shape = evaluate_preset("circularArrow", w, h, &adjs).unwrap();
let cmds = &shape.paths[0].commands;
let cubic_count = cmds
.iter()
.filter(|c| matches!(c, ResolvedCommand::CubicTo { .. }))
.count();
assert!(
cubic_count >= 4,
"Expected at least 4 cubic segments from two arcs, got {cubic_count}"
);
let line_pts: Vec<(f64, f64)> = cmds
.iter()
.filter_map(|c| match c {
ResolvedCommand::LineTo(x, y) => Some((*x, *y)),
_ => None,
})
.collect();
assert_eq!(line_pts.len(), 4, "Expected 4 LineTo commands for arrowhead");
let (mut x_min, mut x_max) = (f64::INFINITY, f64::NEG_INFINITY);
let (mut y_min, mut y_max) = (f64::INFINITY, f64::NEG_INFINITY);
for &(x, y) in &line_pts {
x_min = x_min.min(x);
x_max = x_max.max(x);
y_min = y_min.min(y);
y_max = y_max.max(y);
}
let x_span = x_max - x_min;
let y_span = y_max - y_min;
assert!(
x_span < w * 0.3,
"Arrowhead x-span {x_span:.1} is too large (> 30% of {w})"
);
assert!(
y_span < h * 0.3,
"Arrowhead y-span {y_span:.1} is too large (> 30% of {h})"
);
}
}