pub mod definitions;
pub mod formulas;
pub mod path;
pub mod text_warp_definitions;
pub use definitions::PresetDef;
pub use formulas::{FormulaOp, GuideDef, GuideEnv};
pub use path::{PathCommandDef, PathDef, PathFill, ResolvedCommand, resolve_custom_path};
pub struct EvaluatedPath {
pub commands: Vec<ResolvedCommand>,
pub fill: PathFill,
pub stroke: bool,
}
pub struct EvaluatedShape {
pub paths: Vec<EvaluatedPath>,
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: &crate::model::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));
}
}