use crate::types::{BoxIn, Length as Inches, OffsetIn, Point, Scaler, Size, UnitVec};
use facet_svg::{Circle as SvgCircle, Ellipse as SvgEllipse, Path, PathData, SvgNode};
use glam::DVec2;
use super::defaults;
use super::{TextVSlot, compute_text_vslots, sum_text_heights_above_below};
pub type BoundingBox = BoxIn;
use super::geometry::{
arc_control_point, chop_line, create_arc_path_with_control, create_cylinder_paths_with_rad,
create_file_paths, create_line_path, create_oval_path, create_rounded_box_path,
create_spline_path,
};
use super::svg::{color_to_rgb, color_to_string, render_arrowhead_dom};
use super::types::{ClassName, ObjectStyle, PointIn, PositionedText, RenderedObject};
use enum_dispatch::enum_dispatch;
pub struct ShapeRenderContext<'a> {
pub scaler: &'a Scaler,
pub offset_x: Inches,
pub max_y: Inches,
pub dashwid: Inches,
pub arrow_len: Inches,
pub arrow_wid: Inches,
pub thickness: Inches,
pub use_css_vars: bool,
}
fn chop_point(from: DVec2, to: DVec2, amount: f64) -> DVec2 {
let delta = to - from;
let dist = delta.length();
if dist <= amount {
return from;
}
let r = 1.0 - amount / dist;
from + delta * r
}
fn chop_waypoint_start(waypoints: &mut [PointIn], amount: Inches) {
if waypoints.len() < 2 {
return;
}
let from = waypoints[1];
let to = waypoints[0];
let delta = to - from;
let dist = (delta.dx.0 * delta.dx.0 + delta.dy.0 * delta.dy.0).sqrt();
if dist <= amount.0 {
waypoints[0] = from;
return;
}
let r = 1.0 - amount.0 / dist;
waypoints[0] = Point::new(
Inches(from.x.0 + r * delta.dx.0),
Inches(from.y.0 + r * delta.dy.0),
);
}
fn chop_waypoint_end(waypoints: &mut [PointIn], amount: Inches) {
if waypoints.len() < 2 {
return;
}
let n = waypoints.len();
let from = waypoints[n - 2];
let to = waypoints[n - 1];
let delta = to - from;
let dist = (delta.dx.0 * delta.dx.0 + delta.dy.0 * delta.dy.0).sqrt();
if dist <= amount.0 {
waypoints[n - 1] = from;
return;
}
let r = 1.0 - amount.0 / dist;
waypoints[n - 1] = Point::new(
Inches(from.x.0 + r * delta.dx.0),
Inches(from.y.0 + r * delta.dy.0),
);
}
#[enum_dispatch]
pub trait Shape {
fn center(&self) -> PointIn;
fn width(&self) -> Inches;
fn height(&self) -> Inches;
fn style(&self) -> &ObjectStyle;
fn text(&self) -> &[PositionedText];
fn is_round(&self) -> bool {
false
}
fn edge_point(&self, direction: EdgeDirection) -> PointIn {
match direction {
EdgeDirection::Center => return self.center(),
EdgeDirection::Start => return self.start(),
EdgeDirection::End => return self.end(),
_ => {}
}
let center = self.center();
let hw = self.width() / 2.0;
let hh = self.height() / 2.0;
let is_diagonal = matches!(
direction,
EdgeDirection::NorthEast
| EdgeDirection::NorthWest
| EdgeDirection::SouthEast
| EdgeDirection::SouthWest
);
let diag = if is_diagonal {
if self.is_round() {
1.0
} else {
std::f64::consts::SQRT_2
}
} else {
1.0
};
let offset = direction.unit_vec().scale_xy(hw * diag, hh * diag);
center + offset
}
fn start(&self) -> PointIn {
self.edge_point(EdgeDirection::West)
}
fn end(&self) -> PointIn {
self.edge_point(EdgeDirection::East)
}
fn render_svg(&self, obj: &RenderedObject, ctx: &ShapeRenderContext) -> Vec<SvgNode>;
fn waypoints(&self) -> Option<&[PointIn]> {
None
}
fn translate(&mut self, offset: OffsetIn);
fn expand_core_bounds(&self, bounds: &mut BoundingBox) {
self.expand_bounds(bounds);
}
#[allow(unused_variables)]
fn expand_bounds(&self, bounds: &mut BoundingBox) {
let style = self.style();
let text = self.text();
let center = self.center();
let old_min_x = bounds.min.x.0;
if style.invisible && !text.is_empty() {
let charht = defaults::FONT_SIZE;
let charwid = defaults::CHARWID;
for t in text {
let text_w = Inches(t.width_inches(charwid));
let hh = Inches(t.height(charht)) / 2.0;
let hw = text_w / 2.0;
bounds.expand_point(Point::new(center.x - hw, center.y - hh));
bounds.expand_point(Point::new(center.x + hw, center.y + hh));
}
} else if !style.invisible {
bounds.expand_rect(
center,
Size {
w: self.width(),
h: self.height(),
},
);
if !text.is_empty() {
let charht = defaults::FONT_SIZE;
let charwid = defaults::CHARWID;
let (text_above, text_below) = sum_text_heights_above_below(text, charht);
let max_hw = text
.iter()
.map(|t| Inches(t.width_inches(charwid)) / 2.0)
.fold(Inches::ZERO, |acc, hw| if hw > acc { hw } else { acc });
bounds.expand_point(Point::new(center.x - max_hw, center.y - Inches(text_below)));
bounds.expand_point(Point::new(center.x + max_hw, center.y + Inches(text_above)));
}
}
crate::log::debug!(
old_min_x,
new_min_x = bounds.min.x.0,
center_x = center.x.0,
width = self.width().0,
height = self.height().0,
invisible = style.invisible,
"[BBOX]"
);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EdgeDirection {
North,
NorthEast,
East,
SouthEast,
South,
SouthWest,
West,
NorthWest,
Center,
Start,
End,
}
impl EdgeDirection {
pub fn unit_vec(self) -> UnitVec {
match self {
EdgeDirection::North => UnitVec::NORTH,
EdgeDirection::South => UnitVec::SOUTH,
EdgeDirection::East => UnitVec::EAST,
EdgeDirection::West => UnitVec::WEST,
EdgeDirection::NorthEast => UnitVec::NORTH_EAST,
EdgeDirection::NorthWest => UnitVec::NORTH_WEST,
EdgeDirection::SouthEast => UnitVec::SOUTH_EAST,
EdgeDirection::SouthWest => UnitVec::SOUTH_WEST,
EdgeDirection::Center | EdgeDirection::Start | EdgeDirection::End => UnitVec::ZERO,
}
}
}
#[derive(Debug, Clone)]
pub struct CircleShape {
pub center: PointIn,
pub radius: Inches,
pub style: ObjectStyle,
pub text: Vec<PositionedText>,
}
impl CircleShape {
pub fn new(center: PointIn, radius: Inches) -> Self {
Self {
center,
radius,
style: ObjectStyle::default(),
text: Vec::new(),
}
}
pub fn with_style(mut self, style: ObjectStyle) -> Self {
self.style = style;
self
}
pub fn with_text(mut self, text: Vec<PositionedText>) -> Self {
self.text = text;
self
}
}
impl Shape for CircleShape {
fn center(&self) -> PointIn {
self.center
}
fn width(&self) -> Inches {
self.radius * 2.0
}
fn height(&self) -> Inches {
self.radius * 2.0
}
fn style(&self) -> &ObjectStyle {
&self.style
}
fn text(&self) -> &[PositionedText] {
&self.text
}
fn is_round(&self) -> bool {
true
}
fn render_svg(&self, _obj: &RenderedObject, ctx: &ShapeRenderContext) -> Vec<SvgNode> {
let mut nodes = Vec::new();
if self.style.invisible || self.style.stroke_width.0 < 0.0 {
return nodes;
}
let center_svg = self.center.to_svg(ctx.scaler, ctx.offset_x, ctx.max_y);
let r = ctx.scaler.px(self.radius);
let svg_style = build_svg_style(&self.style, ctx.scaler, ctx.dashwid, ctx.use_css_vars);
let circle = SvgCircle {
cx: Some(center_svg.x),
cy: Some(center_svg.y),
r: Some(r),
fill: None,
stroke: None,
stroke_width: None,
stroke_dasharray: None,
style: Some(svg_style),
};
nodes.push(SvgNode::Circle(circle));
nodes
}
fn translate(&mut self, offset: OffsetIn) {
self.center += offset;
}
}
#[derive(Debug, Clone)]
pub struct BoxShape {
pub center: PointIn,
pub width: Inches,
pub height: Inches,
pub corner_radius: Inches,
pub style: ObjectStyle,
pub text: Vec<PositionedText>,
}
impl BoxShape {
pub fn new(center: PointIn, width: Inches, height: Inches) -> Self {
Self {
center,
width,
height,
corner_radius: Inches::ZERO,
style: ObjectStyle::default(),
text: Vec::new(),
}
}
}
impl Shape for BoxShape {
fn center(&self) -> PointIn {
self.center
}
fn width(&self) -> Inches {
self.width
}
fn height(&self) -> Inches {
self.height
}
fn style(&self) -> &ObjectStyle {
&self.style
}
fn text(&self) -> &[PositionedText] {
&self.text
}
fn edge_point(&self, direction: EdgeDirection) -> PointIn {
match direction {
EdgeDirection::Center => return self.center,
EdgeDirection::Start => return self.edge_point(EdgeDirection::West),
EdgeDirection::End => return self.edge_point(EdgeDirection::East),
_ => {}
}
let hw = self.width / 2.0;
let hh = self.height / 2.0;
let is_diagonal = matches!(
direction,
EdgeDirection::NorthEast
| EdgeDirection::NorthWest
| EdgeDirection::SouthEast
| EdgeDirection::SouthWest
);
let (offset_x, offset_y) = if is_diagonal && self.corner_radius > Inches::ZERO {
let rad = self.corner_radius.min(hw).min(hh);
let rx = Inches(0.292_893_218_813_452_54 * rad.0);
let (sign_x, sign_y) = match direction {
EdgeDirection::NorthEast => (1.0, 1.0), EdgeDirection::NorthWest => (-1.0, 1.0), EdgeDirection::SouthEast => (1.0, -1.0), EdgeDirection::SouthWest => (-1.0, -1.0), _ => unreachable!(),
};
(
Inches(sign_x * (hw.0 - rx.0)),
Inches(sign_y * (hh.0 - rx.0)),
)
} else if is_diagonal {
let (sign_x, sign_y) = match direction {
EdgeDirection::NorthEast => (1.0, 1.0),
EdgeDirection::NorthWest => (-1.0, 1.0),
EdgeDirection::SouthEast => (1.0, -1.0),
EdgeDirection::SouthWest => (-1.0, -1.0),
_ => unreachable!(),
};
(Inches(sign_x * hw.0), Inches(sign_y * hh.0))
} else {
let unit = direction.unit_vec();
(hw * unit.dx(), hh * unit.dy())
};
self.center + OffsetIn::new(offset_x, offset_y)
}
fn render_svg(&self, _obj: &RenderedObject, ctx: &ShapeRenderContext) -> Vec<SvgNode> {
let mut nodes = Vec::new();
if self.style.invisible || self.style.stroke_width.0 < 0.0 {
return nodes;
}
let center_svg = self.center.to_svg(ctx.scaler, ctx.offset_x, ctx.max_y);
let hw = ctx.scaler.px(self.width / 2.0);
let hh = ctx.scaler.px(self.height / 2.0);
let x1 = center_svg.x - hw;
let x2 = center_svg.x + hw;
let y1 = center_svg.y - hh;
let y2 = center_svg.y + hh;
let svg_style = build_svg_style(&self.style, ctx.scaler, ctx.dashwid, ctx.use_css_vars);
let path_data = if self.corner_radius > Inches::ZERO {
let r = ctx.scaler.px(self.corner_radius);
create_rounded_box_path(x1, y1, x2, y2, r)
} else {
PathData::new().m(x1, y2).l(x2, y2).l(x2, y1).l(x1, y1).z()
};
let path = Path {
d: Some(path_data),
fill: None,
stroke: None,
stroke_width: None,
stroke_dasharray: None,
style: Some(svg_style),
};
nodes.push(SvgNode::Path(path));
nodes
}
fn translate(&mut self, offset: OffsetIn) {
self.center += offset;
}
}
#[derive(Debug, Clone)]
pub struct EllipseShape {
pub center: PointIn,
pub width: Inches,
pub height: Inches,
pub style: ObjectStyle,
pub text: Vec<PositionedText>,
}
impl Shape for EllipseShape {
fn center(&self) -> PointIn {
self.center
}
fn width(&self) -> Inches {
self.width
}
fn height(&self) -> Inches {
self.height
}
fn style(&self) -> &ObjectStyle {
&self.style
}
fn text(&self) -> &[PositionedText] {
&self.text
}
fn is_round(&self) -> bool {
true
}
fn render_svg(&self, _obj: &RenderedObject, ctx: &ShapeRenderContext) -> Vec<SvgNode> {
let mut nodes = Vec::new();
if self.style.invisible || self.style.stroke_width.0 < 0.0 {
return nodes;
}
let center_svg = self.center.to_svg(ctx.scaler, ctx.offset_x, ctx.max_y);
let rx = ctx.scaler.px(self.width / 2.0);
let ry = ctx.scaler.px(self.height / 2.0);
let svg_style = build_svg_style(&self.style, ctx.scaler, ctx.dashwid, ctx.use_css_vars);
let ellipse = SvgEllipse {
cx: Some(center_svg.x),
cy: Some(center_svg.y),
rx: Some(rx),
ry: Some(ry),
fill: None,
stroke: None,
stroke_width: None,
stroke_dasharray: None,
style: Some(svg_style),
};
nodes.push(SvgNode::Ellipse(ellipse));
nodes
}
fn translate(&mut self, offset: OffsetIn) {
self.center += offset;
}
}
#[derive(Debug, Clone)]
pub struct OvalShape {
pub center: PointIn,
pub width: Inches,
pub height: Inches,
pub style: ObjectStyle,
pub text: Vec<PositionedText>,
}
impl Shape for OvalShape {
fn center(&self) -> PointIn {
self.center
}
fn width(&self) -> Inches {
self.width
}
fn height(&self) -> Inches {
self.height
}
fn style(&self) -> &ObjectStyle {
&self.style
}
fn text(&self) -> &[PositionedText] {
&self.text
}
fn is_round(&self) -> bool {
true
}
fn edge_point(&self, direction: EdgeDirection) -> PointIn {
match direction {
EdgeDirection::Center => return self.center,
EdgeDirection::Start => return self.edge_point(EdgeDirection::West),
EdgeDirection::End => return self.edge_point(EdgeDirection::East),
_ => {}
}
let hw = self.width / 2.0;
let hh = self.height / 2.0;
let rad = hw.min(hh);
let rx = Inches(0.292_893_218_813_452_54 * rad.0);
let (offset_x, offset_y) = match direction {
EdgeDirection::North => (Inches::ZERO, hh),
EdgeDirection::East => (hw, Inches::ZERO),
EdgeDirection::South => (Inches::ZERO, -hh),
EdgeDirection::West => (-hw, Inches::ZERO),
EdgeDirection::NorthEast => (hw - rx, hh - rx),
EdgeDirection::SouthEast => (hw - rx, -(hh - rx)),
EdgeDirection::SouthWest => (-(hw - rx), -(hh - rx)),
EdgeDirection::NorthWest => (-(hw - rx), hh - rx),
_ => return self.center,
};
self.center + OffsetIn::new(offset_x, offset_y)
}
fn render_svg(&self, _obj: &RenderedObject, ctx: &ShapeRenderContext) -> Vec<SvgNode> {
let mut nodes = Vec::new();
if self.style.invisible || self.style.stroke_width.0 < 0.0 {
return nodes;
}
let center_svg = self.center.to_svg(ctx.scaler, ctx.offset_x, ctx.max_y);
let hw = ctx.scaler.px(self.width / 2.0);
let hh = ctx.scaler.px(self.height / 2.0);
let x1 = center_svg.x - hw;
let x2 = center_svg.x + hw;
let y1 = center_svg.y - hh;
let y2 = center_svg.y + hh;
let rad = ctx.scaler.px(self.width.min(self.height) / 2.0);
let svg_style = build_svg_style(&self.style, ctx.scaler, ctx.dashwid, ctx.use_css_vars);
let path_data = create_oval_path(x1, y1, x2, y2, rad);
let path = Path {
d: Some(path_data),
fill: None,
stroke: None,
stroke_width: None,
stroke_dasharray: None,
style: Some(svg_style),
};
nodes.push(SvgNode::Path(path));
nodes
}
fn translate(&mut self, offset: OffsetIn) {
self.center += offset;
}
}
#[derive(Debug, Clone)]
pub struct DiamondShape {
pub center: PointIn,
pub width: Inches,
pub height: Inches,
pub style: ObjectStyle,
pub text: Vec<PositionedText>,
}
impl Shape for DiamondShape {
fn center(&self) -> PointIn {
self.center
}
fn width(&self) -> Inches {
self.width
}
fn height(&self) -> Inches {
self.height
}
fn style(&self) -> &ObjectStyle {
&self.style
}
fn text(&self) -> &[PositionedText] {
&self.text
}
fn edge_point(&self, direction: EdgeDirection) -> PointIn {
match direction {
EdgeDirection::Center => return self.center,
EdgeDirection::Start => return self.edge_point(EdgeDirection::West),
EdgeDirection::End => return self.edge_point(EdgeDirection::East),
_ => {}
}
let hw = self.width / 2.0;
let hh = self.height / 2.0;
let qw = self.width / 4.0; let qh = self.height / 4.0;
let (offset_x, offset_y) = match direction {
EdgeDirection::North => (Inches::ZERO, hh),
EdgeDirection::East => (hw, Inches::ZERO),
EdgeDirection::South => (Inches::ZERO, -hh),
EdgeDirection::West => (-hw, Inches::ZERO),
EdgeDirection::NorthEast => (qw, qh),
EdgeDirection::SouthEast => (qw, -qh),
EdgeDirection::SouthWest => (-qw, -qh),
EdgeDirection::NorthWest => (-qw, qh),
_ => return self.center,
};
self.center + OffsetIn::new(offset_x, offset_y)
}
fn render_svg(&self, _obj: &RenderedObject, ctx: &ShapeRenderContext) -> Vec<SvgNode> {
let mut nodes = Vec::new();
if self.style.invisible || self.style.stroke_width.0 < 0.0 {
return nodes;
}
let center_svg = self.center.to_svg(ctx.scaler, ctx.offset_x, ctx.max_y);
let hw = ctx.scaler.px(self.width / 2.0);
let hh = ctx.scaler.px(self.height / 2.0);
crate::log::debug!(
center_pikchr_x = self.center.x.0,
center_pikchr_y = self.center.y.0,
center_svg_x = center_svg.x,
center_svg_y = center_svg.y,
offset_x = ctx.offset_x.0,
max_y = ctx.max_y.0,
"DiamondShape render_svg"
);
let left = center_svg.x - hw;
let right = center_svg.x + hw;
let top = center_svg.y - hh;
let bottom = center_svg.y + hh;
let svg_style = build_svg_style(&self.style, ctx.scaler, ctx.dashwid, ctx.use_css_vars);
let path_data = PathData::new()
.m(left, center_svg.y) .l(center_svg.x, bottom) .l(right, center_svg.y) .l(center_svg.x, top) .z();
let path = Path {
d: Some(path_data),
fill: None,
stroke: None,
stroke_width: None,
stroke_dasharray: None,
style: Some(svg_style),
};
nodes.push(SvgNode::Path(path));
nodes
}
fn translate(&mut self, offset: OffsetIn) {
self.center += offset;
}
}
#[derive(Debug, Clone)]
pub struct CylinderShape {
pub center: PointIn,
pub width: Inches,
pub height: Inches,
pub ellipse_rad: Inches, pub style: ObjectStyle,
pub text: Vec<PositionedText>,
}
impl Shape for CylinderShape {
fn center(&self) -> PointIn {
self.center
}
fn width(&self) -> Inches {
self.width
}
fn height(&self) -> Inches {
self.height
}
fn style(&self) -> &ObjectStyle {
&self.style
}
fn text(&self) -> &[PositionedText] {
&self.text
}
fn render_svg(&self, _obj: &RenderedObject, ctx: &ShapeRenderContext) -> Vec<SvgNode> {
let mut nodes = Vec::new();
if self.style.invisible || self.style.stroke_width.0 < 0.0 {
return nodes;
}
let center_svg = self.center.to_svg(ctx.scaler, ctx.offset_x, ctx.max_y);
let w = ctx.scaler.px(self.width);
let h = ctx.scaler.px(self.height);
let mut rad = ctx.scaler.px(self.ellipse_rad);
let h2 = h / 2.0;
if rad > h2 {
rad = h2;
} else if rad < 0.0 {
rad = 0.0;
}
let svg_style = build_svg_style(&self.style, ctx.scaler, ctx.dashwid, ctx.use_css_vars);
let (body_path, bottom_arc_path) =
create_cylinder_paths_with_rad(center_svg.x, center_svg.y, w, h, rad);
let body = Path {
d: Some(body_path),
fill: None,
stroke: None,
stroke_width: None,
stroke_dasharray: None,
style: Some(svg_style.clone()),
};
nodes.push(SvgNode::Path(body));
if !bottom_arc_path.commands.is_empty() {
let bottom_arc = Path {
d: Some(bottom_arc_path),
fill: None,
stroke: None,
stroke_width: None,
stroke_dasharray: None,
style: Some(svg_style),
};
nodes.push(SvgNode::Path(bottom_arc));
}
nodes
}
fn translate(&mut self, offset: OffsetIn) {
self.center += offset;
}
fn edge_point(&self, direction: EdgeDirection) -> PointIn {
match direction {
EdgeDirection::Center => return self.center(),
EdgeDirection::Start => return self.start(),
EdgeDirection::End => return self.end(),
_ => {}
}
let hw = self.width / 2.0;
let hh = self.height / 2.0;
let hh_inner = hh - self.ellipse_rad;
let offset = match direction {
EdgeDirection::North => OffsetIn::new(Inches::ZERO, hh),
EdgeDirection::NorthEast => OffsetIn::new(hw, hh_inner),
EdgeDirection::East => OffsetIn::new(hw, Inches::ZERO),
EdgeDirection::SouthEast => OffsetIn::new(hw, -hh_inner),
EdgeDirection::South => OffsetIn::new(Inches::ZERO, -hh),
EdgeDirection::SouthWest => OffsetIn::new(-hw, -hh_inner),
EdgeDirection::West => OffsetIn::new(-hw, Inches::ZERO),
EdgeDirection::NorthWest => OffsetIn::new(-hw, hh_inner),
EdgeDirection::Center | EdgeDirection::Start | EdgeDirection::End => {
unreachable!("handled above")
}
};
self.center + offset
}
}
#[derive(Debug, Clone)]
pub struct FileShape {
pub center: PointIn,
pub width: Inches,
pub height: Inches,
pub fold_radius: Inches,
pub style: ObjectStyle,
pub text: Vec<PositionedText>,
}
impl FileShape {
pub fn new(center: PointIn, width: Inches, height: Inches) -> Self {
Self {
center,
width,
height,
fold_radius: defaults::FILE_RAD,
style: ObjectStyle::default(),
text: Vec::new(),
}
}
}
impl Shape for FileShape {
fn center(&self) -> PointIn {
self.center
}
fn width(&self) -> Inches {
self.width
}
fn height(&self) -> Inches {
self.height
}
fn style(&self) -> &ObjectStyle {
&self.style
}
fn text(&self) -> &[PositionedText] {
&self.text
}
fn edge_point(&self, direction: EdgeDirection) -> PointIn {
match direction {
EdgeDirection::Center => return self.center,
EdgeDirection::Start => return self.edge_point(EdgeDirection::West),
EdgeDirection::End => return self.edge_point(EdgeDirection::East),
_ => {}
}
let hw = self.width / 2.0;
let hh = self.height / 2.0;
let mn = hw.min(hh);
let mut rx = self.fold_radius;
if rx > mn {
rx = mn;
}
if rx < mn * 0.25 {
rx = mn * 0.25;
}
rx = rx * 0.5;
let (offset_x, offset_y) = match direction {
EdgeDirection::North => (Inches::ZERO, hh),
EdgeDirection::East => (hw, Inches::ZERO),
EdgeDirection::South => (Inches::ZERO, -hh),
EdgeDirection::West => (-hw, Inches::ZERO),
EdgeDirection::NorthEast => (hw - rx, hh - rx),
EdgeDirection::SouthEast => (hw, -hh),
EdgeDirection::SouthWest => (-hw, -hh),
EdgeDirection::NorthWest => (-hw, hh),
_ => return self.center,
};
self.center + OffsetIn::new(offset_x, offset_y)
}
fn render_svg(&self, _obj: &RenderedObject, ctx: &ShapeRenderContext) -> Vec<SvgNode> {
let mut nodes = Vec::new();
if self.style.invisible || self.style.stroke_width.0 < 0.0 {
return nodes;
}
let center_svg = self.center.to_svg(ctx.scaler, ctx.offset_x, ctx.max_y);
let w = ctx.scaler.px(self.width);
let h = ctx.scaler.px(self.height);
let w2 = w / 2.0;
let h2 = h / 2.0;
let mn = w2.min(h2);
let rad = ctx.scaler.px(self.fold_radius).min(mn).max(mn * 0.25);
let svg_style = build_svg_style(&self.style, ctx.scaler, ctx.dashwid, ctx.use_css_vars);
let (main_path, fold_path) = create_file_paths(center_svg.x, center_svg.y, w, h, rad);
let main = Path {
d: Some(main_path),
fill: None,
stroke: None,
stroke_width: None,
stroke_dasharray: None,
style: Some(svg_style.clone()),
};
nodes.push(SvgNode::Path(main));
let fold_style = svg_style_from_entries(vec![
("fill", "none".to_string()),
("stroke", color_to_rgb(&self.style.stroke)),
(
"stroke-width",
format!("{}", ctx.scaler.px(self.style.stroke_width)),
),
]);
let fold = Path {
d: Some(fold_path),
fill: None,
stroke: None,
stroke_width: None,
stroke_dasharray: None,
style: Some(fold_style),
};
nodes.push(SvgNode::Path(fold));
nodes
}
fn translate(&mut self, offset: OffsetIn) {
self.center += offset;
}
}
#[derive(Debug, Clone)]
pub struct LineShape {
pub waypoints: Vec<PointIn>,
pub style: ObjectStyle,
pub text: Vec<PositionedText>,
}
impl LineShape {
pub fn new(start: PointIn, end: PointIn) -> Self {
Self {
waypoints: vec![start, end],
style: ObjectStyle::default(),
text: Vec::new(),
}
}
pub fn with_waypoints(waypoints: Vec<PointIn>) -> Self {
Self {
waypoints,
style: ObjectStyle::default(),
text: Vec::new(),
}
}
}
impl Shape for LineShape {
fn center(&self) -> PointIn {
if self.waypoints.is_empty() {
return Point::ORIGIN;
}
let mut min_x = self.waypoints[0].x;
let mut max_x = self.waypoints[0].x;
let mut min_y = self.waypoints[0].y;
let mut max_y = self.waypoints[0].y;
for pt in &self.waypoints {
if pt.x < min_x {
min_x = pt.x;
}
if pt.x > max_x {
max_x = pt.x;
}
if pt.y < min_y {
min_y = pt.y;
}
if pt.y > max_y {
max_y = pt.y;
}
}
Point::new((min_x + max_x) / 2.0, (min_y + max_y) / 2.0)
}
fn width(&self) -> Inches {
if self.waypoints.is_empty() {
return Inches::ZERO;
}
let mut min_x = self.waypoints[0].x;
let mut max_x = self.waypoints[0].x;
for pt in &self.waypoints {
if pt.x < min_x {
min_x = pt.x;
}
if pt.x > max_x {
max_x = pt.x;
}
}
max_x - min_x
}
fn height(&self) -> Inches {
if self.waypoints.is_empty() {
return Inches::ZERO;
}
let mut min_y = self.waypoints[0].y;
let mut max_y = self.waypoints[0].y;
for pt in &self.waypoints {
if pt.y < min_y {
min_y = pt.y;
}
if pt.y > max_y {
max_y = pt.y;
}
}
max_y - min_y
}
fn style(&self) -> &ObjectStyle {
&self.style
}
fn text(&self) -> &[PositionedText] {
&self.text
}
fn start(&self) -> PointIn {
self.waypoints.first().copied().unwrap_or(Point::ORIGIN)
}
fn end(&self) -> PointIn {
self.waypoints.last().copied().unwrap_or(Point::ORIGIN)
}
fn render_svg(&self, _obj: &RenderedObject, ctx: &ShapeRenderContext) -> Vec<SvgNode> {
let mut nodes = Vec::new();
if self.style.invisible || self.style.stroke_width.0 < 0.0 || self.waypoints.len() < 2 {
return nodes;
}
let add_linejoin = (self.style.close_path || self.waypoints.len() > 2)
&& self.style.corner_radius.raw() == 0.0;
let allow_fill = self.style.close_path;
let svg_style = build_svg_style_full(
&self.style,
ctx.scaler,
ctx.dashwid,
add_linejoin,
allow_fill,
ctx.use_css_vars,
);
let arrow_scale = if ctx.thickness.raw() > 0.0 {
self.style.stroke_width.raw() / ctx.thickness.raw()
} else {
1.0
};
let arrow_len_px = ctx.scaler.px(ctx.arrow_len) * arrow_scale;
let arrow_wid_px = ctx.scaler.px(ctx.arrow_wid) * arrow_scale;
let arrow_chop = arrow_len_px / 2.0;
let mut svg_points: Vec<DVec2> = self
.waypoints
.iter()
.map(|pt| pt.to_svg(ctx.scaler, ctx.offset_x, ctx.max_y))
.collect();
if svg_points.len() <= 2 {
let mut draw_start = svg_points[0];
let mut draw_end = svg_points[svg_points.len() - 1];
if self.style.arrow_start
&& let Some(arrowhead) = render_arrowhead_dom(
draw_end,
draw_start,
&self.style,
arrow_len_px,
arrow_wid_px,
ctx.use_css_vars,
)
{
nodes.push(SvgNode::Polygon(arrowhead));
}
if self.style.arrow_end
&& let Some(arrowhead) = render_arrowhead_dom(
draw_start,
draw_end,
&self.style,
arrow_len_px,
arrow_wid_px,
ctx.use_css_vars,
)
{
nodes.push(SvgNode::Polygon(arrowhead));
}
if self.style.arrow_start {
let (new_start, _) = chop_line(draw_start, draw_end, arrow_chop);
draw_start = new_start;
}
if self.style.arrow_end {
let (_, new_end) = chop_line(draw_start, draw_end, arrow_chop);
draw_end = new_end;
}
let mut path_data = PathData::new()
.m(draw_start.x, draw_start.y)
.l(draw_end.x, draw_end.y);
if self.style.close_path {
path_data = path_data.z();
}
let path = Path {
d: Some(path_data),
fill: None,
stroke: None,
stroke_width: None,
stroke_dasharray: None,
style: Some(svg_style),
};
nodes.push(SvgNode::Path(path));
return nodes;
}
if svg_points.len() >= 2 {
if self.style.arrow_start
&& let Some(arrowhead) = render_arrowhead_dom(
svg_points[1],
svg_points[0],
&self.style,
arrow_len_px,
arrow_wid_px,
ctx.use_css_vars,
)
{
nodes.push(SvgNode::Polygon(arrowhead));
}
let n = svg_points.len();
if self.style.arrow_end
&& let Some(arrowhead) = render_arrowhead_dom(
svg_points[n - 2],
svg_points[n - 1],
&self.style,
arrow_len_px,
arrow_wid_px,
ctx.use_css_vars,
)
{
nodes.push(SvgNode::Polygon(arrowhead));
}
if self.style.arrow_start {
let (new_start, _) = chop_line(svg_points[0], svg_points[1], arrow_chop);
svg_points[0] = new_start;
}
if self.style.arrow_end {
let (_, new_end) = chop_line(svg_points[n - 2], svg_points[n - 1], arrow_chop);
svg_points[n - 1] = new_end;
}
}
let corner_radius_px = ctx.scaler.px(self.style.corner_radius);
let mut path_data = PathData::new();
if corner_radius_px > 0.0 && svg_points.len() >= 3 {
let n = svg_points.len();
let i_last = if self.style.close_path { n } else { n - 1 };
crate::log::debug!(
n,
r = corner_radius_px,
close = self.style.close_path,
"[Rust radiusPath]"
);
path_data = path_data.m(svg_points[0].x, svg_points[0].y);
crate::log::debug!(
x = svg_points[0].x,
y = svg_points[0].y,
"[Rust radiusPath] M"
);
if n >= 2 {
let delta = svg_points[1] - svg_points[0];
let dist = delta.length();
let m = if dist > 1e-6 {
let dir = delta.normalize();
let clamped_r = corner_radius_px.min(dist * 0.5);
svg_points[1] - dir * clamped_r } else {
svg_points[1] };
path_data = path_data.l(m.x, m.y);
crate::log::debug!(x = m.x, y = m.y, "[Rust radiusPath] L (before wp1)");
}
for i in 1..i_last {
let a_i = svg_points[i];
let a_n = if i < n - 1 {
svg_points[i + 1]
} else {
svg_points[0]
};
crate::log::debug!(
i,
a_i_x = a_i.x,
a_i_y = a_i.y,
a_n_x = a_n.x,
a_n_y = a_n.y,
"[Rust radiusPath] loop start"
);
let delta_in = a_i - a_n;
let dist_in = delta_in.length();
let (m_entry, is_mid_in) = if dist_in > 1e-6 {
let dir_in = delta_in.normalize();
let clamped_r = corner_radius_px.min(dist_in * 0.5);
let is_mid = clamped_r < corner_radius_px; (a_i - dir_in * clamped_r, is_mid)
} else {
(a_i, false) };
path_data = path_data.q(a_i.x, a_i.y, m_entry.x, m_entry.y);
crate::log::debug!(
ctrl_x = a_i.x,
ctrl_y = a_i.y,
end_x = m_entry.x,
end_y = m_entry.y,
i,
"[Rust radiusPath] Q (curve at wp)"
);
if !is_mid_in {
let delta_out = a_n - a_i;
let dist_out = delta_out.length();
if dist_out > 1e-6 {
let dir_out = delta_out.normalize();
let clamped_r = corner_radius_px.min(dist_out * 0.5);
let m_exit = a_n - dir_out * clamped_r; path_data = path_data.l(m_exit.x, m_exit.y);
crate::log::debug!(
x = m_exit.x,
y = m_exit.y,
toward = i + 1,
"[Rust radiusPath] L (toward wp)"
);
}
}
}
let a_n = if i_last == n {
svg_points[0]
} else {
svg_points[n - 1]
};
path_data = path_data.l(a_n.x, a_n.y);
crate::log::debug!(x = a_n.x, y = a_n.y, "[Rust radiusPath] L (final)");
} else {
for (i, pt) in svg_points.iter().enumerate() {
if i == 0 {
path_data = path_data.m(pt.x, pt.y);
} else {
path_data = path_data.l(pt.x, pt.y);
}
}
}
if self.style.close_path {
path_data = path_data.z();
}
let path = Path {
d: Some(path_data),
fill: None,
stroke: None,
stroke_width: None,
stroke_dasharray: None,
style: Some(svg_style),
};
nodes.push(SvgNode::Path(path));
nodes
}
fn waypoints(&self) -> Option<&[PointIn]> {
Some(&self.waypoints)
}
fn translate(&mut self, offset: OffsetIn) {
for pt in self.waypoints.iter_mut() {
*pt += offset;
}
}
fn expand_bounds(&self, bounds: &mut BoundingBox) {
if !self.style.invisible && self.style.stroke_width.0 >= 0.0 {
for pt in &self.waypoints {
bounds.expand_point(*pt);
}
}
if !self.waypoints.is_empty() {
let w_arrow = defaults::ARROW_WID * 0.5;
if self.style.arrow_start {
let pt = self.waypoints[0];
bounds.expand_point(Point::new(pt.x - w_arrow, pt.y - w_arrow));
bounds.expand_point(Point::new(pt.x + w_arrow, pt.y + w_arrow));
}
if self.style.arrow_end {
let pt = *self.waypoints.last().unwrap();
bounds.expand_point(Point::new(pt.x - w_arrow, pt.y - w_arrow));
bounds.expand_point(Point::new(pt.x + w_arrow, pt.y + w_arrow));
}
}
if !self.text.is_empty() {
let charht = Inches(defaults::FONT_SIZE);
let charwid = defaults::CHARWID;
let center = self.center();
let vslots = compute_text_vslots(&self.text);
let sw = self.style.stroke_width.0.max(0.0);
let mut hc = Inches(sw * 1.5);
let mut ha1 = Inches::ZERO;
let mut ha2 = Inches::ZERO;
let mut hb1 = Inches::ZERO;
let mut hb2 = Inches::ZERO;
for (t, slot) in self.text.iter().zip(vslots.iter()) {
let h = Inches(t.height(charht.0));
match slot {
TextVSlot::Center => hc = hc.max(h),
TextVSlot::Above => ha1 = ha1.max(h),
TextVSlot::Above2 => ha2 = ha2.max(h),
TextVSlot::Below => hb1 = hb1.max(h),
TextVSlot::Below2 => hb2 = hb2.max(h),
}
}
let y_base = Inches::ZERO;
let (line_dx, line_dy) = if self.waypoints.len() >= 2 {
let start = self.waypoints[0];
let end = self.waypoints[self.waypoints.len() - 1];
let dx = (end.x - start.x).0;
let dy = (end.y - start.y).0;
let dist = (dx * dx + dy * dy).sqrt();
if dist > 0.0 {
(dx / dist, dy / dist)
} else {
(1.0, 0.0) }
} else {
(1.0, 0.0)
};
for (i, t) in self.text.iter().enumerate() {
let cw = Inches(t.width_inches(charwid));
let ch = Inches(t.height(charht.0)) / 2.0;
let y = match vslots.get(i).unwrap_or(&TextVSlot::Center) {
TextVSlot::Above2 => y_base + hc * 0.5 + ha1 + ha2 * 0.5,
TextVSlot::Above => y_base + hc * 0.5 + ha1 * 0.5,
TextVSlot::Center => y_base,
TextVSlot::Below => y_base - hc * 0.5 - hb1 * 0.5,
TextVSlot::Below2 => y_base - hc * 0.5 - hb1 - hb2 * 0.5,
};
let (x0, y0, x1, y1) = if t.rjust {
(Inches::ZERO, y - ch, -cw, y + ch)
} else if t.ljust {
(Inches::ZERO, y - ch, cw, y + ch)
} else {
(cw / 2.0, y + ch, -cw / 2.0, y - ch)
};
let (rx0, ry0, rx1, ry1) = if t.aligned && (line_dx != 1.0 || line_dy != 0.0) {
let new_x0 = line_dx * x0.0 - line_dy * y0.0;
let new_y0 = line_dy * x0.0 - line_dx * y0.0;
let new_x1 = line_dx * x1.0 - line_dy * y1.0;
let new_y1 = line_dy * x1.0 - line_dx * y1.0;
(
Inches(new_x0),
Inches(new_y0),
Inches(new_x1),
Inches(new_y1),
)
} else {
(x0, y0, x1, y1)
};
bounds.expand_point(Point::new(center.x + rx0, center.y + ry0));
bounds.expand_point(Point::new(center.x + rx1, center.y + ry1));
}
}
}
fn expand_core_bounds(&self, bounds: &mut BoundingBox) {
if !self.style.invisible && self.style.stroke_width.0 >= 0.0 {
for pt in &self.waypoints {
bounds.expand_point(*pt);
}
}
}
}
#[derive(Debug, Clone)]
pub struct SplineShape {
pub waypoints: Vec<PointIn>,
pub style: ObjectStyle,
pub text: Vec<PositionedText>,
pub radius: Inches,
}
impl Shape for SplineShape {
fn center(&self) -> PointIn {
if self.waypoints.is_empty() {
return Point::ORIGIN;
}
let mut min_x = self.waypoints[0].x;
let mut max_x = self.waypoints[0].x;
let mut min_y = self.waypoints[0].y;
let mut max_y = self.waypoints[0].y;
for pt in &self.waypoints {
if pt.x < min_x {
min_x = pt.x;
}
if pt.x > max_x {
max_x = pt.x;
}
if pt.y < min_y {
min_y = pt.y;
}
if pt.y > max_y {
max_y = pt.y;
}
}
Point::new((min_x + max_x) / 2.0, (min_y + max_y) / 2.0)
}
fn width(&self) -> Inches {
if self.waypoints.is_empty() {
return Inches::ZERO;
}
let mut min_x = self.waypoints[0].x;
let mut max_x = self.waypoints[0].x;
for pt in &self.waypoints {
if pt.x < min_x {
min_x = pt.x;
}
if pt.x > max_x {
max_x = pt.x;
}
}
max_x - min_x
}
fn height(&self) -> Inches {
if self.waypoints.is_empty() {
return Inches::ZERO;
}
let mut min_y = self.waypoints[0].y;
let mut max_y = self.waypoints[0].y;
for pt in &self.waypoints {
if pt.y < min_y {
min_y = pt.y;
}
if pt.y > max_y {
max_y = pt.y;
}
}
max_y - min_y
}
fn style(&self) -> &ObjectStyle {
&self.style
}
fn text(&self) -> &[PositionedText] {
&self.text
}
fn start(&self) -> PointIn {
self.waypoints.first().copied().unwrap_or(Point::ORIGIN)
}
fn end(&self) -> PointIn {
self.waypoints.last().copied().unwrap_or(Point::ORIGIN)
}
fn render_svg(&self, _obj: &RenderedObject, ctx: &ShapeRenderContext) -> Vec<SvgNode> {
let mut nodes = Vec::new();
if self.style.invisible || self.style.stroke_width.0 < 0.0 || self.waypoints.len() < 2 {
return nodes;
}
let svg_style = build_svg_style(&self.style, ctx.scaler, ctx.dashwid, ctx.use_css_vars);
let arrow_scale = if ctx.thickness.raw() > 0.0 {
self.style.stroke_width.raw() / ctx.thickness.raw()
} else {
1.0
};
let arrow_len_px = ctx.scaler.px(ctx.arrow_len) * arrow_scale;
let arrow_wid_px = ctx.scaler.px(ctx.arrow_wid) * arrow_scale;
let n = self.waypoints.len();
if self.style.arrow_start && n >= 2 {
let p1 = self.waypoints[0].to_svg(ctx.scaler, ctx.offset_x, ctx.max_y);
let p2 = self.waypoints[1].to_svg(ctx.scaler, ctx.offset_x, ctx.max_y);
if let Some(arrowhead) = render_arrowhead_dom(
p2,
p1,
&self.style,
arrow_len_px,
arrow_wid_px,
ctx.use_css_vars,
) {
nodes.push(SvgNode::Polygon(arrowhead));
}
}
if self.style.arrow_end && n >= 2 {
let p1 = self.waypoints[n - 2].to_svg(ctx.scaler, ctx.offset_x, ctx.max_y);
let p2 = self.waypoints[n - 1].to_svg(ctx.scaler, ctx.offset_x, ctx.max_y);
if let Some(arrowhead) = render_arrowhead_dom(
p1,
p2,
&self.style,
arrow_len_px,
arrow_wid_px,
ctx.use_css_vars,
) {
nodes.push(SvgNode::Polygon(arrowhead));
}
}
let mut waypoints = self.waypoints.clone();
let chop_amount = Inches(ctx.arrow_len.raw() * arrow_scale / 2.0);
if self.style.arrow_start && waypoints.len() >= 2 {
chop_waypoint_start(&mut waypoints, chop_amount);
}
if self.style.arrow_end && waypoints.len() >= 2 {
chop_waypoint_end(&mut waypoints, chop_amount);
}
let path_data = if waypoints.len() < 3 || self.radius.raw() <= 0.0 {
create_line_path(&waypoints, ctx.scaler, ctx.offset_x, ctx.max_y)
} else {
create_spline_path(&waypoints, ctx.scaler, ctx.offset_x, ctx.max_y, self.radius)
};
let path = Path {
d: Some(path_data),
fill: None,
stroke: None,
stroke_width: None,
stroke_dasharray: None,
style: Some(svg_style),
};
nodes.push(SvgNode::Path(path));
nodes
}
fn waypoints(&self) -> Option<&[PointIn]> {
Some(&self.waypoints)
}
fn translate(&mut self, offset: OffsetIn) {
for pt in self.waypoints.iter_mut() {
*pt += offset;
}
}
fn expand_bounds(&self, bounds: &mut BoundingBox) {
if !self.style.invisible && self.style.stroke_width.0 >= 0.0 {
for pt in &self.waypoints {
bounds.expand_point(*pt);
}
}
if !self.waypoints.is_empty() {
let w_arrow = defaults::ARROW_WID * 0.5;
if self.style.arrow_start {
let pt = self.waypoints[0];
bounds.expand_point(Point::new(pt.x - w_arrow, pt.y - w_arrow));
bounds.expand_point(Point::new(pt.x + w_arrow, pt.y + w_arrow));
}
if self.style.arrow_end {
let pt = *self.waypoints.last().unwrap();
bounds.expand_point(Point::new(pt.x - w_arrow, pt.y - w_arrow));
bounds.expand_point(Point::new(pt.x + w_arrow, pt.y + w_arrow));
}
}
if !self.text.is_empty() {
let charht = defaults::FONT_SIZE;
let (text_above, text_below) = sum_text_heights_above_below(&self.text, charht);
let center = self.center();
bounds.expand_point(Point::new(center.x, center.y + Inches(text_above)));
bounds.expand_point(Point::new(center.x, center.y - Inches(text_below)));
}
}
fn expand_core_bounds(&self, bounds: &mut BoundingBox) {
if !self.style.invisible && self.style.stroke_width.0 >= 0.0 {
for pt in &self.waypoints {
bounds.expand_point(*pt);
}
}
}
}
#[derive(Debug, Clone)]
pub struct DotShape {
pub center: PointIn,
pub radius: Inches,
pub style: ObjectStyle,
pub text: Vec<PositionedText>,
}
impl DotShape {
pub fn new(center: PointIn) -> Self {
Self {
center,
radius: Inches(0.025), style: ObjectStyle {
fill: "black".to_string(),
..ObjectStyle::default()
},
text: Vec::new(),
}
}
}
impl Shape for DotShape {
fn center(&self) -> PointIn {
self.center
}
fn width(&self) -> Inches {
self.radius * 2.0
}
fn height(&self) -> Inches {
self.radius * 2.0
}
fn style(&self) -> &ObjectStyle {
&self.style
}
fn text(&self) -> &[PositionedText] {
&self.text
}
fn is_round(&self) -> bool {
true
}
fn start(&self) -> PointIn {
self.center
}
fn end(&self) -> PointIn {
self.center
}
fn render_svg(&self, _obj: &RenderedObject, ctx: &ShapeRenderContext) -> Vec<SvgNode> {
let mut nodes = Vec::new();
if self.style.invisible || self.style.stroke_width.0 < 0.0 {
return nodes;
}
let center_svg = self.center.to_svg(ctx.scaler, ctx.offset_x, ctx.max_y);
let r = ctx.scaler.px(self.radius);
crate::log::debug!(
fill = %self.style.fill,
stroke = %self.style.stroke,
"[Rust dot render] About to render dot"
);
let svg_style = build_svg_style(&self.style, ctx.scaler, ctx.dashwid, ctx.use_css_vars);
let circle = SvgCircle {
cx: Some(center_svg.x),
cy: Some(center_svg.y),
r: Some(r),
fill: None,
stroke: None,
stroke_width: None,
stroke_dasharray: None,
style: Some(svg_style),
};
nodes.push(SvgNode::Circle(circle));
nodes
}
fn translate(&mut self, offset: OffsetIn) {
self.center += offset;
}
}
#[derive(Debug, Clone)]
pub struct TextShape {
pub center: PointIn,
pub width: Inches,
pub height: Inches,
pub style: ObjectStyle,
pub text: Vec<PositionedText>,
}
impl Shape for TextShape {
fn center(&self) -> PointIn {
self.center
}
fn width(&self) -> Inches {
self.width
}
fn height(&self) -> Inches {
self.height
}
fn style(&self) -> &ObjectStyle {
&self.style
}
fn text(&self) -> &[PositionedText] {
&self.text
}
fn render_svg(&self, _obj: &RenderedObject, _ctx: &ShapeRenderContext) -> Vec<SvgNode> {
Vec::new()
}
fn translate(&mut self, offset: OffsetIn) {
self.center += offset;
}
fn expand_bounds(&self, bounds: &mut BoundingBox) {
let charht = Inches(defaults::FONT_SIZE);
let charwid = defaults::CHARWID;
let center = self.center;
bounds.expand_rect(
center,
Size {
w: self.width,
h: self.height,
},
);
if self.text.is_empty() {
return;
}
{
let vslots = compute_text_vslots(&self.text);
let mut hc = Inches::ZERO;
let mut ha1 = Inches::ZERO;
let mut ha2 = Inches::ZERO;
let mut hb1 = Inches::ZERO;
let mut hb2 = Inches::ZERO;
for (t, slot) in self.text.iter().zip(vslots.iter()) {
let h = Inches(t.height(charht.0));
match slot {
TextVSlot::Center => hc = hc.max(h),
TextVSlot::Above => ha1 = ha1.max(h),
TextVSlot::Above2 => ha2 = ha2.max(h),
TextVSlot::Below => hb1 = hb1.max(h),
TextVSlot::Below2 => hb2 = hb2.max(h),
}
}
let y_base = Inches::ZERO;
for (i, t) in self.text.iter().enumerate() {
let text_w = Inches(t.width_inches(charwid));
let ch = Inches(t.height(charht.0)) / 2.0;
let y = match vslots.get(i).unwrap_or(&TextVSlot::Center) {
TextVSlot::Above2 => y_base + hc * 0.5 + ha1 + ha2 * 0.5,
TextVSlot::Above => y_base + hc * 0.5 + ha1 * 0.5,
TextVSlot::Center => y_base,
TextVSlot::Below => y_base - hc * 0.5 - hb1 * 0.5,
TextVSlot::Below2 => y_base - hc * 0.5 - hb1 - hb2 * 0.5,
};
let line_y = center.y + y;
if t.rjust {
bounds.expand_point(Point::new(center.x - text_w, line_y - ch));
bounds.expand_point(Point::new(center.x, line_y + ch));
} else if t.ljust {
bounds.expand_point(Point::new(center.x, line_y - ch));
bounds.expand_point(Point::new(center.x + text_w, line_y + ch));
} else {
bounds.expand_point(Point::new(center.x - text_w / 2.0, line_y - ch));
bounds.expand_point(Point::new(center.x + text_w / 2.0, line_y + ch));
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct ArcShape {
pub start: PointIn,
pub end: PointIn,
pub style: ObjectStyle,
pub text: Vec<PositionedText>,
pub clockwise: bool,
}
impl ArcShape {
pub fn new(start: PointIn, end: PointIn, clockwise: bool) -> Self {
Self {
start,
end,
style: ObjectStyle::default(),
text: Vec::new(),
clockwise,
}
}
}
impl Shape for ArcShape {
fn center(&self) -> PointIn {
self.start.midpoint(self.end)
}
fn width(&self) -> Inches {
let delta = self.end - self.start;
delta.dx.abs()
}
fn height(&self) -> Inches {
let delta = self.end - self.start;
delta.dy.abs()
}
fn style(&self) -> &ObjectStyle {
&self.style
}
fn text(&self) -> &[PositionedText] {
&self.text
}
fn start(&self) -> PointIn {
self.start
}
fn end(&self) -> PointIn {
self.end
}
fn render_svg(&self, _obj: &RenderedObject, ctx: &ShapeRenderContext) -> Vec<SvgNode> {
let mut nodes = Vec::new();
if self.style.invisible || self.style.stroke_width.0 < 0.0 {
return nodes;
}
let mut start_svg = self.start.to_svg(ctx.scaler, ctx.offset_x, ctx.max_y);
let mut end_svg = self.end.to_svg(ctx.scaler, ctx.offset_x, ctx.max_y);
let control = arc_control_point(self.style.clockwise, start_svg, end_svg);
let arrow_scale = if ctx.thickness.raw() > 0.0 {
self.style.stroke_width.raw() / ctx.thickness.raw()
} else {
1.0
};
let arrow_len_px = ctx.scaler.px(ctx.arrow_len) * arrow_scale;
let arrow_wid_px = ctx.scaler.px(ctx.arrow_wid) * arrow_scale;
let arrow_chop = arrow_len_px / 2.0;
if self.style.arrow_start {
if let Some(arrowhead) = render_arrowhead_dom(
control,
start_svg,
&self.style,
arrow_len_px,
arrow_wid_px,
ctx.use_css_vars,
) {
nodes.push(SvgNode::Polygon(arrowhead));
}
start_svg = chop_point(control, start_svg, arrow_chop);
}
if self.style.arrow_end {
if let Some(arrowhead) = render_arrowhead_dom(
control,
end_svg,
&self.style,
arrow_len_px,
arrow_wid_px,
ctx.use_css_vars,
) {
nodes.push(SvgNode::Polygon(arrowhead));
}
end_svg = chop_point(control, end_svg, arrow_chop);
}
let svg_style = build_svg_style(&self.style, ctx.scaler, ctx.dashwid, ctx.use_css_vars);
let arc_path_data = create_arc_path_with_control(start_svg, control, end_svg);
let arc_path = Path {
d: Some(arc_path_data),
fill: None,
stroke: None,
stroke_width: None,
stroke_dasharray: None,
style: Some(svg_style),
};
nodes.push(SvgNode::Path(arc_path));
nodes
}
fn translate(&mut self, offset: OffsetIn) {
self.start += offset;
self.end += offset;
}
fn expand_bounds(&self, bounds: &mut BoundingBox) {
if self.style.invisible {
return;
}
let f = self.start;
let t = self.end;
let mid = f.midpoint(t);
let dx = t.x - f.x;
let dy = t.y - f.y;
let m = if self.clockwise {
Point::new(mid.x - dy * 0.5, mid.y + dx * 0.5)
} else {
Point::new(mid.x + dy * 0.5, mid.y - dx * 0.5)
};
let sw = self.style.stroke_width;
for i in 1..16 {
let t1 = 0.0625 * i as f64;
let t2 = 1.0 - t1;
let a = t2 * t2;
let b = 2.0 * t1 * t2;
let c = t1 * t1;
let x = Inches(a * f.x.0 + b * m.x.0 + c * t.x.0);
let y = Inches(a * f.y.0 + b * m.y.0 + c * t.y.0);
bounds.expand_point(Point::new(x - sw, y - sw));
bounds.expand_point(Point::new(x + sw, y + sw));
}
let w_arrow = defaults::ARROW_WID * 0.5;
if self.style.arrow_start {
bounds.expand_point(Point::new(f.x - w_arrow, f.y - w_arrow));
bounds.expand_point(Point::new(f.x + w_arrow, f.y + w_arrow));
}
if self.style.arrow_end {
bounds.expand_point(Point::new(t.x - w_arrow, t.y - w_arrow));
bounds.expand_point(Point::new(t.x + w_arrow, t.y + w_arrow));
}
if !self.text.is_empty() {
let charht = defaults::FONT_SIZE;
let (text_above, text_below) = sum_text_heights_above_below(&self.text, charht);
let center = self.center();
bounds.expand_point(Point::new(center.x, center.y + Inches(text_above)));
bounds.expand_point(Point::new(center.x, center.y - Inches(text_below)));
}
}
fn expand_core_bounds(&self, bounds: &mut BoundingBox) {
if self.style.invisible {
return;
}
let f = self.start;
let t = self.end;
let mid = f.midpoint(t);
let dx = t.x - f.x;
let dy = t.y - f.y;
let m = if self.clockwise {
Point::new(mid.x - dy * 0.5, mid.y + dx * 0.5)
} else {
Point::new(mid.x + dy * 0.5, mid.y - dx * 0.5)
};
let sw = self.style.stroke_width;
for i in 1..16 {
let t1 = 0.0625 * i as f64;
let t2 = 1.0 - t1;
let a = t2 * t2;
let b = 2.0 * t1 * t2;
let c = t1 * t1;
let x = Inches(a * f.x.0 + b * m.x.0 + c * t.x.0);
let y = Inches(a * f.y.0 + b * m.y.0 + c * t.y.0);
bounds.expand_point(Point::new(x - sw, y - sw));
bounds.expand_point(Point::new(x + sw, y + sw));
}
}
}
#[derive(Debug, Clone)]
pub struct MoveShape {
pub start: PointIn,
pub end: PointIn,
pub style: ObjectStyle,
pub text: Vec<PositionedText>,
}
impl MoveShape {
pub fn new(start: PointIn, end: PointIn) -> Self {
Self {
start,
end,
style: ObjectStyle::default(),
text: Vec::new(),
}
}
}
impl Shape for MoveShape {
fn center(&self) -> PointIn {
self.start.midpoint(self.end)
}
fn width(&self) -> Inches {
let delta = self.end - self.start;
delta.dx.abs()
}
fn height(&self) -> Inches {
let delta = self.end - self.start;
delta.dy.abs()
}
fn style(&self) -> &ObjectStyle {
&self.style
}
fn text(&self) -> &[PositionedText] {
&self.text
}
fn start(&self) -> PointIn {
self.start
}
fn end(&self) -> PointIn {
self.end
}
fn render_svg(&self, _obj: &RenderedObject, _ctx: &ShapeRenderContext) -> Vec<SvgNode> {
Vec::new()
}
fn translate(&mut self, offset: OffsetIn) {
self.start += offset;
self.end += offset;
}
}
#[derive(Debug, Clone)]
pub struct SublistShape {
pub center: PointIn,
pub width: Inches,
pub height: Inches,
pub style: ObjectStyle,
pub text: Vec<PositionedText>,
pub children: Vec<RenderedObject>,
}
impl SublistShape {
pub fn new(center: PointIn, width: Inches, height: Inches) -> Self {
Self {
center,
width,
height,
style: ObjectStyle::default(),
text: Vec::new(),
children: Vec::new(),
}
}
}
impl Shape for SublistShape {
fn center(&self) -> PointIn {
self.center
}
fn width(&self) -> Inches {
self.width
}
fn height(&self) -> Inches {
self.height
}
fn style(&self) -> &ObjectStyle {
&self.style
}
fn text(&self) -> &[PositionedText] {
&self.text
}
fn render_svg(&self, _obj: &RenderedObject, ctx: &ShapeRenderContext) -> Vec<SvgNode> {
let mut nodes = Vec::new();
for child in &self.children {
let child_shape = &child.shape;
let child_nodes = child_shape.render_svg(child, ctx);
nodes.extend(child_nodes);
}
nodes
}
fn translate(&mut self, offset: OffsetIn) {
self.center += offset;
for child in self.children.iter_mut() {
child.translate(offset);
}
}
fn expand_bounds(&self, bounds: &mut BoundingBox) {
for child in &self.children {
let shape = &child.shape;
shape.expand_bounds(bounds);
}
}
fn expand_core_bounds(&self, bounds: &mut BoundingBox) {
for child in &self.children {
let shape = &child.shape;
shape.expand_core_bounds(bounds);
}
}
}
#[derive(Debug, Clone)]
#[enum_dispatch(Shape)]
pub enum ShapeEnum {
Box(BoxShape),
Circle(CircleShape),
Ellipse(EllipseShape),
Oval(OvalShape),
Diamond(DiamondShape),
Cylinder(CylinderShape),
File(FileShape),
Line(LineShape),
Spline(SplineShape),
Dot(DotShape),
Text(TextShape),
Arc(ArcShape),
Move(MoveShape),
Sublist(SublistShape),
}
impl ShapeEnum {
pub fn is_path(&self) -> bool {
matches!(
self,
ShapeEnum::Line(_) | ShapeEnum::Spline(_) | ShapeEnum::Arc(_) | ShapeEnum::Move(_)
)
}
pub fn set_center(&mut self, center: PointIn) {
match self {
ShapeEnum::Box(s) => s.center = center,
ShapeEnum::Circle(s) => s.center = center,
ShapeEnum::Ellipse(s) => s.center = center,
ShapeEnum::Oval(s) => s.center = center,
ShapeEnum::Diamond(s) => s.center = center,
ShapeEnum::Cylinder(s) => s.center = center,
ShapeEnum::File(s) => s.center = center,
ShapeEnum::Line(_) | ShapeEnum::Spline(_) | ShapeEnum::Arc(_) | ShapeEnum::Move(_) => {
}
ShapeEnum::Dot(s) => s.center = center,
ShapeEnum::Text(s) => s.center = center,
ShapeEnum::Sublist(s) => s.center = center,
}
}
pub fn style_mut(&mut self) -> &mut ObjectStyle {
match self {
ShapeEnum::Box(s) => &mut s.style,
ShapeEnum::Circle(s) => &mut s.style,
ShapeEnum::Ellipse(s) => &mut s.style,
ShapeEnum::Oval(s) => &mut s.style,
ShapeEnum::Diamond(s) => &mut s.style,
ShapeEnum::Cylinder(s) => &mut s.style,
ShapeEnum::File(s) => &mut s.style,
ShapeEnum::Line(s) => &mut s.style,
ShapeEnum::Spline(s) => &mut s.style,
ShapeEnum::Dot(s) => &mut s.style,
ShapeEnum::Text(s) => &mut s.style,
ShapeEnum::Arc(s) => &mut s.style,
ShapeEnum::Move(s) => &mut s.style,
ShapeEnum::Sublist(s) => &mut s.style,
}
}
pub fn text_mut(&mut self) -> &mut Vec<PositionedText> {
match self {
ShapeEnum::Box(s) => &mut s.text,
ShapeEnum::Circle(s) => &mut s.text,
ShapeEnum::Ellipse(s) => &mut s.text,
ShapeEnum::Oval(s) => &mut s.text,
ShapeEnum::Diamond(s) => &mut s.text,
ShapeEnum::Cylinder(s) => &mut s.text,
ShapeEnum::File(s) => &mut s.text,
ShapeEnum::Line(s) => &mut s.text,
ShapeEnum::Spline(s) => &mut s.text,
ShapeEnum::Dot(s) => &mut s.text,
ShapeEnum::Text(s) => &mut s.text,
ShapeEnum::Arc(s) => &mut s.text,
ShapeEnum::Move(s) => &mut s.text,
ShapeEnum::Sublist(s) => &mut s.text,
}
}
pub fn children(&self) -> Option<&Vec<RenderedObject>> {
match self {
ShapeEnum::Sublist(s) => Some(&s.children),
_ => None,
}
}
pub fn children_mut(&mut self) -> Option<&mut Vec<RenderedObject>> {
match self {
ShapeEnum::Sublist(s) => Some(&mut s.children),
_ => None,
}
}
pub fn waypoints(&self) -> Option<&[PointIn]> {
match self {
ShapeEnum::Line(s) => Some(&s.waypoints),
ShapeEnum::Spline(s) => Some(&s.waypoints),
_ => None,
}
}
pub fn waypoints_mut(&mut self) -> Option<&mut Vec<PointIn>> {
match self {
ShapeEnum::Line(s) => Some(&mut s.waypoints),
ShapeEnum::Spline(s) => Some(&mut s.waypoints),
_ => None,
}
}
pub fn class(&self) -> ClassName {
match self {
ShapeEnum::Box(_) => ClassName::Box,
ShapeEnum::Circle(_) => ClassName::Circle,
ShapeEnum::Ellipse(_) => ClassName::Ellipse,
ShapeEnum::Oval(_) => ClassName::Oval,
ShapeEnum::Diamond(_) => ClassName::Diamond,
ShapeEnum::Cylinder(_) => ClassName::Cylinder,
ShapeEnum::File(_) => ClassName::File,
ShapeEnum::Line(_) => ClassName::Line,
ShapeEnum::Spline(_) => ClassName::Spline,
ShapeEnum::Dot(_) => ClassName::Dot,
ShapeEnum::Text(_) => ClassName::Text,
ShapeEnum::Arc(_) => ClassName::Arc,
ShapeEnum::Move(_) => ClassName::Move,
ShapeEnum::Sublist(_) => ClassName::Sublist,
}
}
}
fn build_svg_style(
style: &ObjectStyle,
scaler: &Scaler,
dashwid: Inches,
use_css_vars: bool,
) -> String {
build_svg_style_full(style, scaler, dashwid, false, true, use_css_vars)
}
fn build_svg_style_full(
style: &ObjectStyle,
scaler: &Scaler,
dashwid: Inches,
add_linejoin: bool,
allow_fill: bool,
use_css_vars: bool,
) -> String {
let fill_rgb = if allow_fill {
color_to_string(&style.fill, use_css_vars)
} else {
"none".to_string()
};
let stroke_rgb = color_to_string(&style.stroke, use_css_vars);
crate::log::debug!(
fill_input = %style.fill,
fill_output = %fill_rgb,
stroke_input = %style.stroke,
stroke_output = %stroke_rgb,
"[Rust build_svg_style] Converting colors"
);
let mut entries = vec![
("fill", fill_rgb),
("stroke", stroke_rgb),
("stroke-width", format!("{}", scaler.px(style.stroke_width))),
];
if let Some(dash_width) = style.dashed {
let dash = scaler.px(dash_width);
entries.push(("stroke-dasharray", format!("{},{}", dash, dash)));
}
else if let Some(gap_width) = style.dotted {
let dot = scaler.px(style.stroke_width);
let gap = scaler.px(gap_width);
entries.push(("stroke-dasharray", format!("{},{}", dot, gap)));
}
if add_linejoin {
entries.push(("stroke-linejoin", "round".to_string()));
}
let _ = dashwid;
svg_style_from_entries(entries)
}
pub(crate) fn svg_style_from_entries(entries: Vec<(&'static str, String)>) -> String {
let mut css = String::new();
for (name, value) in entries {
if value.is_empty() {
continue;
}
css.push_str(name);
css.push(':');
css.push_str(&value);
css.push(';');
}
css
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::Point;
#[test]
fn circle_dimensions() {
let circle = CircleShape::new(Point::new(Inches(1.0), Inches(2.0)), Inches(0.5));
assert_eq!(circle.width(), Inches(1.0));
assert_eq!(circle.height(), Inches(1.0));
assert!(circle.is_round());
}
#[test]
fn circle_edge_points() {
let circle = CircleShape::new(Point::new(Inches(0.0), Inches(0.0)), Inches(1.0));
let north = circle.edge_point(EdgeDirection::North);
assert_eq!(north.x, Inches(0.0));
assert_eq!(north.y, Inches(1.0));
let east = circle.edge_point(EdgeDirection::East);
assert_eq!(east.x, Inches(1.0));
assert_eq!(east.y, Inches(0.0));
let ne = circle.edge_point(EdgeDirection::NorthEast);
let expected = std::f64::consts::FRAC_1_SQRT_2;
assert!((ne.x.0 - expected).abs() < 0.001);
assert!((ne.y.0 - expected).abs() < 0.001); }
#[test]
fn box_dimensions() {
let bx = BoxShape::new(
Point::new(Inches(0.0), Inches(0.0)),
Inches(2.0),
Inches(1.0),
);
assert_eq!(bx.width(), Inches(2.0));
assert_eq!(bx.height(), Inches(1.0));
assert!(!bx.is_round());
}
#[test]
fn line_start_end() {
let line = LineShape::new(
Point::new(Inches(0.0), Inches(0.0)),
Point::new(Inches(1.0), Inches(1.0)),
);
assert_eq!(line.start(), Point::new(Inches(0.0), Inches(0.0)));
assert_eq!(line.end(), Point::new(Inches(1.0), Inches(1.0)));
assert_eq!(line.center(), Point::new(Inches(0.5), Inches(0.5)));
}
}