use std::collections::{HashMap, HashSet};
use crate::{common::decomposition::DecomposedAffine, BabelfontError, Layer, Path, Shape};
use fontdrasil::{
coords::{DesignSpace, Location, NormalizedSpace},
types::Axes,
variations::VariationModel,
};
use kurbo::Affine;
pub(crate) fn interpolate_layer(
glyphname: &str,
layers_locations: &[(Location<DesignSpace>, &Layer)],
axes: &Axes,
target_location: &Location<NormalizedSpace>,
) -> Result<Layer, BabelfontError> {
let mut new_layer = Layer::default();
let locations_in_order = layers_locations
.iter()
.map(|(loc, _)| loc.to_normalized(axes))
.collect::<Vec<Location<NormalizedSpace>>>();
let locations: HashSet<Location<NormalizedSpace>> =
locations_in_order.iter().cloned().collect();
let model = VariationModel::new(locations, axes.axis_order());
let first_layer = layers_locations
.first()
.ok_or_else(|| BabelfontError::GlyphNotInterpolatable {
glyph: glyphname.to_string(),
reason: "No layers found".to_string(),
})?
.1;
for (i, shape) in first_layer.shapes.iter().enumerate() {
let all_shapes =
layers_locations
.iter()
.map(|(_, layer)| {
layer.shapes.get(i).cloned().ok_or_else(|| {
BabelfontError::GlyphNotInterpolatable {
glyph: glyphname.to_string(),
reason: format!("Layer missing shape index {}", i),
}
})
})
.collect::<Result<Vec<crate::shape::Shape>, BabelfontError>>()?;
if !all_shapes
.iter()
.all(|s| std::mem::discriminant(s) == std::mem::discriminant(shape))
{
return Err(BabelfontError::GlyphNotInterpolatable {
glyph: glyphname.to_string(),
reason: format!("Shape index {} has differing types across layers", i),
});
}
new_layer.shapes.push(Shape::interpolate(
glyphname,
&model,
&all_shapes,
&locations_in_order,
target_location,
)?);
}
let all_anchors: HashSet<String> = layers_locations
.iter()
.flat_map(|(_, layer)| layer.anchors.iter().map(|a| a.name.clone()))
.collect();
for anchor_name in all_anchors {
if layers_locations
.iter()
.any(|(_, layer)| !layer.anchors.iter().any(|a| a.name == anchor_name))
{
return Err(BabelfontError::GlyphNotInterpolatable {
glyph: glyphname.to_string(),
reason: format!("Anchor '{}' missing in some layers", anchor_name),
});
}
let anchor_positions = layers_locations
.iter()
.map(|(loc, layer)| {
#[allow(clippy::unwrap_used)] let anchor = layer
.anchors
.iter()
.find(|a| a.name == anchor_name)
.unwrap();
Ok((loc.to_normalized(axes), vec![anchor.x, anchor.y]))
})
.collect::<Result<HashMap<Location<NormalizedSpace>, Vec<f64>>, BabelfontError>>()?;
let deltas = model.deltas(&anchor_positions)?;
let interpolated_pos = model.interpolate_from_deltas(target_location, &deltas);
new_layer.anchors.push(crate::anchor::Anchor {
name: anchor_name,
x: interpolated_pos[0],
y: interpolated_pos[1],
format_specific: Default::default(), });
}
let mut width_positions: HashMap<Location<NormalizedSpace>, Vec<f64>> = HashMap::new();
for (loc, layer) in layers_locations {
width_positions.insert(loc.to_normalized(axes), vec![layer.width as f64]);
}
let width_deltas = model.deltas(&width_positions)?;
let interpolated_width = model.interpolate_from_deltas(target_location, &width_deltas);
new_layer.width = interpolated_width[0] as f32;
Ok(new_layer)
}
impl Shape {
fn interpolate(
glyph: &str,
model: &VariationModel,
shapes: &[Shape],
locations: &[Location<NormalizedSpace>],
target_location: &Location<NormalizedSpace>,
) -> Result<Shape, BabelfontError> {
match &shapes[0] {
Shape::Path(_) => {
let all_paths = shapes
.iter()
.map(|s| {
if let Shape::Path(p) = s {
Ok(p.clone())
} else {
Err(BabelfontError::GlyphNotInterpolatable {
glyph: glyph.to_string(),
reason: "Expected Path shape".to_string(),
})
}
})
.collect::<Result<Vec<Path>, BabelfontError>>()?;
let first_signature = all_paths[0].signature();
if !all_paths.iter().all(|p| p.signature() == first_signature) {
return Err(BabelfontError::GlyphNotInterpolatable {
glyph: glyph.to_string(),
reason: "Path node types do not match".to_string(),
});
}
let mut coordinate_lists: HashMap<Location<NormalizedSpace>, Vec<f64>> =
HashMap::new();
for (path, location) in all_paths.iter().zip(locations.iter()) {
coordinate_lists.insert(location.clone(), path.to_coordinate_list());
}
let deltas = model.deltas(&coordinate_lists)?;
let interpolated_coords = model.interpolate_from_deltas(target_location, &deltas);
let new_path =
all_paths[0].from_coordinate_list_and_signature(&interpolated_coords);
Ok(Shape::Path(new_path))
}
Shape::Component(c) => {
if !shapes.iter().all(|s| {
if let Shape::Component(comp) = s {
comp.reference == c.reference
} else {
false
}
}) {
return Err(BabelfontError::GlyphNotInterpolatable {
glyph: glyph.to_string(),
reason: "Component references do not match".to_string(),
});
}
let mut position_lists: HashMap<Location<NormalizedSpace>, Vec<f64>> =
HashMap::new();
for (shape, location) in shapes.iter().zip(locations.iter()) {
if let Shape::Component(comp) = shape {
let decomposed: DecomposedAffine = comp.transform.into();
position_lists.insert(
location.clone(),
vec![
decomposed.translation.0,
decomposed.translation.1,
decomposed.scale.0,
decomposed.scale.1,
decomposed.rotation,
],
);
} else {
return Err(BabelfontError::GlyphNotInterpolatable {
glyph: glyph.to_string(),
reason: "Expected Component shape".to_string(),
});
}
}
let deltas = model.deltas(&position_lists)?;
let interpolated_params = model.interpolate_from_deltas(target_location, &deltas);
let new_component = crate::shape::Component {
reference: c.reference.clone(),
transform: Affine::IDENTITY
.then_translate((interpolated_params[0], interpolated_params[1]).into())
.then_scale_non_uniform(interpolated_params[2], interpolated_params[3])
.then_rotate(interpolated_params[4]),
format_specific: c.format_specific.clone(),
};
Ok(Shape::Component(new_component))
}
}
}
}
impl Path {
fn signature(&self) -> Vec<u8> {
let mut sig = vec![];
for node in &self.nodes {
sig.push(match node.nodetype {
crate::common::NodeType::Move => 0,
crate::common::NodeType::Line => 1,
crate::common::NodeType::Curve => 2,
crate::common::NodeType::QCurve => 3,
crate::common::NodeType::OffCurve => 4,
});
}
sig
}
fn to_coordinate_list(&self) -> Vec<f64> {
let mut coords = vec![];
for node in &self.nodes {
coords.push(node.x);
coords.push(node.y);
}
coords
}
#[allow(clippy::wrong_self_convention)]
fn from_coordinate_list_and_signature(&self, coords: &[f64]) -> Path {
let mut new_path = Path::default();
for (i, node) in self.nodes.iter().enumerate() {
let x = coords[i * 2];
let y = coords[i * 2 + 1];
new_path.nodes.push(crate::common::Node {
x,
y,
nodetype: node.nodetype,
smooth: node.smooth,
});
}
new_path.closed = self.closed;
new_path
}
}