diff --git a/crates/shift-bridge/src/bridge.rs b/crates/shift-bridge/src/bridge.rs index 12efa7a9..82c16c93 100644 --- a/crates/shift-bridge/src/bridge.rs +++ b/crates/shift-bridge/src/bridge.rs @@ -6,7 +6,7 @@ use napi_derive::napi; use serde::{Deserialize, Serialize}; use shift_backends::{ExportFormat, FontExportRequest, FontExportResult, FontExporter, FontView}; use shift_font::{ - BooleanOp, BulkNodePositionUpdates, ContourId, Font, Glyph, GlyphLayer, LayerId, PointId, + BooleanOp, BulkNodePositionUpdates, ContourId, Font, Glyph, GlyphState, LayerId, PointId, SourceId, }; use shift_wire::{ @@ -15,9 +15,7 @@ use shift_wire::{ NapiGlyphStructure, NapiGlyphStructureChange, NapiGlyphValueChange, NapiPointType, NapiSource, }, interpolation::{build_glyph_variation_data, build_masters, GlyphVariationBuild}, - state::apply_state_to_layer, - Axis, FontMetadata, FontMetrics, GlyphChangedEntities, GlyphRecord, GlyphState, GlyphStructure, - GlyphStructureChange, GlyphValueChange, Source, + Axis, FontMetadata, FontMetrics, GlyphRecord, Source, }; use shift_workspace::{FontWorkspace, GlyphLayerTarget, NewWorkspace}; use std::sync::{ @@ -515,41 +513,6 @@ impl Bridge { }) } - fn edit_glyph_layer( - &mut self, - glyph_ref: GlyphLayerRef, - edit: impl FnOnce(&mut GlyphLayer) -> std::result::Result, - changes: impl FnOnce( - &shift_font::GlyphLayerChangeTarget, - &GlyphLayer, - &R, - ) -> shift_font::FontChangeSet, - ) -> BridgeResult { - let target = self.glyph_layer_target(glyph_ref)?; - Ok( - self - .workspace_mut()? - .edit_glyph_layer(target, edit, changes)?, - ) - } - - fn one_change(change: shift_font::FontChange) -> shift_font::FontChangeSet { - shift_font::FontChangeSet::new(vec![change]) - } - - fn layer_replaced_change( - target: &shift_font::GlyphLayerChangeTarget, - layer: &GlyphLayer, - _result: &impl Sized, - ) -> shift_font::FontChangeSet { - Self::one_change(shift_font::FontChange::LayerGeometryReplaced( - shift_font::LayerGeometryReplaced { - target: target.clone(), - layer: shift_font::GlyphLayerValue::from(layer), - }, - )) - } - fn variation_build_for_glyph( &self, glyph: &Glyph, @@ -710,18 +673,8 @@ impl Bridge { glyph_ref: GlyphLayerRef, width: f64, ) -> errors::Result { - let change = self.edit_glyph_layer( - glyph_ref, - |layer| { - layer.set_x_advance(width); - Ok(GlyphValueChange::from_layer(layer, Default::default())) - }, - |target, layer, _change| { - Self::one_change(shift_font::FontChange::LayerMetricsChanged( - shift_font::LayerMetricsChanged::from_layer(target.clone(), layer), - )) - }, - )?; + let target = self.glyph_layer_target(glyph_ref)?; + let change = self.workspace_mut()?.set_x_advance(target, width)?; self.mark_font_changed(); Ok(change.into()) @@ -734,14 +687,8 @@ impl Bridge { dx: f64, dy: f64, ) -> errors::Result { - let change = self.edit_glyph_layer( - glyph_ref, - |layer| { - layer.translate_layer(dx, dy); - Ok(GlyphValueChange::from_layer(layer, Default::default())) - }, - Self::layer_replaced_change, - )?; + let target = self.glyph_layer_target(glyph_ref)?; + let change = self.workspace_mut()?.translate_layer(target, dx, dy)?; self.mark_font_changed(); Ok(change.into()) @@ -757,36 +704,12 @@ impl Bridge { point_type: NapiPointType, smooth: bool, ) -> errors::Result { + let target = self.glyph_layer_target(glyph_ref)?; let contour_id = parse::(&contour_id)?; let point_type = point_type.into(); - - let change = self.edit_glyph_layer( - glyph_ref, - |layer| { - let point_id = layer.add_point_to_contour(contour_id, x, y, point_type, smooth)?; - let changed = GlyphChangedEntities { - point_ids: vec![point_id], - ..Default::default() - }; - Ok(GlyphStructureChange::from_layer(layer, changed)) - }, - move |target, layer, change| { - let contour = layer - .contour(contour_id) - .map(shift_font::ContourValue::from); - contour - .map(|contour| { - Self::one_change(shift_font::FontChange::PointsAdded( - shift_font::PointsAdded { - target: target.clone(), - contour, - point_ids: change.changed.point_ids.clone(), - }, - )) - }) - .unwrap_or_default() - }, - )?; + let change = self + .workspace_mut()? + .add_point(target, contour_id, x, y, point_type, smooth)?; self.mark_font_changed(); Ok(change.into()) @@ -802,37 +725,16 @@ impl Bridge { point_type: NapiPointType, smooth: bool, ) -> errors::Result { + let target = self.glyph_layer_target(glyph_ref)?; let before_point_id = parse::(&before_point_id)?; let point_type = point_type.into(); - - let change = self.edit_glyph_layer( - glyph_ref, - |layer| { - let point_id = layer.insert_point_before(before_point_id, x, y, point_type, smooth)?; - let changed = GlyphChangedEntities { - point_ids: vec![point_id], - ..Default::default() - }; - Ok(GlyphStructureChange::from_layer(layer, changed)) - }, - move |target, layer, change| { - let Some(contour_id) = layer.find_point_contour(before_point_id) else { - return Self::layer_replaced_change(target, layer, change); - }; - let Some(contour) = layer - .contour(contour_id) - .map(shift_font::ContourValue::from) - else { - return Self::layer_replaced_change(target, layer, change); - }; - Self::one_change(shift_font::FontChange::PointsAdded( - shift_font::PointsAdded { - target: target.clone(), - contour, - point_ids: change.changed.point_ids.clone(), - }, - )) - }, + let change = self.workspace_mut()?.insert_point_before( + target, + before_point_id, + x, + y, + point_type, + smooth, )?; self.mark_font_changed(); @@ -840,33 +742,12 @@ impl Bridge { } #[napi] - pub fn add_contour(&mut self, glyph_ref: GlyphLayerRef) -> Result { - let change = self.edit_glyph_layer( - glyph_ref, - |layer| { - let contour_id = layer.add_empty_contour(); - let changed = GlyphChangedEntities { - contour_ids: vec![contour_id], - ..Default::default() - }; - Ok(GlyphStructureChange::from_layer(layer, changed)) - }, - |target, layer, change| { - let Some(contour) = layer - .contours_iter() - .last() - .map(shift_font::ContourValue::from) - else { - return Self::layer_replaced_change(target, layer, change); - }; - Self::one_change(shift_font::FontChange::ContourAdded( - shift_font::ContourAdded { - target: target.clone(), - contour, - }, - )) - }, - )?; + pub fn add_contour( + &mut self, + glyph_ref: GlyphLayerRef, + ) -> errors::Result { + let target = self.glyph_layer_target(glyph_ref)?; + let change = self.workspace_mut()?.add_contour(target)?; self.mark_font_changed(); Ok(change.into()) @@ -878,27 +759,9 @@ impl Bridge { glyph_ref: GlyphLayerRef, #[napi(ts_arg_type = "ContourId")] contour_id: String, ) -> errors::Result { + let target = self.glyph_layer_target(glyph_ref)?; let contour_id = parse::(&contour_id)?; - let change = self.edit_glyph_layer( - glyph_ref, - |layer| { - layer.open_contour(contour_id)?; - let changed = GlyphChangedEntities { - contour_ids: vec![contour_id], - ..Default::default() - }; - Ok(GlyphStructureChange::from_layer(layer, changed)) - }, - move |target, _layer, _change| { - Self::one_change(shift_font::FontChange::ContourOpenClosedChanged( - shift_font::ContourOpenClosedChanged { - target: target.clone(), - contour_id, - closed: false, - }, - )) - }, - )?; + let change = self.workspace_mut()?.open_contour(target, contour_id)?; self.mark_font_changed(); Ok(change.into()) @@ -910,27 +773,9 @@ impl Bridge { glyph_ref: GlyphLayerRef, #[napi(ts_arg_type = "ContourId")] contour_id: String, ) -> errors::Result { + let target = self.glyph_layer_target(glyph_ref)?; let contour_id = parse::(&contour_id)?; - let change = self.edit_glyph_layer( - glyph_ref, - |layer| { - layer.close_contour(contour_id)?; - let changed = GlyphChangedEntities { - contour_ids: vec![contour_id], - ..Default::default() - }; - Ok(GlyphStructureChange::from_layer(layer, changed)) - }, - move |target, _layer, _change| { - Self::one_change(shift_font::FontChange::ContourOpenClosedChanged( - shift_font::ContourOpenClosedChanged { - target: target.clone(), - contour_id, - closed: true, - }, - )) - }, - )?; + let change = self.workspace_mut()?.close_contour(target, contour_id)?; self.mark_font_changed(); Ok(change.into()) @@ -942,19 +787,9 @@ impl Bridge { glyph_ref: GlyphLayerRef, #[napi(ts_arg_type = "ContourId")] contour_id: String, ) -> errors::Result { + let target = self.glyph_layer_target(glyph_ref)?; let contour_id = parse::(&contour_id)?; - let change = self.edit_glyph_layer( - glyph_ref, - |layer| { - layer.reverse_contour(contour_id)?; - let changed = GlyphChangedEntities { - contour_ids: vec![contour_id], - ..Default::default() - }; - Ok(GlyphStructureChange::from_layer(layer, changed)) - }, - Self::layer_replaced_change, - )?; + let change = self.workspace_mut()?.reverse_contour(target, contour_id)?; self.mark_font_changed(); Ok(change.into()) @@ -968,6 +803,7 @@ impl Bridge { #[napi(ts_arg_type = "ContourId")] contour_id_b: String, operation: String, ) -> errors::Result { + let target = self.glyph_layer_target(glyph_ref)?; let cid_a = parse::(&contour_id_a)?; let cid_b = parse::(&contour_id_b)?; @@ -983,19 +819,9 @@ impl Bridge { }); } }; - - let change = self.edit_glyph_layer( - glyph_ref, - |layer| { - let created_ids = layer.apply_boolean_op(cid_a, cid_b, op)?; - let changed = GlyphChangedEntities { - contour_ids: created_ids, - ..Default::default() - }; - Ok(GlyphStructureChange::from_layer(layer, changed)) - }, - Self::layer_replaced_change, - )?; + let change = self + .workspace_mut()? + .apply_boolean_op(target, cid_a, cid_b, op)?; self.mark_font_changed(); Ok(change.into()) @@ -1007,18 +833,10 @@ impl Bridge { glyph_ref: GlyphLayerRef, #[napi(ts_arg_type = "Array")] point_ids: Vec, ) -> errors::Result { + let target = self.glyph_layer_target(glyph_ref)?; let point_ids: BridgeResult> = point_ids.iter().map(|id| parse::(id)).collect(); let point_ids = point_ids?; - - let change = self.edit_glyph_layer( - glyph_ref, - |layer| { - layer.remove_points(&point_ids)?; - let changed = GlyphChangedEntities::points(point_ids); - Ok(GlyphStructureChange::from_layer(layer, changed)) - }, - Self::layer_replaced_change, - )?; + let change = self.workspace_mut()?.remove_points(target, point_ids)?; self.mark_font_changed(); Ok(change.into()) @@ -1030,35 +848,9 @@ impl Bridge { glyph_ref: GlyphLayerRef, #[napi(ts_arg_type = "PointId")] point_id: String, ) -> errors::Result { - let parsed_id = parse::(&point_id)?; - let change = self.edit_glyph_layer( - glyph_ref, - |layer| { - layer.toggle_smooth(parsed_id)?; - let changed = GlyphChangedEntities { - point_ids: vec![parsed_id], - ..Default::default() - }; - Ok(GlyphStructureChange::from_layer(layer, changed)) - }, - move |target, layer, change| { - let smooth = layer - .contours_iter() - .find_map(|contour| contour.get_point(parsed_id)) - .map(|point| point.is_smooth()); - smooth - .map(|smooth| { - Self::one_change(shift_font::FontChange::PointSmoothChanged( - shift_font::PointSmoothChanged { - target: target.clone(), - point_id: parsed_id, - smooth, - }, - )) - }) - .unwrap_or_else(|| Self::layer_replaced_change(target, layer, change)) - }, - )?; + let target = self.glyph_layer_target(glyph_ref)?; + let point_id = parse::(&point_id)?; + let change = self.workspace_mut()?.toggle_smooth(target, point_id)?; self.mark_font_changed(); Ok(change.into()) @@ -1075,47 +867,35 @@ impl Bridge { anchor_ids: Option, anchor_coords: Option, ) -> errors::Result<()> { + let target = self.glyph_layer_target(glyph_ref)?; let point_position_changes = read_point_position_changes(&point_ids, &point_coords)?; let has_anchor_updates = anchor_ids.as_ref().is_some_and(|ids| !ids.is_empty()) || anchor_coords .as_ref() .is_some_and(|coords| !coords.is_empty()); - - self.edit_glyph_layer( - glyph_ref, - |layer| { - layer.apply_bulk_node_positions(BulkNodePositionUpdates { - point_ids: point_ids.as_ref().map(|ids| { - let ids: &[u64] = ids; - ids - }), - point_coords: point_coords.as_ref().map(|coords| { - let coords: &[f64] = coords; - coords - }), - anchor_ids: anchor_ids.as_ref().map(|ids| { - let ids: &[u64] = ids; - ids - }), - anchor_coords: anchor_coords.as_ref().map(|coords| { - let coords: &[f64] = coords; - coords - }), - })?; - Ok(()) - }, - move |target, layer, result| { - if has_anchor_updates { - return Self::layer_replaced_change(target, layer, result); - } - - Self::one_change(shift_font::FontChange::PointPositionsChanged( - shift_font::PointPositionsChanged { - target: target.clone(), - points: point_position_changes, - }, - )) - }, + let updates = BulkNodePositionUpdates { + point_ids: point_ids.as_ref().map(|ids| { + let ids: &[u64] = ids; + ids + }), + point_coords: point_coords.as_ref().map(|coords| { + let coords: &[f64] = coords; + coords + }), + anchor_ids: anchor_ids.as_ref().map(|ids| { + let ids: &[u64] = ids; + ids + }), + anchor_coords: anchor_coords.as_ref().map(|coords| { + let coords: &[f64] = coords; + coords + }), + }; + self.workspace_mut()?.apply_position_patch( + target, + updates, + point_position_changes, + has_anchor_updates, )?; self.mark_font_changed(); @@ -1129,17 +909,12 @@ impl Bridge { structure: NapiGlyphStructure, values: Float64Array, ) -> errors::Result { - let structure = GlyphStructure::from(structure); + let target = self.glyph_layer_target(glyph_ref)?; + let structure = shift_font::GlyphStructure::from(structure); let values: &[f64] = &values; - - let change = self.edit_glyph_layer( - glyph_ref, - |layer| { - apply_state_to_layer(layer, &structure, values)?; - Ok(GlyphStructureChange::from_layer(layer, Default::default())) - }, - Self::layer_replaced_change, - )?; + let change = self + .workspace_mut()? + .restore_state(target, &structure, values)?; self.mark_font_changed(); Ok(change.into()) @@ -1482,6 +1257,10 @@ mod tests { layer_id: "not-a-layer-id".to_string(), }); - assert!(result.err().unwrap().reason.contains("invalid layer ID")); + assert!(result + .err() + .unwrap() + .to_string() + .contains("invalid layer ID")); } } diff --git a/crates/shift-font/src/lib.rs b/crates/shift-font/src/lib.rs index 04fa4228..5f2e4a9c 100644 --- a/crates/shift-font/src/lib.rs +++ b/crates/shift-font/src/lib.rs @@ -4,6 +4,7 @@ pub mod curve; pub mod error; pub mod ir; pub mod layer_edit; +pub mod state; pub use changes::*; pub use error::{CoreError, CoreResult}; @@ -15,3 +16,4 @@ pub use ir::{ pub use layer_edit::{ BulkNodePositionUpdates, ChangedEntities, EditableNode, PasteContour, PastePoint, PasteResult, }; +pub use state::*; diff --git a/crates/shift-font/src/state.rs b/crates/shift-font/src/state.rs new file mode 100644 index 00000000..9879fabf --- /dev/null +++ b/crates/shift-font/src/state.rs @@ -0,0 +1,634 @@ +//! Canonical glyph state and edit-result types for Shift's font model. +//! +//! These types split stable glyph structure from mutable numeric values. The +//! values layout is canonical and must stay in lockstep with every consumer. + +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; + +use crate::{ + error::{CoreError, CoreResult}, + Anchor, AnchorId, Component, ComponentId, Contour, ContourId, DecomposedTransform as Transform, + GlyphLayer, GlyphName, GuidelineId, Location, Point, PointId, PointType, +}; + +/// Flat numeric glyph values ordered to match `GlyphStructure`. +/// +/// This layout is structure-dependent: +/// +/// 1. x advance +/// 2. contour point positions, in `GlyphStructure.contours` order: +/// `x, y` for each point +/// 3. anchor positions, in `GlyphStructure.anchors` order: +/// `x, y` for each anchor +/// 4. component transforms, in `GlyphStructure.components` order: +/// `translateX, translateY, rotation, scaleX, scaleY, +/// skewX, skewY, tCenterX, tCenterY` for each component +pub type GlyphValue = f64; + +pub type GlyphValues = Vec; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GlyphState { + pub structure: GlyphStructure, + /// Numeric glyph state ordered to match `GlyphStructure`. + pub values: GlyphValues, + pub variation_data: Option, +} + +impl GlyphState { + pub fn from_layer(layer: &GlyphLayer, variation_data: Option) -> Self { + Self { + structure: GlyphStructure::from(layer), + values: values_from_layer(layer), + variation_data, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GlyphStructure { + pub contours: Vec, + pub anchors: Vec, + pub components: Vec, +} + +impl From<&GlyphLayer> for GlyphStructure { + fn from(layer: &GlyphLayer) -> Self { + Self { + contours: layer.contours_iter().map(ContourData::from).collect(), + anchors: layer.anchors_iter().map(AnchorData::from).collect(), + components: sorted_components(layer) + .into_iter() + .map(ComponentData::from) + .collect(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ContourData { + pub id: String, + pub points: Vec, + pub closed: bool, +} + +impl From<&Contour> for ContourData { + fn from(contour: &Contour) -> Self { + Self { + id: contour.id().to_string(), + points: contour.points().iter().map(PointData::from).collect(), + closed: contour.is_closed(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PointData { + pub id: String, + pub point_type: PointType, + pub smooth: bool, +} + +impl From<&Point> for PointData { + fn from(point: &Point) -> Self { + Self { + id: point.id().to_string(), + point_type: point.point_type(), + smooth: point.is_smooth(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AnchorData { + pub id: String, + pub name: Option, +} + +impl From<&Anchor> for AnchorData { + fn from(anchor: &Anchor) -> Self { + Self { + id: anchor.id().to_string(), + name: anchor.name().map(str::to_owned), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ComponentData { + pub id: String, + pub base_glyph_name: GlyphName, +} + +impl From<&Component> for ComponentData { + fn from(component: &Component) -> Self { + Self { + id: component.id().to_string(), + base_glyph_name: component.base_glyph().clone(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct GlyphChangedEntities { + pub point_ids: Vec, + pub contour_ids: Vec, + pub anchor_ids: Vec, + pub guideline_ids: Vec, + pub component_ids: Vec, +} + +impl GlyphChangedEntities { + pub fn point(id: PointId) -> Self { + Self { + point_ids: vec![id], + ..Default::default() + } + } + + pub fn points(ids: Vec) -> Self { + Self { + point_ids: ids, + ..Default::default() + } + } + + pub fn contour(id: ContourId) -> Self { + Self { + contour_ids: vec![id], + ..Default::default() + } + } + + pub fn contours(ids: Vec) -> Self { + Self { + contour_ids: ids, + ..Default::default() + } + } + + pub fn anchor(id: AnchorId) -> Self { + Self { + anchor_ids: vec![id], + ..Default::default() + } + } + + pub fn guideline(id: GuidelineId) -> Self { + Self { + guideline_ids: vec![id], + ..Default::default() + } + } + + pub fn component(id: ComponentId) -> Self { + Self { + component_ids: vec![id], + ..Default::default() + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GlyphValueChange { + pub values: GlyphValues, + pub changed: GlyphChangedEntities, +} + +impl GlyphValueChange { + pub fn from_layer(layer: &GlyphLayer, changed: GlyphChangedEntities) -> Self { + Self { + values: values_from_layer(layer), + changed, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GlyphStructureChange { + pub structure: GlyphStructure, + pub values: GlyphValues, + pub changed: GlyphChangedEntities, +} + +impl GlyphStructureChange { + pub fn from_layer(layer: &GlyphLayer, changed: GlyphChangedEntities) -> Self { + Self { + structure: GlyphStructure::from(layer), + values: values_from_layer(layer), + changed, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AxisTent { + pub axis_tag: String, + pub lower: f64, + pub peak: f64, + pub upper: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GlyphVariationData { + /// One entry per region. Inner = tents on the axes the region depends on. + pub regions: Vec>, + /// Deltas are flattened in `GlyphState::values` order. + pub deltas: Vec>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GlyphMaster { + pub source_id: String, + pub source_name: String, + pub is_default_source: bool, + pub location: Location, + pub structure: GlyphStructure, + pub values: GlyphValues, +} + +/// Flatten mutable numeric glyph state in the order described by `GlyphState::values`. +pub fn values_from_layer(layer: &GlyphLayer) -> GlyphValues { + let mut values = Vec::new(); + values.push(layer.width()); + + for contour in layer.contours_iter() { + for point in contour.points() { + values.push(point.x()); + values.push(point.y()); + } + } + + for anchor in layer.anchors_iter() { + values.push(anchor.x()); + values.push(anchor.y()); + } + + for component in sorted_components(layer) { + push_transform_values(&mut values, component.transform()); + } + + values +} + +/// Builds a layer from canonical glyph structure and value buffers. +pub fn layer_from_state( + structure: &GlyphStructure, + values: &[GlyphValue], +) -> CoreResult { + let mut layer = GlyphLayer::new(); + apply_state_to_layer(&mut layer, structure, values)?; + Ok(layer) +} + +/// Replaces a layer's editable geometry from canonical glyph structure and values. +pub fn apply_state_to_layer( + layer: &mut GlyphLayer, + structure: &GlyphStructure, + values: &[GlyphValue], +) -> CoreResult<()> { + let mut cursor = GlyphValueCursor::new(values); + let width = cursor.read_x_advance()?; + + layer.clear_contours(); + layer.clear_anchors(); + layer.clear_components(); + layer.set_width(width); + + restore_contours(layer, &structure.contours, &mut cursor)?; + restore_anchors(layer, &structure.anchors, &mut cursor)?; + restore_components(layer, &structure.components, &mut cursor)?; + + cursor.finish()?; + Ok(()) +} + +/// Components live in a map in the IR, but the values array needs stable +/// ordering. Sort by component ID everywhere structure and values are exported. +fn sorted_components(layer: &GlyphLayer) -> Vec<&Component> { + let mut components: Vec<_> = layer.components_iter().collect(); + components.sort_by_key(|component| component.id().raw()); + components +} + +fn push_transform_values(values: &mut Vec, transform: &Transform) { + values.push(transform.translate_x); + values.push(transform.translate_y); + values.push(transform.rotation); + values.push(transform.scale_x); + values.push(transform.scale_y); + values.push(transform.skew_x); + values.push(transform.skew_y); + values.push(transform.t_center_x); + values.push(transform.t_center_y); +} + +#[derive(Debug, Clone, Copy)] +struct PointPosition { + x: GlyphValue, + y: GlyphValue, +} + +struct GlyphValueCursor<'a> { + values: &'a [GlyphValue], + index: usize, +} + +impl<'a> GlyphValueCursor<'a> { + fn new(values: &'a [GlyphValue]) -> Self { + Self { values, index: 0 } + } + + fn read_x_advance(&mut self) -> CoreResult { + self.next() + } + + fn read_point(&mut self) -> CoreResult { + Ok(PointPosition { + x: self.next()?, + y: self.next()?, + }) + } + + fn read_component_transform(&mut self) -> CoreResult { + Ok(Transform { + translate_x: self.next()?, + translate_y: self.next()?, + rotation: self.next()?, + scale_x: self.next()?, + scale_y: self.next()?, + skew_x: self.next()?, + skew_y: self.next()?, + t_center_x: self.next()?, + t_center_y: self.next()?, + }) + } + + fn finish(self) -> CoreResult<()> { + if self.index == self.values.len() { + Ok(()) + } else { + Err(CoreError::TrailingGlyphValues { + expected: self.index, + actual: self.values.len(), + }) + } + } + + fn next(&mut self) -> CoreResult { + let idx = self.index; + let value = self + .values + .get(idx) + .copied() + .ok_or(CoreError::MissingGlyphValue { index: idx })?; + + self.index += 1; + Ok(value) + } +} + +fn parse_id(value: &str, invalid: impl FnOnce(String) -> CoreError) -> CoreResult +where + T: FromStr, +{ + value.parse().map_err(|_| invalid(value.to_string())) +} + +fn restore_contours( + layer: &mut GlyphLayer, + contours: &[ContourData], + cursor: &mut GlyphValueCursor<'_>, +) -> CoreResult<()> { + for contour_data in contours { + let contour_id: ContourId = parse_id(&contour_data.id, CoreError::InvalidContourId)?; + let mut new_contour = Contour::with_id(contour_id); + + for point in &contour_data.points { + let point_id: PointId = parse_id(&point.id, CoreError::InvalidPointId)?; + let new_pos = cursor.read_point()?; + new_contour.add_point_with_id( + point_id, + new_pos.x, + new_pos.y, + point.point_type, + point.smooth, + ); + } + + if contour_data.closed { + new_contour.close(); + } + + layer.add_contour(new_contour); + } + + Ok(()) +} + +fn restore_anchors( + layer: &mut GlyphLayer, + anchors: &[AnchorData], + cursor: &mut GlyphValueCursor<'_>, +) -> CoreResult<()> { + for anchor_data in anchors { + let anchor_id: AnchorId = parse_id(&anchor_data.id, CoreError::InvalidAnchorId)?; + let position = cursor.read_point()?; + let anchor = Anchor::with_id(anchor_id, anchor_data.name.clone(), position.x, position.y); + + layer.add_anchor(anchor); + } + + Ok(()) +} + +fn restore_components( + layer: &mut GlyphLayer, + components: &[ComponentData], + cursor: &mut GlyphValueCursor<'_>, +) -> CoreResult<()> { + for component_data in components { + let component_id: ComponentId = + parse_id(&component_data.id, CoreError::InvalidComponentId)?; + let transform = cursor.read_component_transform()?; + let component = Component::with_id( + component_id, + component_data.base_glyph_name.clone(), + transform, + ); + + layer.add_component(component); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample_layer() -> GlyphLayer { + let mut layer = GlyphLayer::with_width(500.0); + + let mut contour = Contour::with_id(ContourId::from_raw(10)); + contour.add_point_with_id(PointId::from_raw(20), 1.0, 2.0, PointType::OnCurve, false); + contour.add_point_with_id(PointId::from_raw(21), 3.0, 4.0, PointType::OffCurve, true); + contour.close(); + layer.add_contour(contour); + + layer.add_anchor(Anchor::with_id( + AnchorId::from_raw(30), + Some("top".to_string()), + 5.0, + 6.0, + )); + + layer.add_component(Component::with_id( + ComponentId::from_raw(40), + "base".to_string(), + Transform { + translate_x: 7.0, + translate_y: 8.0, + rotation: 9.0, + scale_x: 10.0, + scale_y: 11.0, + skew_x: 12.0, + skew_y: 13.0, + t_center_x: 14.0, + t_center_y: 15.0, + }, + )); + + layer + } + + #[test] + fn values_from_layer_uses_canonical_order() { + let layer = sample_layer(); + + assert_eq!( + values_from_layer(&layer), + vec![ + 500.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, + 15.0, + ] + ); + } + + #[test] + fn layer_from_state_restores_ids_structure_and_values() -> CoreResult<()> { + let layer = sample_layer(); + let structure = GlyphStructure::from(&layer); + let restored = layer_from_state(&structure, &values_from_layer(&layer))?; + + assert_eq!(restored.width(), 500.0); + + let contour = restored.contour(ContourId::from_raw(10)).unwrap(); + assert!(contour.is_closed()); + assert_eq!(contour.points().len(), 2); + + let first = contour.get_point(PointId::from_raw(20)).unwrap(); + assert_eq!((first.x(), first.y()), (1.0, 2.0)); + assert_eq!(first.point_type(), PointType::OnCurve); + assert!(!first.is_smooth()); + + let second = contour.get_point(PointId::from_raw(21)).unwrap(); + assert_eq!((second.x(), second.y()), (3.0, 4.0)); + assert_eq!(second.point_type(), PointType::OffCurve); + assert!(second.is_smooth()); + + let anchor = restored.anchor(AnchorId::from_raw(30)).unwrap(); + assert_eq!(anchor.name(), Some("top")); + assert_eq!(anchor.position(), (5.0, 6.0)); + + let component = restored.component(ComponentId::from_raw(40)).unwrap(); + assert_eq!(component.base_glyph().as_str(), "base"); + assert_eq!(component.transform().translate_x, 7.0); + assert_eq!(component.transform().t_center_y, 15.0); + + Ok(()) + } + + #[test] + fn glyph_structure_sorts_components_by_id() { + let mut layer = GlyphLayer::new(); + layer.add_component(Component::with_id( + ComponentId::from_raw(200), + "later".to_string(), + Transform::identity(), + )); + layer.add_component(Component::with_id( + ComponentId::from_raw(100), + "earlier".to_string(), + Transform::identity(), + )); + + let structure = GlyphStructure::from(&layer); + + assert_eq!(structure.components[0].id, "100"); + assert_eq!(structure.components[1].id, "200"); + } + + #[test] + fn layer_from_state_rejects_missing_values() { + let structure = GlyphStructure { + contours: vec![], + anchors: vec![], + components: vec![], + }; + + assert!(matches!( + layer_from_state(&structure, &[]), + Err(CoreError::MissingGlyphValue { index: 0 }) + )); + } + + #[test] + fn layer_from_state_rejects_trailing_values() { + let structure = GlyphStructure { + contours: vec![], + anchors: vec![], + components: vec![], + }; + + assert!(matches!( + layer_from_state(&structure, &[500.0, 1.0]), + Err(CoreError::TrailingGlyphValues { + expected: 1, + actual: 2, + }) + )); + } + + #[test] + fn layer_from_state_rejects_invalid_contour_ids() { + let structure = GlyphStructure { + contours: vec![ContourData { + id: "not-a-contour-id".to_string(), + points: vec![], + closed: false, + }], + anchors: vec![], + components: vec![], + }; + + assert!(matches!( + layer_from_state(&structure, &[500.0]), + Err(CoreError::InvalidContourId(value)) if value == "not-a-contour-id" + )); + } +} diff --git a/crates/shift-wire/src/bridges/napi/mod.rs b/crates/shift-wire/src/bridges/napi/mod.rs index f8b3b952..c7eaa059 100644 --- a/crates/shift-wire/src/bridges/napi/mod.rs +++ b/crates/shift-wire/src/bridges/napi/mod.rs @@ -2,15 +2,14 @@ use std::collections::HashMap; use napi::bindgen_prelude::Float64Array; use napi_derive::napi; -use shift_font::PointType as IrPointType; - -use crate::{ - AnchorData, Axis, AxisTent, ComponentData, ContourData, FontMetadata, FontMetrics, - GlyphChangedEntities, GlyphMaster, GlyphRecord, GlyphState, GlyphStructure, - GlyphStructureChange, GlyphValueChange, GlyphVariationData, Location, PointData, PointType, - Source, +use shift_font::{ + AnchorData, AxisTent, ComponentData, ContourData, GlyphChangedEntities, GlyphMaster, + GlyphState, GlyphStructure, GlyphStructureChange, GlyphValueChange, GlyphVariationData, + Location as IrLocation, PointData, PointType, }; +use crate::{Axis, FontMetadata, FontMetrics, GlyphRecord, Location, Source}; + #[napi(string_enum = "camelCase")] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum NapiPointType { @@ -21,7 +20,7 @@ pub enum NapiPointType { impl From for NapiPointType { fn from(point_type: PointType) -> Self { match point_type { - PointType::OnCurve => Self::OnCurve, + PointType::OnCurve | PointType::QCurve => Self::OnCurve, PointType::OffCurve => Self::OffCurve, } } @@ -36,13 +35,6 @@ impl From for PointType { } } -impl From for IrPointType { - fn from(point_type: NapiPointType) -> Self { - let point_type: PointType = point_type.into(); - point_type.into() - } -} - #[napi(object)] pub struct NapiFontMetadata { pub family_name: Option, @@ -424,6 +416,17 @@ impl From for NapiLocation { } } +impl From for NapiLocation { + fn from(location: IrLocation) -> Self { + Self { + values: location + .iter() + .map(|(tag, value)| (tag.clone(), *value)) + .collect(), + } + } +} + #[napi(object)] pub struct NapiAxisTent { pub axis_tag: String, diff --git a/crates/shift-wire/src/interpolation.rs b/crates/shift-wire/src/interpolation.rs index 948d799c..33f2e7a8 100644 --- a/crates/shift-wire/src/interpolation.rs +++ b/crates/shift-wire/src/interpolation.rs @@ -4,10 +4,9 @@ use std::str::FromStr; use fontdrasil::coords::{NormalizedCoord, NormalizedLocation}; use fontdrasil::types::Tag; use fontdrasil::variations::VariationModel; -use shift_font::{Axis, Font, Glyph}; - -use crate::{ - values_from_layer, AxisTent, GlyphMaster, GlyphStructure, GlyphVariationData, Location, +use shift_font::{ + values_from_layer, Axis, AxisTent, Font, Glyph, GlyphMaster, GlyphStructure, + GlyphVariationData, Location, }; #[derive(Debug, Clone)] @@ -129,11 +128,7 @@ fn to_fd_wire_location(location: &Location, axes: &[Axis]) -> NormalizedLocation let mut result = NormalizedLocation::new(); for axis in axes { - let value = location - .values - .get(axis.tag()) - .copied() - .unwrap_or(axis.default()); + let value = location.get(axis.tag()).unwrap_or(axis.default()); let normalized = axis.normalize(value); let Ok(tag) = Tag::from_str(axis.tag()) else { continue; @@ -180,7 +175,7 @@ pub fn build_masters(font: &Font, glyph: &Glyph) -> Option> { source_id: source.id().to_string(), source_name: source.name().to_string(), is_default_source: default_source_id == Some(source.id()), - location: source.location().into(), + location: source.location().clone(), structure, values, }); @@ -260,7 +255,7 @@ pub fn get_glyph_variation_data( mod tests { use std::collections::HashMap; - use crate::{ContourData, GlyphMaster, GlyphStructure, Location, PointData, PointType}; + use shift_font::{ContourData, GlyphMaster, GlyphStructure, Location, PointData, PointType}; use super::build_glyph_variation_data; use shift_font::Axis; @@ -299,9 +294,7 @@ mod tests { source_id: source_name.to_string(), source_name: source_name.to_string(), is_default_source, - location: Location { - values: HashMap::from([("wght".to_string(), location_value)]), - }, + location: Location::from_map(HashMap::from([("wght".to_string(), location_value)])), structure: structure_with_smooth(smooth), values: vec![500.0, x_offset, 0.0, 100.0 + x_offset, 0.0], } diff --git a/crates/shift-wire/src/lib.rs b/crates/shift-wire/src/lib.rs index 46169193..22deec9e 100644 --- a/crates/shift-wire/src/lib.rs +++ b/crates/shift-wire/src/lib.rs @@ -7,32 +7,19 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; use shift_font::{ - Anchor as IrAnchor, AnchorId, Axis as IrAxis, Component as IrComponent, ComponentId, - Contour as IrContour, ContourId, DecomposedTransform as IrTransform, - FontMetadata as IrFontMetadata, FontMetrics as IrFontMetrics, Glyph as IrGlyph, GlyphLayer, - GlyphName, GuidelineId, LayerId, Location as IrLocation, Point as IrPoint, PointId, - PointType as IrPointType, Source as IrSource, SourceId, + Axis as IrAxis, FontMetadata as IrFontMetadata, FontMetrics as IrFontMetrics, Glyph as IrGlyph, + GlyphName, LayerId, Location as IrLocation, Source as IrSource, SourceId, +}; + +pub use shift_font::{ + apply_state_to_layer, layer_from_state, values_from_layer, AnchorData, AxisTent, ComponentData, + ContourData, GlyphChangedEntities, GlyphMaster, GlyphState, GlyphStructure, + GlyphStructureChange, GlyphValue, GlyphValueChange, GlyphValues, GlyphVariationData, PointData, + PointType, }; pub mod bridges; pub mod interpolation; -pub mod state; - -/// Flat numeric glyph values ordered to match `GlyphStructure`. -/// -/// This layout is structure-dependent: -/// -/// 1. x advance -/// 2. contour point positions, in `GlyphStructure.contours` order: -/// `x, y` for each point -/// 3. anchor positions, in `GlyphStructure.anchors` order: -/// `x, y` for each anchor -/// 4. component transforms, in `GlyphStructure.components` order: -/// `translateX, translateY, rotation, scaleX, scaleY, -/// skewX, skewY, tCenterX, tCenterY` for each component -pub type GlyphValue = f64; - -pub type GlyphValues = Vec; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -177,234 +164,6 @@ impl From<&IrGlyph> for GlyphRecord { } } -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GlyphState { - pub structure: GlyphStructure, - /// Numeric glyph state ordered to match `GlyphStructure`. - pub values: GlyphValues, - pub variation_data: Option, -} - -impl GlyphState { - pub fn from_layer(layer: &GlyphLayer, variation_data: Option) -> Self { - Self { - structure: GlyphStructure::from(layer), - values: values_from_layer(layer), - variation_data, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GlyphStructure { - pub contours: Vec, - pub anchors: Vec, - pub components: Vec, -} - -impl From<&GlyphLayer> for GlyphStructure { - fn from(layer: &GlyphLayer) -> Self { - Self { - contours: layer.contours_iter().map(ContourData::from).collect(), - anchors: layer.anchors_iter().map(AnchorData::from).collect(), - components: sorted_components(layer) - .into_iter() - .map(ComponentData::from) - .collect(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ContourData { - pub id: String, - pub points: Vec, - pub closed: bool, -} - -impl From<&IrContour> for ContourData { - fn from(contour: &IrContour) -> Self { - Self { - id: contour.id().to_string(), - points: contour.points().iter().map(PointData::from).collect(), - closed: contour.is_closed(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PointData { - pub id: String, - pub point_type: PointType, - pub smooth: bool, -} - -impl From<&IrPoint> for PointData { - fn from(point: &IrPoint) -> Self { - Self { - id: point.id().to_string(), - point_type: point.point_type().into(), - smooth: point.is_smooth(), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum PointType { - OnCurve, - OffCurve, -} - -impl From for PointType { - fn from(point_type: IrPointType) -> Self { - match point_type { - IrPointType::OnCurve | IrPointType::QCurve => Self::OnCurve, - IrPointType::OffCurve => Self::OffCurve, - } - } -} - -impl From for IrPointType { - fn from(point_type: PointType) -> Self { - match point_type { - PointType::OffCurve => IrPointType::OffCurve, - PointType::OnCurve => IrPointType::OnCurve, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AnchorData { - pub id: String, - pub name: Option, -} - -impl From<&IrAnchor> for AnchorData { - fn from(anchor: &IrAnchor) -> Self { - Self { - id: anchor.id().to_string(), - name: anchor.name().map(str::to_owned), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ComponentData { - pub id: String, - pub base_glyph_name: GlyphName, -} - -impl From<&IrComponent> for ComponentData { - fn from(component: &IrComponent) -> Self { - Self { - id: component.id().to_string(), - base_glyph_name: component.base_glyph().clone(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -pub struct GlyphChangedEntities { - pub point_ids: Vec, - pub contour_ids: Vec, - pub anchor_ids: Vec, - pub guideline_ids: Vec, - pub component_ids: Vec, -} - -impl GlyphChangedEntities { - pub fn point(id: PointId) -> Self { - Self { - point_ids: vec![id], - ..Default::default() - } - } - - pub fn points(ids: Vec) -> Self { - Self { - point_ids: ids, - ..Default::default() - } - } - - pub fn contour(id: ContourId) -> Self { - Self { - contour_ids: vec![id], - ..Default::default() - } - } - - pub fn contours(ids: Vec) -> Self { - Self { - contour_ids: ids, - ..Default::default() - } - } - - pub fn anchor(id: AnchorId) -> Self { - Self { - anchor_ids: vec![id], - ..Default::default() - } - } - - pub fn guideline(id: GuidelineId) -> Self { - Self { - guideline_ids: vec![id], - ..Default::default() - } - } - - pub fn component(id: ComponentId) -> Self { - Self { - component_ids: vec![id], - ..Default::default() - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GlyphValueChange { - pub values: GlyphValues, - pub changed: GlyphChangedEntities, -} - -impl GlyphValueChange { - pub fn from_layer(layer: &GlyphLayer, changed: GlyphChangedEntities) -> Self { - Self { - values: values_from_layer(layer), - changed, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GlyphStructureChange { - pub structure: GlyphStructure, - pub values: GlyphValues, - pub changed: GlyphChangedEntities, -} - -impl GlyphStructureChange { - pub fn from_layer(layer: &GlyphLayer, changed: GlyphChangedEntities) -> Self { - Self { - structure: GlyphStructure::from(layer), - values: values_from_layer(layer), - changed, - } - } -} - #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Location { @@ -421,76 +180,3 @@ impl From<&IrLocation> for Location { } } } - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AxisTent { - pub axis_tag: String, - pub lower: f64, - pub peak: f64, - pub upper: f64, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GlyphVariationData { - /// One entry per region. Inner = tents on the axes the region depends on. - pub regions: Vec>, - /// Deltas are flattened in `GlyphState::values` order. - pub deltas: Vec>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GlyphMaster { - pub source_id: String, - pub source_name: String, - pub is_default_source: bool, - pub location: Location, - pub structure: GlyphStructure, - pub values: GlyphValues, -} - -/// Flatten mutable numeric glyph state in the order described by `GlyphState::values`. -pub fn values_from_layer(layer: &GlyphLayer) -> GlyphValues { - let mut values = Vec::new(); - values.push(layer.width()); - - for contour in layer.contours_iter() { - for point in contour.points() { - values.push(point.x()); - values.push(point.y()); - } - } - - for anchor in layer.anchors_iter() { - values.push(anchor.x()); - values.push(anchor.y()); - } - - for component in sorted_components(layer) { - push_transform_values(&mut values, component.transform()); - } - - values -} - -/// Components live in a `HashMap` in the IR, but the values array needs stable -/// ordering. Sort by component ID everywhere structure and values are exported. -fn sorted_components(layer: &GlyphLayer) -> Vec<&IrComponent> { - let mut components: Vec<_> = layer.components_iter().collect(); - components.sort_by_key(|component| component.id().raw()); - components -} - -fn push_transform_values(values: &mut Vec, transform: &IrTransform) { - values.push(transform.translate_x); - values.push(transform.translate_y); - values.push(transform.rotation); - values.push(transform.scale_x); - values.push(transform.scale_y); - values.push(transform.skew_x); - values.push(transform.skew_y); - values.push(transform.t_center_x); - values.push(transform.t_center_y); -} diff --git a/crates/shift-wire/src/state.rs b/crates/shift-wire/src/state.rs deleted file mode 100644 index 4bac7b70..00000000 --- a/crates/shift-wire/src/state.rs +++ /dev/null @@ -1,333 +0,0 @@ -//! Strict restore helpers for the shared glyph state wire format. - -use std::str::FromStr; - -use crate::{AnchorData, ComponentData, ContourData, GlyphStructure, GlyphValue}; -use shift_font::{ - Anchor as IrAnchor, AnchorId, Component as IrComponent, ComponentId, Contour as IrContour, - ContourId, CoreError, CoreResult, DecomposedTransform as IrTransform, GlyphLayer, PointId, - PointType as IrPointType, -}; - -pub fn layer_from_state( - structure: &GlyphStructure, - values: &[GlyphValue], -) -> CoreResult { - let mut layer = GlyphLayer::new(); - apply_state_to_layer(&mut layer, structure, values)?; - Ok(layer) -} - -pub fn apply_state_to_layer( - layer: &mut GlyphLayer, - structure: &GlyphStructure, - values: &[GlyphValue], -) -> CoreResult<()> { - let mut cursor = GlyphValueCursor::new(values); - let width = cursor.read_x_advance()?; - - layer.clear_contours(); - layer.clear_anchors(); - layer.clear_components(); - layer.set_width(width); - - restore_contours(layer, &structure.contours, &mut cursor)?; - restore_anchors(layer, &structure.anchors, &mut cursor)?; - restore_components(layer, &structure.components, &mut cursor)?; - - cursor.finish()?; - Ok(()) -} - -#[derive(Debug, Clone, Copy)] -struct PointPosition { - x: GlyphValue, - y: GlyphValue, -} - -struct GlyphValueCursor<'a> { - values: &'a [GlyphValue], - index: usize, -} - -impl<'a> GlyphValueCursor<'a> { - fn new(values: &'a [GlyphValue]) -> Self { - Self { values, index: 0 } - } - - fn read_x_advance(&mut self) -> CoreResult { - self.next() - } - - fn read_point(&mut self) -> CoreResult { - Ok(PointPosition { - x: self.next()?, - y: self.next()?, - }) - } - - fn read_component_transform(&mut self) -> CoreResult { - Ok(IrTransform { - translate_x: self.next()?, - translate_y: self.next()?, - rotation: self.next()?, - scale_x: self.next()?, - scale_y: self.next()?, - skew_x: self.next()?, - skew_y: self.next()?, - t_center_x: self.next()?, - t_center_y: self.next()?, - }) - } - - fn finish(self) -> CoreResult<()> { - if self.index == self.values.len() { - Ok(()) - } else { - Err(CoreError::TrailingGlyphValues { - expected: self.index, - actual: self.values.len(), - }) - } - } - - fn next(&mut self) -> CoreResult { - let idx = self.index; - let value = self - .values - .get(idx) - .copied() - .ok_or(CoreError::MissingGlyphValue { index: idx })?; - - self.index += 1; - Ok(value) - } -} - -fn parse_id(value: &str, invalid: impl FnOnce(String) -> CoreError) -> CoreResult -where - T: FromStr, -{ - value.parse().map_err(|_| invalid(value.to_string())) -} - -fn restore_contours( - layer: &mut GlyphLayer, - contours: &[ContourData], - cursor: &mut GlyphValueCursor<'_>, -) -> CoreResult<()> { - for contour_data in contours { - let contour_id: ContourId = parse_id(&contour_data.id, CoreError::InvalidContourId)?; - let mut new_contour = IrContour::with_id(contour_id); - - for point in &contour_data.points { - let point_id: PointId = parse_id(&point.id, CoreError::InvalidPointId)?; - let new_pos = cursor.read_point()?; - let point_type = IrPointType::from(point.point_type); - new_contour.add_point_with_id(point_id, new_pos.x, new_pos.y, point_type, point.smooth); - } - - if contour_data.closed { - new_contour.close(); - } - - layer.add_contour(new_contour); - } - - Ok(()) -} - -fn restore_anchors( - layer: &mut GlyphLayer, - anchors: &[AnchorData], - cursor: &mut GlyphValueCursor<'_>, -) -> CoreResult<()> { - for anchor_data in anchors { - let anchor_id: AnchorId = parse_id(&anchor_data.id, CoreError::InvalidAnchorId)?; - let position = cursor.read_point()?; - let anchor = IrAnchor::with_id(anchor_id, anchor_data.name.clone(), position.x, position.y); - - layer.add_anchor(anchor); - } - - Ok(()) -} - -fn restore_components( - layer: &mut GlyphLayer, - components: &[ComponentData], - cursor: &mut GlyphValueCursor<'_>, -) -> CoreResult<()> { - for component_data in components { - let component_id: ComponentId = - parse_id(&component_data.id, CoreError::InvalidComponentId)?; - let transform = cursor.read_component_transform()?; - let component = IrComponent::with_id( - component_id, - component_data.base_glyph_name.clone(), - transform, - ); - - layer.add_component(component); - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::values_from_layer; - use shift_font::{Anchor, Component, DecomposedTransform}; - - fn sample_layer() -> GlyphLayer { - let mut layer = GlyphLayer::with_width(500.0); - - let mut contour = IrContour::with_id(ContourId::from_raw(10)); - contour.add_point_with_id(PointId::from_raw(20), 1.0, 2.0, IrPointType::OnCurve, false); - contour.add_point_with_id(PointId::from_raw(21), 3.0, 4.0, IrPointType::OffCurve, true); - contour.close(); - layer.add_contour(contour); - - layer.add_anchor(Anchor::with_id( - AnchorId::from_raw(30), - Some("top".to_string()), - 5.0, - 6.0, - )); - - layer.add_component(Component::with_id( - ComponentId::from_raw(40), - "base".to_string(), - DecomposedTransform { - translate_x: 7.0, - translate_y: 8.0, - rotation: 9.0, - scale_x: 10.0, - scale_y: 11.0, - skew_x: 12.0, - skew_y: 13.0, - t_center_x: 14.0, - t_center_y: 15.0, - }, - )); - - layer - } - - #[test] - fn values_from_layer_uses_canonical_order() { - let layer = sample_layer(); - - assert_eq!( - values_from_layer(&layer), - vec![ - 500.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, - 15.0, - ] - ); - } - - #[test] - fn layer_from_state_restores_ids_structure_and_values() -> CoreResult<()> { - let layer = sample_layer(); - let structure = GlyphStructure::from(&layer); - let restored = layer_from_state(&structure, &values_from_layer(&layer))?; - - assert_eq!(restored.width(), 500.0); - - let contour = restored.contour(ContourId::from_raw(10)).unwrap(); - assert!(contour.is_closed()); - assert_eq!(contour.points().len(), 2); - - let first = contour.get_point(PointId::from_raw(20)).unwrap(); - assert_eq!((first.x(), first.y()), (1.0, 2.0)); - assert_eq!(first.point_type(), IrPointType::OnCurve); - assert!(!first.is_smooth()); - - let second = contour.get_point(PointId::from_raw(21)).unwrap(); - assert_eq!((second.x(), second.y()), (3.0, 4.0)); - assert_eq!(second.point_type(), IrPointType::OffCurve); - assert!(second.is_smooth()); - - let anchor = restored.anchor(AnchorId::from_raw(30)).unwrap(); - assert_eq!(anchor.name(), Some("top")); - assert_eq!(anchor.position(), (5.0, 6.0)); - - let component = restored.component(ComponentId::from_raw(40)).unwrap(); - assert_eq!(component.base_glyph().as_str(), "base"); - assert_eq!(component.transform().translate_x, 7.0); - assert_eq!(component.transform().t_center_y, 15.0); - - Ok(()) - } - - #[test] - fn glyph_structure_sorts_components_by_id() { - let mut layer = GlyphLayer::new(); - layer.add_component(Component::with_id( - ComponentId::from_raw(200), - "later".to_string(), - DecomposedTransform::identity(), - )); - layer.add_component(Component::with_id( - ComponentId::from_raw(100), - "earlier".to_string(), - DecomposedTransform::identity(), - )); - - let structure = GlyphStructure::from(&layer); - - assert_eq!(structure.components[0].id, "100"); - assert_eq!(structure.components[1].id, "200"); - } - - #[test] - fn layer_from_state_rejects_missing_values() { - let structure = GlyphStructure { - contours: vec![], - anchors: vec![], - components: vec![], - }; - - assert!(matches!( - layer_from_state(&structure, &[]), - Err(CoreError::MissingGlyphValue { index: 0 }) - )); - } - - #[test] - fn layer_from_state_rejects_trailing_values() { - let structure = GlyphStructure { - contours: vec![], - anchors: vec![], - components: vec![], - }; - - assert!(matches!( - layer_from_state(&structure, &[500.0, 1.0]), - Err(CoreError::TrailingGlyphValues { - expected: 1, - actual: 2, - }) - )); - } - - #[test] - fn layer_from_state_rejects_invalid_contour_ids() { - let structure = GlyphStructure { - contours: vec![ContourData { - id: "not-a-contour-id".to_string(), - points: vec![], - closed: false, - }], - anchors: vec![], - components: vec![], - }; - - assert!(matches!( - layer_from_state(&structure, &[500.0]), - Err(CoreError::InvalidContourId(value)) if value == "not-a-contour-id" - )); - } -} diff --git a/crates/shift-workspace/src/workspace.rs b/crates/shift-workspace/src/workspace.rs index 0fe9915e..a5d23d98 100644 --- a/crates/shift-workspace/src/workspace.rs +++ b/crates/shift-workspace/src/workspace.rs @@ -2,8 +2,10 @@ use std::path::{Path, PathBuf}; use shift_backends::{FontExportRequest, FontExportResult, FontExporter, font_loader::FontLoader}; use shift_font::{ - FontChange, FontChangeSet, Glyph, GlyphCreated, GlyphLayer, GlyphLayerChangeTarget, LayerId, - SourceId, error::CoreError, + BooleanOp, BulkNodePositionUpdates, ContourId, FontChange, FontChangeSet, Glyph, + GlyphChangedEntities, GlyphCreated, GlyphLayer, GlyphLayerChangeTarget, GlyphStructure, + GlyphStructureChange, GlyphValueChange, LayerId, PointId, PointPosition, PointType, SourceId, + apply_state_to_layer, error::CoreError, }; use shift_source::ShiftSourcePackage; use shift_store::ShiftStore; @@ -188,6 +190,297 @@ impl FontWorkspace { Ok(()) } + pub fn set_x_advance( + &mut self, + target: GlyphLayerTarget, + width: f64, + ) -> Result { + self.edit_glyph_layer( + target, + |layer| { + layer.set_x_advance(width); + Ok(GlyphValueChange::from_layer(layer, Default::default())) + }, + |target, layer, _change| { + FontChangeSet::new(vec![FontChange::LayerMetricsChanged( + shift_font::LayerMetricsChanged::from_layer(target.clone(), layer), + )]) + }, + ) + } + + pub fn translate_layer( + &mut self, + target: GlyphLayerTarget, + dx: f64, + dy: f64, + ) -> Result { + self.edit_glyph_layer( + target, + |layer| { + layer.translate_layer(dx, dy); + Ok(GlyphValueChange::from_layer(layer, Default::default())) + }, + layer_replaced_change, + ) + } + + pub fn add_point( + &mut self, + target: GlyphLayerTarget, + contour_id: ContourId, + x: f64, + y: f64, + point_type: PointType, + smooth: bool, + ) -> Result { + self.edit_glyph_layer( + target, + |layer| { + let point_id = layer.add_point_to_contour(contour_id, x, y, point_type, smooth)?; + let changed = GlyphChangedEntities { + point_ids: vec![point_id], + ..Default::default() + }; + Ok(GlyphStructureChange::from_layer(layer, changed)) + }, + move |target, layer, change| { + let contour = layer + .contour(contour_id) + .map(shift_font::ContourValue::from); + contour + .map(|contour| { + one_change(FontChange::PointsAdded(shift_font::PointsAdded { + target: target.clone(), + contour, + point_ids: change.changed.point_ids.clone(), + })) + }) + .unwrap_or_default() + }, + ) + } + + pub fn insert_point_before( + &mut self, + target: GlyphLayerTarget, + before_point_id: PointId, + x: f64, + y: f64, + point_type: PointType, + smooth: bool, + ) -> Result { + self.edit_glyph_layer( + target, + |layer| { + let point_id = + layer.insert_point_before(before_point_id, x, y, point_type, smooth)?; + let changed = GlyphChangedEntities { + point_ids: vec![point_id], + ..Default::default() + }; + Ok(GlyphStructureChange::from_layer(layer, changed)) + }, + move |target, layer, change| { + let Some(contour_id) = layer.find_point_contour(before_point_id) else { + return layer_replaced_change(target, layer, change); + }; + let Some(contour) = layer + .contour(contour_id) + .map(shift_font::ContourValue::from) + else { + return layer_replaced_change(target, layer, change); + }; + one_change(FontChange::PointsAdded(shift_font::PointsAdded { + target: target.clone(), + contour, + point_ids: change.changed.point_ids.clone(), + })) + }, + ) + } + + pub fn add_contour( + &mut self, + target: GlyphLayerTarget, + ) -> Result { + self.edit_glyph_layer( + target, + |layer| { + let contour_id = layer.add_empty_contour(); + let changed = GlyphChangedEntities { + contour_ids: vec![contour_id], + ..Default::default() + }; + Ok(GlyphStructureChange::from_layer(layer, changed)) + }, + |target, layer, change| { + let Some(contour) = layer + .contours_iter() + .last() + .map(shift_font::ContourValue::from) + else { + return layer_replaced_change(target, layer, change); + }; + one_change(FontChange::ContourAdded(shift_font::ContourAdded { + target: target.clone(), + contour, + })) + }, + ) + } + + pub fn open_contour( + &mut self, + target: GlyphLayerTarget, + contour_id: ContourId, + ) -> Result { + self.set_contour_closed(target, contour_id, false) + } + + pub fn close_contour( + &mut self, + target: GlyphLayerTarget, + contour_id: ContourId, + ) -> Result { + self.set_contour_closed(target, contour_id, true) + } + + pub fn reverse_contour( + &mut self, + target: GlyphLayerTarget, + contour_id: ContourId, + ) -> Result { + self.edit_glyph_layer( + target, + |layer| { + layer.reverse_contour(contour_id)?; + let changed = GlyphChangedEntities { + contour_ids: vec![contour_id], + ..Default::default() + }; + Ok(GlyphStructureChange::from_layer(layer, changed)) + }, + layer_replaced_change, + ) + } + + pub fn apply_boolean_op( + &mut self, + target: GlyphLayerTarget, + contour_id_a: ContourId, + contour_id_b: ContourId, + operation: BooleanOp, + ) -> Result { + self.edit_glyph_layer( + target, + |layer| { + let created_ids = layer.apply_boolean_op(contour_id_a, contour_id_b, operation)?; + let changed = GlyphChangedEntities { + contour_ids: created_ids, + ..Default::default() + }; + Ok(GlyphStructureChange::from_layer(layer, changed)) + }, + layer_replaced_change, + ) + } + + pub fn remove_points( + &mut self, + target: GlyphLayerTarget, + point_ids: Vec, + ) -> Result { + self.edit_glyph_layer( + target, + |layer| { + layer.remove_points(&point_ids)?; + let changed = GlyphChangedEntities::points(point_ids); + Ok(GlyphStructureChange::from_layer(layer, changed)) + }, + layer_replaced_change, + ) + } + + pub fn toggle_smooth( + &mut self, + target: GlyphLayerTarget, + point_id: PointId, + ) -> Result { + self.edit_glyph_layer( + target, + |layer| { + layer.toggle_smooth(point_id)?; + let changed = GlyphChangedEntities { + point_ids: vec![point_id], + ..Default::default() + }; + Ok(GlyphStructureChange::from_layer(layer, changed)) + }, + move |target, layer, change| { + let smooth = layer + .contours_iter() + .find_map(|contour| contour.get_point(point_id)) + .map(|point| point.is_smooth()); + smooth + .map(|smooth| { + one_change(FontChange::PointSmoothChanged( + shift_font::PointSmoothChanged { + target: target.clone(), + point_id, + smooth, + }, + )) + }) + .unwrap_or_else(|| layer_replaced_change(target, layer, change)) + }, + ) + } + + pub fn apply_position_patch( + &mut self, + target: GlyphLayerTarget, + updates: BulkNodePositionUpdates<'_>, + point_position_changes: Vec, + has_anchor_updates: bool, + ) -> Result<(), WorkspaceError> { + self.edit_glyph_layer( + target, + |layer| { + layer.apply_bulk_node_positions(updates)?; + Ok(()) + }, + move |target, layer, result| { + if has_anchor_updates { + return layer_replaced_change(target, layer, result); + } + + one_change(FontChange::PointPositionsChanged( + shift_font::PointPositionsChanged { + target: target.clone(), + points: point_position_changes, + }, + )) + }, + ) + } + + pub fn restore_state( + &mut self, + target: GlyphLayerTarget, + structure: &GlyphStructure, + values: &[f64], + ) -> Result { + self.edit_glyph_layer( + target, + |layer| { + apply_state_to_layer(layer, structure, values)?; + Ok(GlyphStructureChange::from_layer(layer, Default::default())) + }, + layer_replaced_change, + ) + } + pub fn edit_glyph_layer( &mut self, target: GlyphLayerTarget, @@ -247,6 +540,38 @@ impl FontWorkspace { Ok(result) } + fn set_contour_closed( + &mut self, + target: GlyphLayerTarget, + contour_id: ContourId, + closed: bool, + ) -> Result { + self.edit_glyph_layer( + target, + |layer| { + if closed { + layer.close_contour(contour_id)?; + } else { + layer.open_contour(contour_id)?; + } + let changed = GlyphChangedEntities { + contour_ids: vec![contour_id], + ..Default::default() + }; + Ok(GlyphStructureChange::from_layer(layer, changed)) + }, + move |target, _layer, _change| { + one_change(FontChange::ContourOpenClosedChanged( + shift_font::ContourOpenClosedChanged { + target: target.clone(), + contour_id, + closed, + }, + )) + }, + ) + } + fn commit_font( &mut self, next_font: shift_font::Font, @@ -325,6 +650,23 @@ impl FontWorkspace { } } +fn one_change(change: FontChange) -> FontChangeSet { + FontChangeSet::new(vec![change]) +} + +fn layer_replaced_change( + target: &GlyphLayerChangeTarget, + layer: &GlyphLayer, + _result: &impl Sized, +) -> FontChangeSet { + one_change(FontChange::LayerGeometryReplaced( + shift_font::LayerGeometryReplaced { + target: target.clone(), + layer: shift_font::GlyphLayerValue::from(layer), + }, + )) +} + fn font_info_from_font(font: &shift_font::Font) -> shift_store::FontInfo { let metadata = font.metadata(); shift_store::FontInfo { diff --git a/crates/shift-workspace/tests/workspace_test.rs b/crates/shift-workspace/tests/workspace_test.rs index 6316306f..5f0d3f19 100644 --- a/crates/shift-workspace/tests/workspace_test.rs +++ b/crates/shift-workspace/tests/workspace_test.rs @@ -1,6 +1,10 @@ use std::path::PathBuf; -use shift_workspace::{FontWorkspace, NewWorkspace, WorkspaceError, WorkspaceSource}; +use shift_font::PointType; +use shift_store::GlyphId as StoreGlyphId; +use shift_workspace::{ + FontWorkspace, GlyphLayerTarget, NewWorkspace, WorkspaceError, WorkspaceSource, +}; #[test] fn creates_workspace_with_source_package_and_working_store() { @@ -109,6 +113,87 @@ fn save_as_assigns_a_shift_package_save_target() { workspace.save().unwrap(); } +#[test] +fn workspace_add_contour_creates_glyph_and_persists_layer() { + let temp = tempfile::tempdir().unwrap(); + let source_path = temp.path().join("TestFont.shift"); + let store_path = temp.path().join("working.sqlite"); + let mut workspace = + FontWorkspace::create(&source_path, &store_path, NewWorkspace::new()).unwrap(); + + let target = GlyphLayerTarget { + glyph_name: "A".to_string(), + unicode: Some(65), + layer_id: workspace.font().default_layer_id(), + }; + + let change = workspace.add_contour(target).unwrap(); + + assert_eq!(change.structure.contours.len(), 1); + let glyph = workspace.font().glyph("A").unwrap(); + assert_eq!(glyph.unicodes(), &[65]); + assert!( + workspace + .store() + .get_glyph(&StoreGlyphId::new(glyph.id().to_string())) + .unwrap() + .is_some() + ); +} + +#[test] +fn workspace_add_point_updates_live_font_and_store() { + let temp = tempfile::tempdir().unwrap(); + let source_path = temp.path().join("TestFont.shift"); + let store_path = temp.path().join("working.sqlite"); + let mut workspace = + FontWorkspace::create(&source_path, &store_path, NewWorkspace::new()).unwrap(); + + let target = GlyphLayerTarget { + glyph_name: "A".to_string(), + unicode: Some(65), + layer_id: workspace.font().default_layer_id(), + }; + let contour = workspace + .add_contour(target.clone()) + .unwrap() + .structure + .contours[0] + .id + .parse() + .unwrap(); + + let change = workspace + .add_point( + target.clone(), + contour, + 10.0, + 20.0, + PointType::OnCurve, + false, + ) + .unwrap(); + + assert_eq!(change.changed.point_ids.len(), 1); + let glyph = workspace.font().glyph("A").unwrap(); + let layer = glyph.layer(target.layer_id).unwrap(); + let point = layer + .contour(contour) + .unwrap() + .get_point(change.changed.point_ids[0]) + .unwrap(); + assert_eq!((point.x(), point.y()), (10.0, 20.0)); + assert_eq!(point.point_type(), PointType::OnCurve); + assert_eq!( + workspace + .store() + .list_points_for_contour(&contour.to_string()) + .unwrap() + .len(), + 1 + ); +} + fn fixture(path: &str) -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../..")