use std::collections::HashMap;
use crate::GeometryPathCommand;
const ELLIPSE_KAPPA: f64 = 0.552_284_749_830_793_6;
const ROUND_RECT_RADIUS: f64 = 0.166_666_666_666_666_66;
pub fn preset_geometry_to_path(
shape_type: &str,
adjustments: &HashMap<String, f64>,
) -> Option<Vec<GeometryPathCommand>> {
use GeometryPathCommand as C;
let result = match shape_type {
"rect" => vec![
C::Move { x: 0.0, y: 0.0 },
C::Line { x: 1.0, y: 0.0 },
C::Line { x: 1.0, y: 1.0 },
C::Line { x: 0.0, y: 1.0 },
C::Close,
],
"roundRect" => {
let r = ROUND_RECT_RADIUS;
vec![
C::Move { x: r, y: 0.0 },
C::Line { x: 1.0 - r, y: 0.0 },
C::Quad {
cpx: 1.0,
cpy: 0.0,
x: 1.0,
y: r,
},
C::Line { x: 1.0, y: 1.0 - r },
C::Quad {
cpx: 1.0,
cpy: 1.0,
x: 1.0 - r,
y: 1.0,
},
C::Line { x: r, y: 1.0 },
C::Quad {
cpx: 0.0,
cpy: 1.0,
x: 0.0,
y: 1.0 - r,
},
C::Line { x: 0.0, y: r },
C::Quad {
cpx: 0.0,
cpy: 0.0,
x: r,
y: 0.0,
},
C::Close,
]
}
"ellipse" => vec![
C::Move { x: 1.0, y: 0.5 },
C::Cubic {
cp1x: 1.0,
cp1y: 0.5 + ELLIPSE_KAPPA / 2.0,
cp2x: 0.5 + ELLIPSE_KAPPA / 2.0,
cp2y: 1.0,
x: 0.5,
y: 1.0,
},
C::Cubic {
cp1x: 0.5 - ELLIPSE_KAPPA / 2.0,
cp1y: 1.0,
cp2x: 0.0,
cp2y: 0.5 + ELLIPSE_KAPPA / 2.0,
x: 0.0,
y: 0.5,
},
C::Cubic {
cp1x: 0.0,
cp1y: 0.5 - ELLIPSE_KAPPA / 2.0,
cp2x: 0.5 - ELLIPSE_KAPPA / 2.0,
cp2y: 0.0,
x: 0.5,
y: 0.0,
},
C::Cubic {
cp1x: 0.5 + ELLIPSE_KAPPA / 2.0,
cp1y: 0.0,
cp2x: 1.0,
cp2y: 0.5 - ELLIPSE_KAPPA / 2.0,
x: 1.0,
y: 0.5,
},
C::Close,
],
"line" | "straightConnector1" => {
vec![C::Move { x: 0.0, y: 0.0 }, C::Line { x: 1.0, y: 1.0 }]
}
"triangle" | "isosTriangle" => polygon(&[(0.5, 0.0), (1.0, 1.0), (0.0, 1.0)]),
"rtTriangle" => polygon(&[(0.0, 0.0), (1.0, 1.0), (0.0, 1.0)]),
"diamond" | "flowChartDecision" => {
polygon(&[(0.5, 0.0), (1.0, 0.5), (0.5, 1.0), (0.0, 0.5)])
}
"parallelogram" => {
let i = clamp_fraction(adjustments.get("adj").copied(), 0.25);
polygon(&[(i, 0.0), (1.0, 0.0), (1.0 - i, 1.0), (0.0, 1.0)])
}
"trapezoid" => {
let i = clamp_fraction(adjustments.get("adj").copied(), 0.2);
polygon(&[(i, 0.0), (1.0 - i, 0.0), (1.0, 1.0), (0.0, 1.0)])
}
"pentagon" | "flowChartOffpageConnector" => regular_polygon(5),
"hexagon" => regular_polygon(6),
"heptagon" => regular_polygon(7),
"octagon" => regular_polygon(8),
"decagon" => regular_polygon(10),
"dodecagon" => regular_polygon(12),
value if value.starts_with("star") => {
let points = value[4..].parse::<usize>().ok()?;
if !matches!(points, 4 | 5 | 6 | 7 | 8 | 10 | 12 | 16 | 24 | 32) {
return None;
}
star(points)
}
"bentConnector2" => bent_connector(2, adjustments.get("adj1").copied()),
"bentConnector3" => bent_connector(3, adjustments.get("adj1").copied()),
"bentConnector4" => bent_connector(4, adjustments.get("adj1").copied()),
"bentConnector5" => bent_connector(5, adjustments.get("adj1").copied()),
"curvedConnector2" => curved_connector(2),
"curvedConnector3" => curved_connector(3),
"curvedConnector4" => curved_connector(4),
"curvedConnector5" => curved_connector(5),
"rightArrow" => arrow("right", adjustments.get("adj2").copied()),
"leftArrow" => arrow("left", adjustments.get("adj2").copied()),
"upArrow" => arrow("up", adjustments.get("adj2").copied()),
"downArrow" => arrow("down", adjustments.get("adj2").copied()),
"leftRightArrow" => polygon(&[
(0.0, 0.5),
(0.25, 0.0),
(0.25, 0.25),
(0.75, 0.25),
(0.75, 0.0),
(1.0, 0.5),
(0.75, 1.0),
(0.75, 0.75),
(0.25, 0.75),
(0.25, 1.0),
]),
"upDownArrow" => polygon(&[
(0.5, 0.0),
(1.0, 0.25),
(0.75, 0.25),
(0.75, 0.75),
(1.0, 0.75),
(0.5, 1.0),
(0.0, 0.75),
(0.25, 0.75),
(0.25, 0.25),
(0.0, 0.25),
]),
"chevron" => polygon(&[
(0.0, 0.0),
(0.65, 0.0),
(1.0, 0.5),
(0.65, 1.0),
(0.0, 1.0),
(0.35, 0.5),
]),
"homePlate" => polygon(&[(0.0, 0.0), (0.75, 0.0), (1.0, 0.5), (0.75, 1.0), (0.0, 1.0)]),
"flowChartProcess"
| "flowChartAlternateProcess"
| "flowChartPredefinedProcess"
| "flowChartInternalStorage"
| "flowChartPreparation"
| "flowChartManualOperation"
| "flowChartMagneticTape"
| "flowChartMagneticDisk"
| "flowChartMagneticDrum"
| "flowChartDisplay"
| "textBox" => preset_geometry_to_path("rect", adjustments)?,
"flowChartConnector" => preset_geometry_to_path("ellipse", adjustments)?,
"flowChartInputOutput" | "flowChartManualInput" => {
preset_geometry_to_path("parallelogram", adjustments)?
}
"flowChartTerminator" => preset_geometry_to_path("roundRect", adjustments)?,
_ => return None,
};
Some(result)
}
fn polygon(points: &[(f64, f64)]) -> Vec<GeometryPathCommand> {
let mut commands = points
.iter()
.enumerate()
.map(|(i, &(x, y))| {
if i == 0 {
GeometryPathCommand::Move { x, y }
} else {
GeometryPathCommand::Line { x, y }
}
})
.collect::<Vec<_>>();
if !points.is_empty() {
commands.push(GeometryPathCommand::Close);
}
commands
}
fn regular_polygon(sides: usize) -> Vec<GeometryPathCommand> {
polygon(
&(0..sides)
.map(|i| {
let a = -std::f64::consts::PI / 2.0
+ i as f64 * std::f64::consts::PI * 2.0 / sides as f64;
(0.5 + a.cos() * 0.5, 0.5 + a.sin() * 0.5)
})
.collect::<Vec<_>>(),
)
}
fn star(points: usize) -> Vec<GeometryPathCommand> {
polygon(
&(0..points * 2)
.map(|i| {
let a =
-std::f64::consts::PI / 2.0 + i as f64 * std::f64::consts::PI / points as f64;
let r = if i % 2 == 0 { 0.5 } else { 0.225 };
(0.5 + a.cos() * r, 0.5 + a.sin() * r)
})
.collect::<Vec<_>>(),
)
}
fn clamp_fraction(value: Option<f64>, fallback: f64) -> f64 {
value
.filter(|v| v.is_finite())
.map(|v| if v > 1.0 { v / 100_000.0 } else { v })
.unwrap_or(fallback)
.clamp(0.0, 1.0)
}
fn arrow(direction: &str, adjustment: Option<f64>) -> Vec<GeometryPathCommand> {
let h = clamp_fraction(adjustment, 0.5);
polygon(&[
(0.0, 0.25),
(1.0 - h, 0.25),
(1.0 - h, 0.0),
(1.0, 0.5),
(1.0 - h, 1.0),
(1.0 - h, 0.75),
(0.0, 0.75),
])
.into_iter()
.map(|command| match command {
GeometryPathCommand::Move { x, y } => {
let (x, y) = orient(direction, x, y);
GeometryPathCommand::Move { x, y }
}
GeometryPathCommand::Line { x, y } => {
let (x, y) = orient(direction, x, y);
GeometryPathCommand::Line { x, y }
}
command => command,
})
.collect()
}
fn orient(direction: &str, x: f64, y: f64) -> (f64, f64) {
match direction {
"left" => (1.0 - x, y),
"up" => (y, 1.0 - x),
"down" => (y, x),
_ => (x, y),
}
}
fn bent_connector(segments: usize, adjustment: Option<f64>) -> Vec<GeometryPathCommand> {
let bend = clamp_fraction(adjustment, 0.5);
if segments <= 2 {
return vec![
GeometryPathCommand::Move { x: 0.0, y: 0.0 },
GeometryPathCommand::Line { x: bend, y: 0.0 },
GeometryPathCommand::Line { x: bend, y: 1.0 },
GeometryPathCommand::Line { x: 1.0, y: 1.0 },
];
}
let mut commands = vec![GeometryPathCommand::Move { x: 0.0, y: 0.0 }];
for i in 1..segments {
let fraction = i as f64 / segments as f64;
let (x, y) = if i % 2 == 1 {
(
if i == 1 { bend } else { fraction },
(i - 1) as f64 / segments as f64,
)
} else {
((i - 1) as f64 / segments as f64, fraction)
};
commands.push(GeometryPathCommand::Line { x, y });
}
commands.push(GeometryPathCommand::Line { x: 1.0, y: 1.0 });
commands
}
fn curved_connector(segments: usize) -> Vec<GeometryPathCommand> {
if segments <= 2 {
return vec![
GeometryPathCommand::Move { x: 0.0, y: 0.0 },
GeometryPathCommand::Cubic {
cp1x: 0.5,
cp1y: 0.0,
cp2x: 0.5,
cp2y: 1.0,
x: 1.0,
y: 1.0,
},
];
}
let mut commands = vec![GeometryPathCommand::Move { x: 0.0, y: 0.0 }];
for i in 0..segments - 1 {
let start = i as f64 / (segments - 1) as f64;
let end = (i + 1) as f64 / (segments - 1) as f64;
commands.push(GeometryPathCommand::Cubic {
cp1x: start + (end - start) * 0.5,
cp1y: start,
cp2x: start + (end - start) * 0.5,
cp2y: end,
x: end,
y: end,
});
}
commands
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compiles_common_presets_and_rejects_unknown_shapes() {
let adjustments = HashMap::new();
assert!(preset_geometry_to_path("rect", &adjustments).is_some());
assert!(preset_geometry_to_path("ellipse", &adjustments).is_some());
assert!(preset_geometry_to_path("rightArrow", &adjustments).is_some());
assert!(preset_geometry_to_path("unknown", &adjustments).is_none());
}
}