use crate::model::dimension::Dimension;
use crate::model::{AdjAngle, AdjCoord, AdjPoint, CustomGeometry, PathCommand, PathDef};
use crate::render::dimension::Pt;
use crate::render::geometry::{PtOffset, PtRect, PtSize};
use super::guides::{evaluate_guides, resolve_adj_angle, resolve_adj_coord, GuideContext};
use super::{PathVerb, ShapePath, SubPath};
pub fn build_custom(geom: &CustomGeometry, extent: PtSize) -> Option<ShapePath> {
if geom.paths.is_empty() {
return None;
}
let paths: Vec<SubPath> = geom
.paths
.iter()
.filter_map(|p| build_subpath(geom, p, extent))
.collect();
if paths.is_empty() {
return None;
}
let text_rect = geom
.rect
.as_ref()
.and_then(|r| resolve_text_rect(geom, &geom.paths[0], r, extent));
Some(ShapePath { paths, text_rect })
}
fn build_subpath(geom: &CustomGeometry, path: &PathDef, extent: PtSize) -> Option<SubPath> {
let path_w = path.w.raw() as f64;
let path_h = path.h.raw() as f64;
if path_w <= 0.0 || path_h <= 0.0 {
return None;
}
let ctx = GuideContext::new(path_w, path_h);
let mut values = evaluate_guides(&geom.av_list, ctx);
let computed = evaluate_guides(&geom.gd_list, ctx);
values.extend(computed);
let sx = extent.width.raw() / path_w as f32;
let sy = extent.height.raw() / path_h as f32;
let mut verbs = Vec::with_capacity(path.commands.len());
for cmd in &path.commands {
match cmd {
PathCommand::MoveTo(p) => {
verbs.push(PathVerb::MoveTo(point_to_pt(p, &values, ctx, sx, sy)))
}
PathCommand::LineTo(p) => {
verbs.push(PathVerb::LineTo(point_to_pt(p, &values, ctx, sx, sy)))
}
PathCommand::QuadBezTo(c, p) => verbs.push(PathVerb::QuadTo(
point_to_pt(c, &values, ctx, sx, sy),
point_to_pt(p, &values, ctx, sx, sy),
)),
PathCommand::CubicBezTo(c1, c2, p) => verbs.push(PathVerb::CubicTo(
point_to_pt(c1, &values, ctx, sx, sy),
point_to_pt(c2, &values, ctx, sx, sy),
point_to_pt(p, &values, ctx, sx, sy),
)),
PathCommand::ArcTo {
wr,
hr,
start_angle,
swing_angle,
} => {
let wr_pt = Pt::new((coord_val(wr, &values, ctx) as f32) * sx);
let hr_pt = Pt::new((coord_val(hr, &values, ctx) as f32) * sy);
verbs.push(PathVerb::ArcTo {
radii: PtSize::new(wr_pt, hr_pt),
start_angle: angle(start_angle, &values, ctx),
swing_angle: angle(swing_angle, &values, ctx),
});
}
PathCommand::Close => verbs.push(PathVerb::Close),
}
}
Some(SubPath {
verbs,
fill_mode: path.fill,
stroked: path.stroke,
})
}
fn resolve_text_rect(
geom: &CustomGeometry,
path: &PathDef,
rect: &crate::model::TextRect,
extent: PtSize,
) -> Option<PtRect> {
let path_w = path.w.raw() as f64;
let path_h = path.h.raw() as f64;
if path_w <= 0.0 || path_h <= 0.0 {
return None;
}
let ctx = GuideContext::new(path_w, path_h);
let mut values = evaluate_guides(&geom.av_list, ctx);
values.extend(evaluate_guides(&geom.gd_list, ctx));
let sx = extent.width.raw() / path_w as f32;
let sy = extent.height.raw() / path_h as f32;
let l = (coord_val(&rect.left, &values, ctx) as f32) * sx;
let t = (coord_val(&rect.top, &values, ctx) as f32) * sy;
let r = (coord_val(&rect.right, &values, ctx) as f32) * sx;
let b = (coord_val(&rect.bottom, &values, ctx) as f32) * sy;
Some(PtRect::from_xywh(
Pt::new(l),
Pt::new(t),
Pt::new((r - l).max(0.0)),
Pt::new((b - t).max(0.0)),
))
}
fn point_to_pt(
p: &AdjPoint,
values: &super::guides::GuideValues,
ctx: GuideContext,
sx: f32,
sy: f32,
) -> PtOffset {
let x = (resolve_adj_coord(&p.x, values, ctx) as f32) * sx;
let y = (resolve_adj_coord(&p.y, values, ctx) as f32) * sy;
PtOffset::new(Pt::new(x), Pt::new(y))
}
fn coord_val(c: &AdjCoord, values: &super::guides::GuideValues, ctx: GuideContext) -> f64 {
resolve_adj_coord(c, values, ctx)
}
fn angle(
a: &AdjAngle,
values: &super::guides::GuideValues,
ctx: GuideContext,
) -> Dimension<crate::model::dimension::SixtieThousandthDeg> {
Dimension::new(resolve_adj_angle(a, values, ctx) as i64)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{GeomGuide, PathDef, PathFillMode};
fn lit_point(x: i64, y: i64) -> AdjPoint {
AdjPoint {
x: AdjCoord::Lit(x),
y: AdjCoord::Lit(y),
}
}
fn emu(v: i64) -> Dimension<crate::model::dimension::Emu> {
Dimension::new(v)
}
fn rect_path() -> PathDef {
PathDef {
w: emu(100),
h: emu(50),
fill: PathFillMode::Norm,
stroke: true,
extrusion_ok: true,
commands: vec![
PathCommand::MoveTo(lit_point(0, 0)),
PathCommand::LineTo(lit_point(100, 0)),
PathCommand::LineTo(lit_point(100, 50)),
PathCommand::LineTo(lit_point(0, 50)),
PathCommand::Close,
],
}
}
#[test]
fn scales_path_coords_to_shape_extent() {
let geom = CustomGeometry {
paths: vec![rect_path()],
..Default::default()
};
let extent = PtSize::new(Pt::new(200.0), Pt::new(100.0));
let s = build_custom(&geom, extent).unwrap();
let sub = &s.paths[0];
let PathVerb::LineTo(p) = sub.verbs[1] else {
panic!()
};
assert_eq!(p.x, Pt::new(200.0));
assert_eq!(p.y, Pt::new(0.0));
let PathVerb::LineTo(p) = sub.verbs[3] else {
panic!()
};
assert_eq!(p.x, Pt::new(0.0));
assert_eq!(p.y, Pt::new(100.0));
}
#[test]
fn empty_paths_return_none() {
let geom = CustomGeometry::default();
assert!(build_custom(&geom, PtSize::new(Pt::new(10.0), Pt::new(10.0))).is_none());
}
#[test]
fn zero_width_path_is_skipped() {
let mut path = rect_path();
path.w = emu(0);
let geom = CustomGeometry {
paths: vec![path],
..Default::default()
};
assert!(build_custom(&geom, PtSize::new(Pt::new(10.0), Pt::new(10.0))).is_none());
}
#[test]
fn guide_reference_resolved_via_gd_list() {
let geom = CustomGeometry {
gd_list: vec![GeomGuide {
name: "mid".into(),
formula: "*/ w 1 2".into(),
}],
paths: vec![PathDef {
w: emu(100),
h: emu(50),
fill: PathFillMode::Norm,
stroke: true,
extrusion_ok: true,
commands: vec![
PathCommand::MoveTo(AdjPoint {
x: AdjCoord::Guide("mid".into()),
y: AdjCoord::Lit(0),
}),
PathCommand::LineTo(AdjPoint {
x: AdjCoord::Guide("mid".into()),
y: AdjCoord::Guide("h".into()),
}),
],
}],
..Default::default()
};
let extent = PtSize::new(Pt::new(200.0), Pt::new(100.0));
let s = build_custom(&geom, extent).unwrap();
let PathVerb::MoveTo(p) = s.paths[0].verbs[0] else {
panic!()
};
assert_eq!(p.x, Pt::new(100.0));
let PathVerb::LineTo(p) = s.paths[0].verbs[1] else {
panic!()
};
assert_eq!(p.y, Pt::new(100.0));
}
#[test]
fn arc_to_preserves_angle_in_60k_deg() {
let geom = CustomGeometry {
paths: vec![PathDef {
w: emu(100),
h: emu(100),
fill: PathFillMode::Norm,
stroke: true,
extrusion_ok: true,
commands: vec![PathCommand::ArcTo {
wr: AdjCoord::Lit(25),
hr: AdjCoord::Lit(25),
start_angle: AdjCoord::Lit(0),
swing_angle: AdjCoord::Lit(5_400_000),
}],
}],
..Default::default()
};
let extent = PtSize::new(Pt::new(100.0), Pt::new(100.0));
let s = build_custom(&geom, extent).unwrap();
let PathVerb::ArcTo {
start_angle,
swing_angle,
..
} = s.paths[0].verbs[0]
else {
panic!()
};
assert_eq!(start_angle.raw(), 0);
assert_eq!(swing_angle.raw(), 5_400_000);
}
#[test]
fn text_rect_resolves_and_scales() {
let geom = CustomGeometry {
rect: Some(crate::model::TextRect {
left: AdjCoord::Lit(0),
top: AdjCoord::Lit(0),
right: AdjCoord::Lit(100),
bottom: AdjCoord::Lit(50),
}),
paths: vec![rect_path()],
..Default::default()
};
let s = build_custom(&geom, PtSize::new(Pt::new(200.0), Pt::new(100.0))).unwrap();
let tr = s.text_rect.unwrap();
assert_eq!(tr.origin.x, Pt::ZERO);
assert_eq!(tr.size.width, Pt::new(200.0));
assert_eq!(tr.size.height, Pt::new(100.0));
}
}