From 6819e153bd7d87add4d0b04aa2841fd0cd0e4f81 Mon Sep 17 00:00:00 2001 From: Kostya Farber Date: Thu, 4 Jun 2026 09:25:19 +0300 Subject: [PATCH] Add workspace font change sets --- Cargo.lock | 1 + crates/shift-bridge/src/bridge.rs | 423 ++++++++++++++++++------ crates/shift-font/src/changes.rs | 203 ++++++++++++ crates/shift-font/src/lib.rs | 2 + crates/shift-store/Cargo.toml | 1 + crates/shift-store/src/change_set.rs | 374 +++++++++++++++++++++ crates/shift-store/src/error.rs | 3 + crates/shift-store/src/glyph.rs | 17 + crates/shift-store/src/layer.rs | 14 +- crates/shift-store/src/lib.rs | 3 + crates/shift-store/src/outline.rs | 79 +++++ crates/shift-store/src/schema.rs | 47 ++- crates/shift-store/tests/store_test.rs | 170 ++++++++++ crates/shift-workspace/src/workspace.rs | 78 ++++- 14 files changed, 1298 insertions(+), 117 deletions(-) create mode 100644 crates/shift-font/src/changes.rs create mode 100644 crates/shift-store/src/change_set.rs create mode 100644 crates/shift-store/src/outline.rs diff --git a/Cargo.lock b/Cargo.lock index 65774edf..458a68df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1754,6 +1754,7 @@ name = "shift-store" version = "0.1.0" dependencies = [ "rusqlite", + "shift-font", "tempfile", "thiserror 2.0.18", ] diff --git a/crates/shift-bridge/src/bridge.rs b/crates/shift-bridge/src/bridge.rs index 02e28cba..12efa7a9 100644 --- a/crates/shift-bridge/src/bridge.rs +++ b/crates/shift-bridge/src/bridge.rs @@ -519,9 +519,35 @@ impl Bridge { &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)?) + 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( @@ -684,10 +710,18 @@ 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())) - })?; + 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), + )) + }, + )?; self.mark_font_changed(); Ok(change.into()) @@ -700,10 +734,14 @@ 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())) - })?; + 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, + )?; self.mark_font_changed(); Ok(change.into()) @@ -722,14 +760,33 @@ impl Bridge { 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)) - })?; + 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() + }, + )?; self.mark_font_changed(); Ok(change.into()) @@ -748,14 +805,35 @@ impl Bridge { 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)) - })?; + 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(), + }, + )) + }, + )?; self.mark_font_changed(); Ok(change.into()) @@ -763,14 +841,32 @@ 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)) - })?; + 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, + }, + )) + }, + )?; self.mark_font_changed(); Ok(change.into()) @@ -783,14 +879,26 @@ impl Bridge { #[napi(ts_arg_type = "ContourId")] contour_id: String, ) -> errors::Result { 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)) - })?; + 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, + }, + )) + }, + )?; self.mark_font_changed(); Ok(change.into()) @@ -803,14 +911,26 @@ impl Bridge { #[napi(ts_arg_type = "ContourId")] contour_id: String, ) -> errors::Result { 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)) - })?; + 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, + }, + )) + }, + )?; self.mark_font_changed(); Ok(change.into()) @@ -823,14 +943,18 @@ impl Bridge { #[napi(ts_arg_type = "ContourId")] contour_id: String, ) -> errors::Result { 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)) - })?; + 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, + )?; self.mark_font_changed(); Ok(change.into()) @@ -860,14 +984,18 @@ 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)) - })?; + 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, + )?; self.mark_font_changed(); Ok(change.into()) @@ -882,11 +1010,15 @@ impl Bridge { 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)) - })?; + 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, + )?; self.mark_font_changed(); Ok(change.into()) @@ -899,14 +1031,34 @@ impl Bridge { #[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)) - })?; + 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)) + }, + )?; self.mark_font_changed(); Ok(change.into()) @@ -923,27 +1075,48 @@ impl Bridge { anchor_ids: Option, anchor_coords: Option, ) -> errors::Result<()> { - 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(()) - })?; + 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, + }, + )) + }, + )?; self.mark_font_changed(); Ok(()) @@ -959,16 +1132,60 @@ impl Bridge { let structure = 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())) - })?; + 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, + )?; self.mark_font_changed(); Ok(change.into()) } } +fn read_point_position_changes( + point_ids: &Option, + point_coords: &Option, +) -> BridgeResult> { + let Some(point_ids) = point_ids.as_ref() else { + return Ok(Vec::new()); + }; + + let point_ids: &[u64] = point_ids; + let point_coords = point_coords + .as_ref() + .ok_or_else(|| BridgeError::InvalidInput { + kind: "point positions", + value: "missing coordinates".to_string(), + })?; + let point_coords: &[f64] = point_coords; + let expected_coords = point_ids.len() * 2; + if point_coords.len() != expected_coords { + return Err(BridgeError::InvalidInput { + kind: "point positions", + value: format!( + "expected {expected_coords} coordinates, got {}", + point_coords.len() + ), + }); + } + + Ok( + point_ids + .iter() + .enumerate() + .map(|(index, point_id)| shift_font::PointPosition { + point_id: PointId::from_raw(*point_id as u128), + x: point_coords[index * 2], + y: point_coords[index * 2 + 1], + }) + .collect(), + ) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/shift-font/src/changes.rs b/crates/shift-font/src/changes.rs new file mode 100644 index 00000000..413bab7e --- /dev/null +++ b/crates/shift-font/src/changes.rs @@ -0,0 +1,203 @@ +use crate::{ + Contour, ContourId, Glyph, GlyphId, GlyphLayer, GlyphName, LayerId, Point, PointId, PointType, + SourceId, +}; + +#[derive(Clone, Debug, Default)] +pub struct FontChangeSet { + pub changes: Vec, +} + +impl FontChangeSet { + pub fn new(changes: Vec) -> Self { + Self { changes } + } + + pub fn push(&mut self, change: FontChange) { + self.changes.push(change); + } + + pub fn is_empty(&self) -> bool { + self.changes.is_empty() + } +} + +#[derive(Clone, Debug)] +pub enum FontChange { + GlyphCreated(GlyphCreated), + GlyphIdentityChanged(GlyphIdentityChanged), + LayerMetricsChanged(LayerMetricsChanged), + ContourAdded(ContourAdded), + ContourOpenClosedChanged(ContourOpenClosedChanged), + PointsAdded(PointsAdded), + PointsDeleted(PointsDeleted), + PointSmoothChanged(PointSmoothChanged), + PointPositionsChanged(PointPositionsChanged), + LayerGeometryReplaced(LayerGeometryReplaced), +} + +#[derive(Clone, Debug)] +pub struct GlyphCreated { + pub glyph_id: GlyphId, + pub name: GlyphName, + pub unicodes: Vec, +} + +impl From<&Glyph> for GlyphCreated { + fn from(glyph: &Glyph) -> Self { + Self { + glyph_id: glyph.id(), + name: glyph.glyph_name().clone(), + unicodes: glyph.unicodes().to_vec(), + } + } +} + +#[derive(Clone, Debug)] +pub struct GlyphIdentityChanged { + pub glyph_id: GlyphId, + pub from_name: GlyphName, + pub to_name: GlyphName, + pub from_unicodes: Vec, + pub to_unicodes: Vec, +} + +#[derive(Clone, Debug)] +pub struct GlyphLayerChangeTarget { + pub glyph_id: GlyphId, + pub glyph_name: GlyphName, + pub source_id: SourceId, + pub layer_id: LayerId, +} + +#[derive(Clone, Debug)] +pub struct LayerMetricsChanged { + pub target: GlyphLayerChangeTarget, + pub width: f64, + pub height: Option, +} + +impl LayerMetricsChanged { + pub fn from_layer(target: GlyphLayerChangeTarget, layer: &GlyphLayer) -> Self { + Self { + target, + width: layer.width(), + height: layer.height(), + } + } +} + +#[derive(Clone, Debug)] +pub struct ContourAdded { + pub target: GlyphLayerChangeTarget, + pub contour: ContourValue, +} + +#[derive(Clone, Debug)] +pub struct ContourOpenClosedChanged { + pub target: GlyphLayerChangeTarget, + pub contour_id: ContourId, + pub closed: bool, +} + +#[derive(Clone, Debug)] +pub struct PointsAdded { + pub target: GlyphLayerChangeTarget, + pub contour: ContourValue, + pub point_ids: Vec, +} + +#[derive(Clone, Debug)] +pub struct PointsDeleted { + pub target: GlyphLayerChangeTarget, + pub contour: ContourValue, + pub point_ids: Vec, +} + +#[derive(Clone, Debug)] +pub struct PointSmoothChanged { + pub target: GlyphLayerChangeTarget, + pub point_id: PointId, + pub smooth: bool, +} + +#[derive(Clone, Debug)] +pub struct PointPositionsChanged { + pub target: GlyphLayerChangeTarget, + pub points: Vec, +} + +#[derive(Clone, Debug)] +pub struct PointPosition { + pub point_id: PointId, + pub x: f64, + pub y: f64, +} + +#[derive(Clone, Debug)] +pub struct LayerGeometryReplaced { + pub target: GlyphLayerChangeTarget, + pub layer: GlyphLayerValue, +} + +#[derive(Clone, Debug)] +pub struct GlyphLayerValue { + pub width: f64, + pub height: Option, + pub contours: Vec, +} + +impl From<&GlyphLayer> for GlyphLayerValue { + fn from(layer: &GlyphLayer) -> Self { + Self { + width: layer.width(), + height: layer.height(), + contours: layer.contours_iter().map(ContourValue::from).collect(), + } + } +} + +#[derive(Clone, Debug)] +pub struct ContourValue { + pub id: ContourId, + pub closed: bool, + pub points: Vec, +} + +impl From<&Contour> for ContourValue { + fn from(contour: &Contour) -> Self { + Self { + id: contour.id(), + closed: contour.is_closed(), + points: contour + .points() + .iter() + .enumerate() + .map(|(order_index, point)| PointValue::from_point(order_index, point)) + .collect(), + } + } +} + +#[derive(Clone, Debug)] +pub struct PointValue { + pub id: PointId, + pub order_index: usize, + pub x: f64, + pub y: f64, + pub point_type: PointType, + pub smooth: bool, +} + +impl PointValue { + pub fn from_point(order_index: usize, point: &Point) -> Self { + Self { + id: point.id(), + order_index, + x: point.x(), + y: point.y(), + point_type: point.point_type(), + smooth: point.is_smooth(), + } + } +} diff --git a/crates/shift-font/src/lib.rs b/crates/shift-font/src/lib.rs index b8031a27..04fa4228 100644 --- a/crates/shift-font/src/lib.rs +++ b/crates/shift-font/src/lib.rs @@ -1,9 +1,11 @@ +pub mod changes; pub mod composite; pub mod curve; pub mod error; pub mod ir; pub mod layer_edit; +pub use changes::*; pub use error::{CoreError, CoreResult}; pub use ir::*; pub use ir::{ diff --git a/crates/shift-store/Cargo.toml b/crates/shift-store/Cargo.toml index 3182e61a..2c9098d4 100644 --- a/crates/shift-store/Cargo.toml +++ b/crates/shift-store/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +shift-font = { workspace = true } rusqlite = { version = "0.37.0", features = ["blob", "backup", "limits"] } thiserror = "2" diff --git a/crates/shift-store/src/change_set.rs b/crates/shift-store/src/change_set.rs new file mode 100644 index 00000000..3b501c6a --- /dev/null +++ b/crates/shift-store/src/change_set.rs @@ -0,0 +1,374 @@ +use rusqlite::{Transaction, params}; +use shift_font as font; + +use crate::{ShiftStore, StoreError}; + +impl ShiftStore { + pub fn apply_change_set(&mut self, change_set: &font::FontChangeSet) -> Result<(), StoreError> { + let tx = self.conn.transaction()?; + + for change in &change_set.changes { + apply_change(&tx, change)?; + } + + tx.commit()?; + Ok(()) + } + + pub fn replace_font_state(&mut self, font: &font::Font) -> Result<(), StoreError> { + let tx = self.conn.transaction()?; + + tx.execute("DELETE FROM glyph_layer_points", [])?; + tx.execute("DELETE FROM glyph_layer_contours", [])?; + tx.execute("DELETE FROM glyph_components", [])?; + tx.execute("DELETE FROM glyph_layers", [])?; + tx.execute("DELETE FROM glyph_unicodes", [])?; + tx.execute("DELETE FROM glyphs", [])?; + tx.execute("DELETE FROM source_locations", [])?; + tx.execute("DELETE FROM sources", [])?; + tx.execute("DELETE FROM axes", [])?; + + for source in font.sources() { + upsert_source(&tx, source.id(), Some(source.name()))?; + } + + for glyph in font.glyphs().values().map(|glyph| glyph.as_ref()) { + upsert_glyph(&tx, glyph.id(), glyph.glyph_name())?; + replace_glyph_unicodes(&tx, glyph.id(), glyph.unicodes())?; + + for (layer_id, layer) in glyph.layers() { + let target = font::GlyphLayerChangeTarget { + glyph_id: glyph.id(), + glyph_name: glyph.glyph_name().clone(), + source_id: source_id_for_layer(font, *layer_id), + layer_id: *layer_id, + }; + upsert_layer(&tx, &target, layer.width(), layer.height())?; + replace_layer_geometry(&tx, &target, &font::GlyphLayerValue::from(layer.as_ref()))?; + } + } + + tx.commit()?; + Ok(()) + } +} + +fn apply_change(tx: &Transaction<'_>, change: &font::FontChange) -> Result<(), StoreError> { + match change { + font::FontChange::GlyphCreated(change) => { + upsert_glyph(tx, change.glyph_id, &change.name)?; + replace_glyph_unicodes(tx, change.glyph_id, &change.unicodes) + } + font::FontChange::GlyphIdentityChanged(change) => { + upsert_glyph(tx, change.glyph_id, &change.to_name)?; + replace_glyph_unicodes(tx, change.glyph_id, &change.to_unicodes) + } + font::FontChange::LayerMetricsChanged(change) => { + ensure_target(tx, &change.target)?; + let rows_changed = tx.execute( + " + UPDATE glyph_layers + SET width = ?2, height = ?3 + WHERE id = ?1 + ", + params![layer_row_id(&change.target), change.width, change.height,], + )?; + require_changed(rows_changed, "glyph layer", layer_row_id(&change.target))?; + Ok(()) + } + font::FontChange::ContourAdded(change) => { + ensure_target(tx, &change.target)?; + replace_contour(tx, &change.target, &change.contour) + } + font::FontChange::ContourOpenClosedChanged(change) => { + ensure_target(tx, &change.target)?; + let rows_changed = tx.execute( + " + UPDATE glyph_layer_contours + SET closed = ?2 + WHERE id = ?1 + ", + params![change.contour_id.to_string(), change.closed], + )?; + require_changed(rows_changed, "contour", change.contour_id.to_string())?; + Ok(()) + } + font::FontChange::PointsAdded(change) => { + ensure_target(tx, &change.target)?; + replace_contour(tx, &change.target, &change.contour) + } + font::FontChange::PointsDeleted(change) => { + ensure_target(tx, &change.target)?; + replace_contour(tx, &change.target, &change.contour) + } + font::FontChange::PointSmoothChanged(change) => { + ensure_target(tx, &change.target)?; + let rows_changed = tx.execute( + " + UPDATE glyph_layer_points + SET smooth = ?2 + WHERE id = ?1 + ", + params![change.point_id.to_string(), change.smooth], + )?; + require_changed(rows_changed, "point", change.point_id.to_string())?; + Ok(()) + } + font::FontChange::PointPositionsChanged(change) => { + ensure_target(tx, &change.target)?; + for point in &change.points { + let rows_changed = tx.execute( + " + UPDATE glyph_layer_points + SET x = ?2, y = ?3 + WHERE id = ?1 + ", + params![point.point_id.to_string(), point.x, point.y], + )?; + require_changed(rows_changed, "point", point.point_id.to_string())?; + } + Ok(()) + } + font::FontChange::LayerGeometryReplaced(change) => { + ensure_target(tx, &change.target)?; + replace_layer_geometry(tx, &change.target, &change.layer) + } + } +} + +fn ensure_target( + tx: &Transaction<'_>, + target: &font::GlyphLayerChangeTarget, +) -> Result<(), StoreError> { + upsert_source(tx, target.source_id, None)?; + upsert_glyph(tx, target.glyph_id, &target.glyph_name)?; + upsert_layer(tx, target, 0.0, None) +} + +fn upsert_source( + tx: &Transaction<'_>, + source_id: font::SourceId, + name: Option<&str>, +) -> Result<(), StoreError> { + tx.execute( + " + INSERT INTO sources (id, name, kind) + VALUES (?1, ?2, 'master') + ON CONFLICT(id) DO UPDATE SET + name = COALESCE(excluded.name, sources.name) + ", + params![source_id.to_string(), name], + )?; + Ok(()) +} + +fn upsert_glyph( + tx: &Transaction<'_>, + glyph_id: font::GlyphId, + name: &font::GlyphName, +) -> Result<(), StoreError> { + tx.execute( + " + INSERT INTO glyphs (id, name) + VALUES (?1, ?2) + ON CONFLICT(id) DO UPDATE SET + name = excluded.name + ", + params![glyph_id.to_string(), name.as_str()], + )?; + Ok(()) +} + +fn replace_glyph_unicodes( + tx: &Transaction<'_>, + glyph_id: font::GlyphId, + unicodes: &[u32], +) -> Result<(), StoreError> { + tx.execute( + "DELETE FROM glyph_unicodes WHERE glyph_id = ?1", + [glyph_id.to_string()], + )?; + + for (order_index, unicode) in unicodes.iter().enumerate() { + tx.execute( + " + INSERT INTO glyph_unicodes (glyph_id, unicode, order_index) + VALUES (?1, ?2, ?3) + ", + params![glyph_id.to_string(), *unicode as i64, order_index as i64], + )?; + } + + Ok(()) +} + +fn upsert_layer( + tx: &Transaction<'_>, + target: &font::GlyphLayerChangeTarget, + width: f64, + height: Option, +) -> Result<(), StoreError> { + tx.execute( + " + INSERT INTO glyph_layers (id, glyph_id, source_id, name, width, height) + VALUES (?1, ?2, ?3, ?4, ?5, ?6) + ON CONFLICT(id) DO UPDATE SET + glyph_id = excluded.glyph_id, + source_id = excluded.source_id, + name = excluded.name + ", + params![ + layer_row_id(target), + target.glyph_id.to_string(), + target.source_id.to_string(), + target.glyph_name.as_str(), + width, + height, + ], + )?; + Ok(()) +} + +fn replace_layer_geometry( + tx: &Transaction<'_>, + target: &font::GlyphLayerChangeTarget, + layer: &font::GlyphLayerValue, +) -> Result<(), StoreError> { + tx.execute( + " + UPDATE glyph_layers + SET width = ?2, height = ?3 + WHERE id = ?1 + ", + params![layer_row_id(target), layer.width, layer.height], + )?; + tx.execute( + " + DELETE FROM glyph_layer_contours + WHERE layer_id = ?1 + ", + [layer_row_id(target)], + )?; + + for (order_index, contour) in layer.contours.iter().enumerate() { + insert_contour(tx, target, order_index, contour)?; + } + + Ok(()) +} + +fn replace_contour( + tx: &Transaction<'_>, + target: &font::GlyphLayerChangeTarget, + contour: &font::ContourValue, +) -> Result<(), StoreError> { + tx.execute( + " + INSERT INTO glyph_layer_contours (id, layer_id, closed, order_index) + VALUES (?1, ?2, ?3, COALESCE( + (SELECT order_index FROM glyph_layer_contours WHERE id = ?1), + (SELECT COUNT(*) FROM glyph_layer_contours WHERE layer_id = ?2) + )) + ON CONFLICT(id) DO UPDATE SET + closed = excluded.closed, + layer_id = excluded.layer_id + ", + params![contour.id.to_string(), layer_row_id(target), contour.closed,], + )?; + tx.execute( + "DELETE FROM glyph_layer_points WHERE contour_id = ?1", + [contour.id.to_string()], + )?; + + for point in &contour.points { + insert_point(tx, contour.id, point)?; + } + + Ok(()) +} + +fn insert_contour( + tx: &Transaction<'_>, + target: &font::GlyphLayerChangeTarget, + order_index: usize, + contour: &font::ContourValue, +) -> Result<(), StoreError> { + tx.execute( + " + INSERT INTO glyph_layer_contours (id, layer_id, closed, order_index) + VALUES (?1, ?2, ?3, ?4) + ", + params![ + contour.id.to_string(), + layer_row_id(target), + contour.closed, + order_index as i64, + ], + )?; + + for point in &contour.points { + insert_point(tx, contour.id, point)?; + } + + Ok(()) +} + +fn insert_point( + tx: &Transaction<'_>, + contour_id: font::ContourId, + point: &font::PointValue, +) -> Result<(), StoreError> { + tx.execute( + " + INSERT INTO glyph_layer_points ( + id, + contour_id, + order_index, + x, + y, + point_type, + smooth + ) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) + ", + params![ + point.id.to_string(), + contour_id.to_string(), + point.order_index as i64, + point.x, + point.y, + point_type_name(point.point_type), + point.smooth, + ], + )?; + Ok(()) +} + +fn point_type_name(point_type: font::PointType) -> &'static str { + match point_type { + font::PointType::OnCurve => "onCurve", + font::PointType::OffCurve => "offCurve", + font::PointType::QCurve => "qCurve", + } +} + +fn require_changed(rows_changed: usize, kind: &'static str, id: String) -> Result<(), StoreError> { + if rows_changed == 0 { + Err(StoreError::MissingEntity { kind, id }) + } else { + Ok(()) + } +} + +fn layer_row_id(target: &font::GlyphLayerChangeTarget) -> String { + format!("{}:{}", target.glyph_id, target.layer_id) +} + +fn source_id_for_layer(font: &font::Font, layer_id: font::LayerId) -> font::SourceId { + font.sources() + .iter() + .find(|source| source.layer_id() == layer_id) + .map(font::Source::id) + .or_else(|| font.default_source().map(font::Source::id)) + .unwrap_or_default() +} diff --git a/crates/shift-store/src/error.rs b/crates/shift-store/src/error.rs index 2de79f1d..f19d0a84 100644 --- a/crates/shift-store/src/error.rs +++ b/crates/shift-store/src/error.rs @@ -5,4 +5,7 @@ pub enum StoreError { #[error("unknown source kind: {0}")] UnknownSourceKind(String), + + #[error("missing {kind}: {id}")] + MissingEntity { kind: &'static str, id: String }, } diff --git a/crates/shift-store/src/glyph.rs b/crates/shift-store/src/glyph.rs index 8862564a..696886b6 100644 --- a/crates/shift-store/src/glyph.rs +++ b/crates/shift-store/src/glyph.rs @@ -39,6 +39,23 @@ impl ShiftStore { Err(err) => Err(err.into()), } } + + pub fn list_glyph_unicodes(&self, id: &GlyphId) -> Result, StoreError> { + let mut stmt = self.conn.prepare( + " + SELECT unicode + FROM glyph_unicodes + WHERE glyph_id = ?1 + ORDER BY order_index + ", + )?; + + let rows = stmt.query_map([id.as_str()], |row| { + row.get::<_, i64>(0).map(|unicode| unicode as u32) + })?; + rows.collect::, _>>() + .map_err(StoreError::from) + } } fn map_glyph_row(row: &rusqlite::Row<'_>) -> rusqlite::Result { diff --git a/crates/shift-store/src/layer.rs b/crates/shift-store/src/layer.rs index 626fcd9f..6c3b983a 100644 --- a/crates/shift-store/src/layer.rs +++ b/crates/shift-store/src/layer.rs @@ -7,12 +7,14 @@ pub struct NewGlyphLayer { pub name: Option, } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct GlyphLayerRecord { pub id: LayerId, pub glyph_id: GlyphId, pub source_id: SourceId, pub name: Option, + pub width: f64, + pub height: Option, } impl ShiftStore { @@ -45,7 +47,9 @@ impl ShiftStore { id, glyph_id, source_id, - name + name, + width, + height FROM glyph_layers WHERE id = ?1 ", @@ -68,7 +72,9 @@ impl ShiftStore { id, glyph_id, source_id, - name + name, + width, + height FROM glyph_layers WHERE glyph_id = ?1 ORDER BY source_id, id @@ -87,5 +93,7 @@ fn map_glyph_layer_row(row: &rusqlite::Row<'_>) -> rusqlite::Result(1)?), source_id: SourceId::new(row.get::<_, String>(2)?), name: row.get(3)?, + width: row.get(4)?, + height: row.get(5)?, }) } diff --git a/crates/shift-store/src/lib.rs b/crates/shift-store/src/lib.rs index 1cf5ffb8..5cc8c0b0 100644 --- a/crates/shift-store/src/lib.rs +++ b/crates/shift-store/src/lib.rs @@ -1,9 +1,11 @@ +mod change_set; mod component; mod connection; mod error; mod font; mod glyph; mod layer; +mod outline; mod schema; mod source; mod store; @@ -14,6 +16,7 @@ pub use error::StoreError; pub use font::FontInfo; pub use glyph::{GlyphRecord, NewGlyph}; pub use layer::{GlyphLayerRecord, NewGlyphLayer}; +pub use outline::{ContourRecord, PointRecord}; pub use source::{AxisRecord, NewAxis, NewSource, SourceAxisLocation, SourceKind, SourceRecord}; pub use store::ShiftStore; pub use types::{AxisId, ComponentId, GlyphId, LayerId, RevisionId, SourceId}; diff --git a/crates/shift-store/src/outline.rs b/crates/shift-store/src/outline.rs new file mode 100644 index 00000000..c379b491 --- /dev/null +++ b/crates/shift-store/src/outline.rs @@ -0,0 +1,79 @@ +use crate::{LayerId, ShiftStore, StoreError}; + +#[derive(Clone, Debug, PartialEq)] +pub struct ContourRecord { + pub id: String, + pub layer_id: LayerId, + pub closed: bool, + pub order_index: i64, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct PointRecord { + pub id: String, + pub contour_id: String, + pub order_index: i64, + pub x: f64, + pub y: f64, + pub point_type: String, + pub smooth: bool, +} + +impl ShiftStore { + pub fn list_contours_for_layer( + &self, + layer_id: &LayerId, + ) -> Result, StoreError> { + let mut stmt = self.conn.prepare( + " + SELECT id, layer_id, closed, order_index + FROM glyph_layer_contours + WHERE layer_id = ?1 + ORDER BY order_index + ", + )?; + + let rows = stmt.query_map([layer_id.as_str()], map_contour_row)?; + rows.collect::, _>>() + .map_err(StoreError::from) + } + + pub fn list_points_for_contour( + &self, + contour_id: &str, + ) -> Result, StoreError> { + let mut stmt = self.conn.prepare( + " + SELECT id, contour_id, order_index, x, y, point_type, smooth + FROM glyph_layer_points + WHERE contour_id = ?1 + ORDER BY order_index + ", + )?; + + let rows = stmt.query_map([contour_id], map_point_row)?; + rows.collect::, _>>() + .map_err(StoreError::from) + } +} + +fn map_contour_row(row: &rusqlite::Row<'_>) -> rusqlite::Result { + Ok(ContourRecord { + id: row.get(0)?, + layer_id: LayerId::new(row.get::<_, String>(1)?), + closed: row.get(2)?, + order_index: row.get(3)?, + }) +} + +fn map_point_row(row: &rusqlite::Row<'_>) -> rusqlite::Result { + Ok(PointRecord { + id: row.get(0)?, + contour_id: row.get(1)?, + order_index: row.get(2)?, + x: row.get(3)?, + y: row.get(4)?, + point_type: row.get(5)?, + smooth: row.get(6)?, + }) +} diff --git a/crates/shift-store/src/schema.rs b/crates/shift-store/src/schema.rs index f8cf4917..a725e5dd 100644 --- a/crates/shift-store/src/schema.rs +++ b/crates/shift-store/src/schema.rs @@ -49,24 +49,65 @@ CREATE TABLE IF NOT EXISTS glyphs ( CREATE INDEX IF NOT EXISTS glyphs_name_idx ON glyphs(name); +CREATE TABLE IF NOT EXISTS glyph_unicodes ( + glyph_id TEXT NOT NULL, + unicode INTEGER NOT NULL CHECK (unicode >= 0), + order_index INTEGER NOT NULL, + PRIMARY KEY (glyph_id, unicode), + FOREIGN KEY (glyph_id) REFERENCES glyphs(id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS glyph_unicodes_glyph_id_idx +ON glyph_unicodes(glyph_id); + CREATE TABLE IF NOT EXISTS glyph_layers ( id TEXT PRIMARY KEY, glyph_id TEXT NOT NULL, source_id TEXT NOT NULL, name TEXT, + width REAL NOT NULL DEFAULT 0, + height REAL, FOREIGN KEY (glyph_id) REFERENCES glyphs(id) ON DELETE CASCADE, FOREIGN KEY (source_id) REFERENCES sources(id) ON DELETE CASCADE ); -CREATE UNIQUE INDEX IF NOT EXISTS glyph_layers_glyph_source_unique -ON glyph_layers(glyph_id, source_id); - CREATE INDEX IF NOT EXISTS glyph_layers_glyph_id_idx ON glyph_layers(glyph_id); CREATE INDEX IF NOT EXISTS glyph_layers_source_id_idx ON glyph_layers(source_id); +CREATE TABLE IF NOT EXISTS glyph_layer_contours ( + id TEXT PRIMARY KEY, + layer_id TEXT NOT NULL, + closed INTEGER NOT NULL DEFAULT 0 CHECK (closed IN (0, 1)), + order_index INTEGER NOT NULL, + FOREIGN KEY (layer_id) REFERENCES glyph_layers(id) ON DELETE CASCADE +); + +CREATE UNIQUE INDEX IF NOT EXISTS glyph_layer_contours_layer_order_unique +ON glyph_layer_contours(layer_id, order_index); + +CREATE INDEX IF NOT EXISTS glyph_layer_contours_layer_id_idx +ON glyph_layer_contours(layer_id); + +CREATE TABLE IF NOT EXISTS glyph_layer_points ( + id TEXT PRIMARY KEY, + contour_id TEXT NOT NULL, + order_index INTEGER NOT NULL, + x REAL NOT NULL, + y REAL NOT NULL, + point_type TEXT NOT NULL, + smooth INTEGER NOT NULL DEFAULT 0 CHECK (smooth IN (0, 1)), + FOREIGN KEY (contour_id) REFERENCES glyph_layer_contours(id) ON DELETE CASCADE +); + +CREATE UNIQUE INDEX IF NOT EXISTS glyph_layer_points_contour_order_unique +ON glyph_layer_points(contour_id, order_index); + +CREATE INDEX IF NOT EXISTS glyph_layer_points_contour_id_idx +ON glyph_layer_points(contour_id); + CREATE TABLE IF NOT EXISTS glyph_components ( id TEXT PRIMARY KEY, layer_id TEXT NOT NULL, diff --git a/crates/shift-store/tests/store_test.rs b/crates/shift-store/tests/store_test.rs index 5544316f..4e432c71 100644 --- a/crates/shift-store/tests/store_test.rs +++ b/crates/shift-store/tests/store_test.rs @@ -257,6 +257,143 @@ fn lists_glyph_components_for_layer() { assert_eq!(components[0].order_index, 0); } +#[test] +fn applies_glyph_identity_change_set() { + let mut store = ShiftStore::open_memory_for_test().expect("memory store should open"); + let glyph = shift_font::Glyph::with_unicode("A", 65); + let glyph_id = glyph.id(); + + store + .apply_change_set(&shift_font::FontChangeSet::new(vec![ + shift_font::FontChange::GlyphCreated(shift_font::GlyphCreated::from(&glyph)), + shift_font::FontChange::GlyphIdentityChanged(shift_font::GlyphIdentityChanged { + glyph_id, + from_name: shift_font::GlyphName::from("A"), + to_name: shift_font::GlyphName::from("A.alt"), + from_unicodes: vec![65], + to_unicodes: vec![0x00c1], + }), + ])) + .expect("change set should apply"); + + let stored = store + .get_glyph(&GlyphId::new(glyph_id.to_string())) + .expect("glyph query should succeed") + .expect("glyph should exist"); + let unicodes = store + .list_glyph_unicodes(&stored.id) + .expect("unicode query should succeed"); + + assert_eq!(stored.name.as_deref(), Some("A.alt")); + assert_eq!(unicodes, vec![0x00c1]); +} + +#[test] +fn applies_layer_metrics_and_contour_point_changes() { + let mut store = ShiftStore::open_memory_for_test().expect("memory store should open"); + let (target, contour, point_id) = store_change_target_with_contour(); + let store_layer_id = store_layer_id(&target); + + store + .apply_change_set(&shift_font::FontChangeSet::new(vec![ + shift_font::FontChange::LayerMetricsChanged(shift_font::LayerMetricsChanged { + target: target.clone(), + width: 720.0, + height: None, + }), + shift_font::FontChange::ContourAdded(shift_font::ContourAdded { + target: target.clone(), + contour, + }), + shift_font::FontChange::PointPositionsChanged(shift_font::PointPositionsChanged { + target, + points: vec![shift_font::PointPosition { + point_id, + x: 40.0, + y: 50.0, + }], + }), + ])) + .expect("change set should apply"); + + let layer = store + .get_glyph_layer(&store_layer_id) + .expect("layer query should succeed") + .expect("layer should exist"); + let contours = store + .list_contours_for_layer(&store_layer_id) + .expect("contour query should succeed"); + let points = store + .list_points_for_contour(&contours[0].id) + .expect("point query should succeed"); + + assert_eq!(layer.width, 720.0); + assert_eq!(contours.len(), 1); + assert!(!contours[0].closed); + assert_eq!(points.len(), 1); + assert_eq!(points[0].id, point_id.to_string()); + assert_eq!((points[0].x, points[0].y), (40.0, 50.0)); + assert_eq!(points[0].point_type, "onCurve"); +} + +#[test] +fn applies_layer_geometry_replacement() { + let mut store = ShiftStore::open_memory_for_test().expect("memory store should open"); + let (target, first_contour, _) = store_change_target_with_contour(); + let store_layer_id = store_layer_id(&target); + let mut layer = shift_font::GlyphLayer::with_width(500.0); + layer.add_contour(contour_with_point(10.0, 20.0)); + + store + .apply_change_set(&shift_font::FontChangeSet::new(vec![ + shift_font::FontChange::ContourAdded(shift_font::ContourAdded { + target: target.clone(), + contour: first_contour, + }), + shift_font::FontChange::LayerGeometryReplaced(shift_font::LayerGeometryReplaced { + target: target.clone(), + layer: shift_font::GlyphLayerValue::from(&layer), + }), + ])) + .expect("change set should apply"); + + let contours = store + .list_contours_for_layer(&store_layer_id) + .expect("contour query should succeed"); + let points = store + .list_points_for_contour(&contours[0].id) + .expect("point query should succeed"); + + assert_eq!(contours.len(), 1); + assert_eq!(points.len(), 1); + assert_eq!((points[0].x, points[0].y), (10.0, 20.0)); +} + +#[test] +fn rejects_incremental_change_for_missing_point_row() { + let mut store = ShiftStore::open_memory_for_test().expect("memory store should open"); + let (target, _, _) = store_change_target_with_contour(); + let missing_point_id = shift_font::PointId::new(); + + let result = store.apply_change_set(&shift_font::FontChangeSet::new(vec![ + shift_font::FontChange::PointPositionsChanged(shift_font::PointPositionsChanged { + target, + points: vec![shift_font::PointPosition { + point_id: missing_point_id, + x: 1.0, + y: 2.0, + }], + }), + ])); + + assert!( + result + .expect_err("missing point should reject") + .to_string() + .contains(&missing_point_id.to_string()) + ); +} + fn create_glyph_a(store: &mut ShiftStore) -> GlyphId { let glyph_id = GlyphId::new("glyph-A"); @@ -403,3 +540,36 @@ fn create_regular_source(store: &mut ShiftStore) -> SourceId { source_id } + +fn store_change_target_with_contour() -> ( + shift_font::GlyphLayerChangeTarget, + shift_font::ContourValue, + shift_font::PointId, +) { + let glyph = shift_font::Glyph::with_unicode("A", 65); + let layer_id = shift_font::LayerId::new(); + let source_id = shift_font::SourceId::new(); + let contour = contour_with_point(10.0, 20.0); + let point_id = contour.points()[0].id(); + + ( + shift_font::GlyphLayerChangeTarget { + glyph_id: glyph.id(), + glyph_name: glyph.glyph_name().clone(), + source_id, + layer_id, + }, + shift_font::ContourValue::from(&contour), + point_id, + ) +} + +fn contour_with_point(x: f64, y: f64) -> shift_font::Contour { + let mut contour = shift_font::Contour::new(); + contour.add_point(x, y, shift_font::PointType::OnCurve, false); + contour +} + +fn store_layer_id(target: &shift_font::GlyphLayerChangeTarget) -> LayerId { + LayerId::new(format!("{}:{}", target.glyph_id, target.layer_id)) +} diff --git a/crates/shift-workspace/src/workspace.rs b/crates/shift-workspace/src/workspace.rs index a80b8282..0fe9915e 100644 --- a/crates/shift-workspace/src/workspace.rs +++ b/crates/shift-workspace/src/workspace.rs @@ -1,7 +1,10 @@ use std::path::{Path, PathBuf}; use shift_backends::{FontExportRequest, FontExportResult, FontExporter, font_loader::FontLoader}; -use shift_font::{Glyph, GlyphLayer, LayerId, error::CoreError}; +use shift_font::{ + FontChange, FontChangeSet, Glyph, GlyphCreated, GlyphLayer, GlyphLayerChangeTarget, LayerId, + SourceId, error::CoreError, +}; use shift_source::ShiftSourcePackage; use shift_store::ShiftStore; @@ -66,6 +69,7 @@ impl FontWorkspace { let mut font = shift_font::Font::new(); font.metadata_mut().family_name = Some(new_workspace.family_name); font.metrics_mut().units_per_em = new_workspace.units_per_em as f64; + store.replace_font_state(&font)?; Ok(Self { font, @@ -153,6 +157,9 @@ impl FontWorkspace { }); }; + let glyph_id = glyph.id(); + let from_name = glyph.glyph_name().clone(); + let from_unicodes = glyph.unicodes().to_vec(); let glyph_name = shift_font::GlyphName::new(name.to_string()).map_err(|_| { WorkspaceError::InvalidInput { kind: "glyph name", @@ -162,9 +169,22 @@ impl FontWorkspace { glyph.set_name(glyph_name); glyph.set_unicodes(unicodes); + let to_name = glyph.glyph_name().clone(); + let to_unicodes = glyph.unicodes().to_vec(); next_font.put_glyph(glyph); - self.commit_font(next_font); + self.commit_font( + next_font, + FontChangeSet::new(vec![FontChange::GlyphIdentityChanged( + shift_font::GlyphIdentityChanged { + glyph_id, + from_name, + to_name, + from_unicodes, + to_unicodes, + }, + )]), + )?; Ok(()) } @@ -172,8 +192,10 @@ impl FontWorkspace { &mut self, target: GlyphLayerTarget, edit: impl FnOnce(&mut GlyphLayer) -> Result, + changes: impl FnOnce(&GlyphLayerChangeTarget, &GlyphLayer, &R) -> FontChangeSet, ) -> Result { let mut next_font = self.font.clone(); + let mut change_set = FontChangeSet::default(); if next_font.glyph(&target.glyph_name).is_none() { let mut glyph = Glyph::new(target.glyph_name.clone()); @@ -181,9 +203,29 @@ impl FontWorkspace { glyph.add_unicode(unicode); } glyph.set_layer(target.layer_id, GlyphLayer::with_width(500.0)); + change_set.push(FontChange::GlyphCreated(GlyphCreated::from(&glyph))); next_font.insert_glyph(glyph); } + let source_id = source_id_for_layer(&next_font, target.layer_id).ok_or_else(|| { + WorkspaceError::InvalidInput { + kind: "layer ID", + value: target.layer_id.to_string(), + } + })?; + let glyph = + next_font + .glyph(&target.glyph_name) + .ok_or_else(|| WorkspaceError::InvalidInput { + kind: "glyph name", + value: target.glyph_name.clone(), + })?; + let change_target = GlyphLayerChangeTarget { + glyph_id: glyph.id(), + glyph_name: glyph.glyph_name().clone(), + source_id, + layer_id: target.layer_id, + }; let glyph = next_font.glyph_mut(&target.glyph_name).ok_or_else(|| { WorkspaceError::InvalidInput { kind: "glyph name", @@ -195,15 +237,24 @@ impl FontWorkspace { glyph.add_unicode(unicode); } - let result = edit(glyph.get_or_create_layer(target.layer_id))?; - self.commit_font(next_font); + let layer = glyph.get_or_create_layer(target.layer_id); + let result = edit(layer)?; + let layer_changes = changes(&change_target, layer, &result); + change_set.changes.extend(layer_changes.changes); + + self.commit_font(next_font, change_set)?; Ok(result) } - fn commit_font(&mut self, next_font: shift_font::Font) { - // TODO: apply a shift-font domain change set to shift-store before swapping. + fn commit_font( + &mut self, + next_font: shift_font::Font, + change_set: FontChangeSet, + ) -> Result<(), WorkspaceError> { + self.store.apply_change_set(&change_set)?; self.font = next_font; + Ok(()) } fn open_package( @@ -211,10 +262,12 @@ impl FontWorkspace { store_path: impl AsRef, ) -> Result { let source_package = ShiftSourcePackage::open(source_path)?; - let store = ShiftStore::open(store_path)?; + let mut store = ShiftStore::open(store_path)?; + let font = shift_font::Font::new(); + store.replace_font_state(&font)?; Ok(Self { - font: shift_font::Font::new(), + font, source: WorkspaceSource::Package { path: source_package.path().to_path_buf(), }, @@ -233,6 +286,7 @@ impl FontWorkspace { let font = FontLoader::new().read_font(import_path_str)?; let mut store = ShiftStore::open(store_path)?; store.set_font_info(font_info_from_font(&font))?; + store.replace_font_state(&font)?; Ok(Self { font, @@ -291,3 +345,11 @@ fn font_info_from_font(font: &shift_font::Font) -> shift_store::FontInfo { units_per_em: font.metrics().units_per_em as i64, } } + +fn source_id_for_layer(font: &shift_font::Font, layer_id: LayerId) -> Option { + font.sources() + .iter() + .find(|source| source.layer_id() == layer_id) + .map(shift_font::Source::id) + .or_else(|| font.default_source().map(shift_font::Source::id)) +}