From a9cca27f4e380c305c1c89fba269fa0055776757 Mon Sep 17 00:00:00 2001 From: Kostya Farber Date: Sat, 6 Jun 2026 07:57:09 +0300 Subject: [PATCH] feat(align): align data models more closely. Decouple source id from layer id, add docs --- README.md | 33 ++-- .../src/renderer/src/lib/model/Font.ts | 2 +- .../src/renderer/src/lib/model/Glyph.ts | 2 +- crates/shift-backends/src/binary/reader.rs | 11 +- crates/shift-backends/src/designspace/mod.rs | 7 +- .../shift-backends/src/designspace/reader.rs | 143 ++++++++++++------ .../shift-backends/src/designspace/writer.rs | 12 +- crates/shift-backends/src/export.rs | 8 +- crates/shift-backends/src/glyphs/reader.rs | 24 ++- crates/shift-backends/src/traits.rs | 7 +- crates/shift-backends/src/ufo/mod.rs | 25 +-- crates/shift-backends/src/ufo/reader.rs | 31 ++-- crates/shift-backends/src/ufo/writer.rs | 15 +- crates/shift-bridge/index.d.ts | 3 +- crates/shift-bridge/src/bridge.rs | 57 ++++--- crates/shift-font/README.md | 38 +++++ crates/shift-font/docs/DOCS.md | 22 ++- crates/shift-font/src/composite.rs | 103 +++++++------ crates/shift-font/src/ir/font.rs | 24 +-- crates/shift-font/src/ir/glyph.rs | 97 ++++++++++-- crates/shift-font/src/ir/source.rs | 21 +-- crates/shift-font/src/layer_edit.rs | 4 +- crates/shift-font/src/lib.rs | 31 ++++ crates/shift-store/src/change_set.rs | 17 +-- crates/shift-store/tests/store_test.rs | 3 +- crates/shift-wire/src/bridges/napi/mod.rs | 3 - crates/shift-wire/src/interpolation.rs | 2 +- crates/shift-wire/src/lib.rs | 4 +- crates/shift-wire/src/state.rs | 31 ++-- crates/shift-workspace/src/workspace.rs | 52 ++++--- packages/types/src/bridge/generated.ts | 4 +- scripts/generate-bridge-types.mjs | 1 - 32 files changed, 540 insertions(+), 297 deletions(-) create mode 100644 crates/shift-font/README.md diff --git a/README.md b/README.md index 4af6833c..fcf0ca35 100644 --- a/README.md +++ b/README.md @@ -19,18 +19,31 @@ Shift aims to redefine font editing by combining the power of Rust for performan ## Architecture ``` -┌─────────────────────────────────────────────────────────┐ -│ Frontend │ -│ React UI ←→ Editor ←→ Canvas 2D Renderer │ -└────────────────────────┬────────────────────────────────┘ - │ IPC / NAPI -┌────────────────────────┴────────────────────────────────┐ -│ Backend │ -│ shift-node (N-API bindings) ←→ shift-core (Rust) │ -└─────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────┐ +│ Desktop App │ +│ Electron shell ←→ React UI ←→ TypeScript Editor │ +└───────────────────────────────┬──────────────────────────────┘ + │ IPC / native bridge +┌───────────────────────────────┴──────────────────────────────┐ +│ Rust Crates │ +│ shift-bridge transport adapter │ +│ shift-workspace open working state │ +│ shift-font live font authoring model │ +│ shift-store SQLite working store │ +│ shift-source .shift source package IO │ +└──────────────────────────────────────────────────────────────┘ ``` -The frontend handles UI and rendering via Electron, while all font data and editing operations live in Rust. Communication happens through native Node.js bindings, keeping performance-critical work off the main thread. +The desktop app owns shell and editor interaction. Rust owns the live font authoring model, durable working state, source package IO, and native transport boundary. + +`shift-font` is the core Rust object model: + +- `Font` owns glyphs, sources, axes, metadata, and font-level data. +- `Source` is an editable designspace position with a name and location. +- `Glyph` is a glyph concept identified by `GlyphId`. +- `GlyphLayer` is authored editable data for one glyph at one source. + +Stable IDs are identity. Names and Unicode values are editable metadata. ## Getting Started diff --git a/apps/desktop/src/renderer/src/lib/model/Font.ts b/apps/desktop/src/renderer/src/lib/model/Font.ts index 9d4b9bde..83ce87da 100644 --- a/apps/desktop/src/renderer/src/lib/model/Font.ts +++ b/apps/desktop/src/renderer/src/lib/model/Font.ts @@ -460,7 +460,7 @@ export class Font { this.#bridge.setXAdvance( { glyphHandle: handle, - layerId: this.defaultSource.layerId, + sourceId: this.defaultSource.id, }, 500, ); diff --git a/apps/desktop/src/renderer/src/lib/model/Glyph.ts b/apps/desktop/src/renderer/src/lib/model/Glyph.ts index babc6467..639bd253 100644 --- a/apps/desktop/src/renderer/src/lib/model/Glyph.ts +++ b/apps/desktop/src/renderer/src/lib/model/Glyph.ts @@ -231,7 +231,7 @@ class GlyphEditSession { #glyphRef(): GlyphLayerRef { return { glyphHandle: this.#handle, - layerId: this.#source.layerId, + sourceId: this.#source.id, }; } diff --git a/crates/shift-backends/src/binary/reader.rs b/crates/shift-backends/src/binary/reader.rs index c3d88f48..badb7c0f 100644 --- a/crates/shift-backends/src/binary/reader.rs +++ b/crates/shift-backends/src/binary/reader.rs @@ -1,5 +1,5 @@ use crate::errors::{FormatBackendError, FormatBackendResult}; -use shift_font::{Contour, Font, Glyph, GlyphLayer, PointType}; +use shift_font::{Contour, Font, Glyph, GlyphLayer, LayerId, PointType}; use skrifa::{ outline::{DrawSettings, OutlinePen}, prelude::{LocationRef, Size}, @@ -125,7 +125,9 @@ fn font_from_skrifa(font: &FontRef<'_>) -> Font { let metrics = font.metrics(Size::unscaled(), LocationRef::default()); let mut ir_font = Font::new(); - let default_layer_id = ir_font.default_layer_id(); + let default_source_id = ir_font + .default_source_id() + .expect("new font should have a default source"); ir_font.metrics_mut().units_per_em = metrics.units_per_em as f64; ir_font.metrics_mut().ascender = metrics.ascent as f64; @@ -154,13 +156,14 @@ fn font_from_skrifa(font: &FontRef<'_>) -> Font { .unwrap_or_else(|| format!("uni{unicode:04X}")); let mut glyph = Glyph::with_unicode(glyph_name, unicode); - let mut layer = GlyphLayer::with_width(advance_width as f64); + let mut layer = + GlyphLayer::with_width(LayerId::new(), default_source_id, advance_width as f64); let mut contours = pen.contours(); detect_smooth_points(&mut contours); for contour in contours { layer.add_contour(contour); } - glyph.set_layer(default_layer_id, layer); + glyph.set_layer(layer); ir_font.insert_glyph(glyph); } diff --git a/crates/shift-backends/src/designspace/mod.rs b/crates/shift-backends/src/designspace/mod.rs index 01ce9fd4..02b6bfbb 100644 --- a/crates/shift-backends/src/designspace/mod.rs +++ b/crates/shift-backends/src/designspace/mod.rs @@ -10,7 +10,7 @@ pub use writer::DesignspaceWriter; mod tests { use super::*; use crate::traits::{FontReader, FontWriter}; - use shift_font::{Contour, Font, Glyph, GlyphLayer, PointType}; + use shift_font::{Contour, Font, Glyph, GlyphLayer, LayerId, PointType}; use std::fs; fn test_font() -> Font { @@ -20,7 +20,8 @@ mod tests { font.metrics_mut().units_per_em = 1000.0; let mut glyph = Glyph::with_unicode("o".to_string(), 'o' as u32); - let mut layer = GlyphLayer::with_width(520.0); + let mut layer = + GlyphLayer::with_width(LayerId::new(), font.default_source_id().unwrap(), 520.0); let mut contour = Contour::new(); contour.add_point(100.0, 0.0, PointType::OnCurve, false); contour.add_point(420.0, 0.0, PointType::OnCurve, false); @@ -28,7 +29,7 @@ mod tests { contour.add_point(100.0, 500.0, PointType::OnCurve, false); contour.close(); layer.add_contour(contour); - glyph.set_layer(font.default_layer_id(), layer); + glyph.set_layer(layer); font.insert_glyph(glyph); font diff --git a/crates/shift-backends/src/designspace/reader.rs b/crates/shift-backends/src/designspace/reader.rs index ed345831..3b9a2059 100644 --- a/crates/shift-backends/src/designspace/reader.rs +++ b/crates/shift-backends/src/designspace/reader.rs @@ -5,7 +5,7 @@ use crate::ufo::UfoReader; use norad::designspace::DesignSpaceDocument; use quick_xml::events::{BytesStart, Event}; use quick_xml::Reader; -use shift_font::{Axis, Font, Layer, LayerId, Location, Source}; +use shift_font::{Axis, Font, Layer, LayerId, Location, Source, SourceId}; use std::collections::HashMap; use std::fs; use std::path::Path; @@ -77,6 +77,9 @@ impl DesignspaceReader { path: default_ufo_path.clone(), details: source.to_string(), })?; + let default_ufo_source_id = font + .default_source_id() + .ok_or(DesignspaceError::NoSources)?; font.clear_sources(); if let Some(ref family) = default_ds_source.familyname { @@ -98,16 +101,15 @@ impl DesignspaceReader { } // Register the default source. - let default_layer_id = font.default_layer_id(); let default_location = location_from_dimensions(&default_ds_source.location, &doc); let default_name = source_name(default_ds_source, default_idx); let default_source_id = font.add_source(Source::with_filename( default_name, default_location, - default_layer_id, default_ds_source.filename.clone(), )); font.set_default_source_id(default_source_id); + move_glyph_layers_to_source(&mut font, default_ufo_source_id, default_source_id); // Cache loaded UFO fonts so we don't re-read the same file for support layers. let mut ufo_cache: HashMap = HashMap::new(); @@ -142,40 +144,38 @@ impl DesignspaceReader { }; // Determine which layer from the source UFO to read. - let source_layer_id = match &ds_source.layer { - Some(layer_name) => { - find_layer_by_name(source_font, layer_name).ok_or_else(|| { - DesignspaceError::MissingLayer { - layer: layer_name.clone(), - filename: ds_source.filename.clone(), - } - })? - } - None => source_font.default_layer_id(), + let source_source_id = match &ds_source.layer { + Some(layer_name) => find_source_by_external_layer_name(source_font, layer_name) + .ok_or_else(|| DesignspaceError::MissingLayer { + layer: layer_name.clone(), + filename: ds_source.filename.clone(), + })?, + None => source_font + .default_source_id() + .ok_or(DesignspaceError::NoSources)?, }; let name = source_name(ds_source, idx); - let layer = Layer::new(name.clone()); - let layer_id = font.add_layer(layer); + font.add_layer(Layer::new(name.clone())); + let location = location_from_dimensions(&ds_source.location, &doc); + let source_id = font.add_source(Source::with_filename( + name, + location, + ds_source.filename.clone(), + )); // Copy glyphs from the resolved layer into the new layer. for (glyph_name, source_glyph) in source_font.glyphs() { - if let Some(source_layer) = source_glyph.layer(source_layer_id) { + if let Some(source_layer) = source_glyph.layer_for_source(source_source_id) { if let Some(existing_glyph) = font.glyph_mut(glyph_name) { - existing_glyph.set_layer(layer_id, source_layer.clone()); + existing_glyph + .set_layer(source_layer.clone_with_identity(LayerId::new(), source_id)); } } } - - let location = location_from_dimensions(&ds_source.location, &doc); - font.add_source(Source::with_filename( - name, - location, - layer_id, - ds_source.filename.clone(), - )); } + remove_glyph_layers_without_source(&mut font); Ok(font) } } @@ -227,6 +227,9 @@ fn load_axisless_designspace( path: default_ufo_path.clone(), details: source.to_string(), })?; + let default_ufo_source_id = font + .default_source_id() + .ok_or(DesignspaceError::NoSources)?; font.clear_sources(); if let Some(family) = &default_source.familyname { @@ -236,10 +239,10 @@ fn load_axisless_designspace( let default_source_id = font.add_source(Source::with_filename( axisless_source_name(default_source, 0), Location::new(), - font.default_layer_id(), default_source.filename.clone(), )); font.set_default_source_id(default_source_id); + move_glyph_layers_to_source(&mut font, default_ufo_source_id, default_source_id); let mut ufo_cache: HashMap = HashMap::new(); for (idx, ds_source) in sources.iter().enumerate().skip(1) { @@ -266,36 +269,36 @@ fn load_axisless_designspace( } }; - let source_layer_id = match &ds_source.layer { - Some(layer_name) => find_layer_by_name(source_font, layer_name).ok_or_else(|| { - DesignspaceError::MissingLayer { + let source_source_id = match &ds_source.layer { + Some(layer_name) => find_source_by_external_layer_name(source_font, layer_name) + .ok_or_else(|| DesignspaceError::MissingLayer { layer: layer_name.clone(), filename: ds_source.filename.clone(), - } - })?, - None => source_font.default_layer_id(), + })?, + None => source_font + .default_source_id() + .ok_or(DesignspaceError::NoSources)?, }; let name = axisless_source_name(ds_source, idx); - let layer = Layer::new(name.clone()); - let layer_id = font.add_layer(layer); + font.add_layer(Layer::new(name.clone())); + let source_id = font.add_source(Source::with_filename( + name, + Location::new(), + ds_source.filename.clone(), + )); for (glyph_name, source_glyph) in source_font.glyphs() { - if let Some(source_layer) = source_glyph.layer(source_layer_id) { + if let Some(source_layer) = source_glyph.layer_for_source(source_source_id) { if let Some(existing_glyph) = font.glyph_mut(glyph_name) { - existing_glyph.set_layer(layer_id, source_layer.clone()); + existing_glyph + .set_layer(source_layer.clone_with_identity(LayerId::new(), source_id)); } } } - - font.add_source(Source::with_filename( - name, - Location::new(), - layer_id, - ds_source.filename.clone(), - )); } + remove_glyph_layers_without_source(&mut font); Ok(font) } @@ -415,11 +418,57 @@ fn find_default_source_index(doc: &DesignSpaceDocument) -> usize { 0 } -fn find_layer_by_name(font: &Font, name: &str) -> Option { - font.layers() +fn find_source_by_external_layer_name(font: &Font, name: &str) -> Option { + font.sources() .iter() - .find(|(_, layer)| layer.name() == name) - .map(|(&id, _)| id) + .find(|source| source.name() == name) + .map(Source::id) +} + +fn move_glyph_layers_to_source(font: &mut Font, from_source_id: SourceId, to_source_id: SourceId) { + let glyph_names: Vec<_> = font.glyphs().keys().cloned().collect(); + + for glyph_name in glyph_names { + let Some(glyph) = font.glyph_mut(&glyph_name) else { + continue; + }; + let Some(layer) = glyph + .layer_for_source(from_source_id) + .map(|layer| layer.clone_with_identity(LayerId::new(), to_source_id)) + else { + continue; + }; + let old_layer_ids: Vec<_> = glyph + .layers() + .values() + .filter(|layer| layer.source_id() == from_source_id) + .map(|layer| layer.id()) + .collect(); + for old_layer_id in old_layer_ids { + glyph.remove_layer(old_layer_id); + } + glyph.set_layer(layer); + } +} + +fn remove_glyph_layers_without_source(font: &mut Font) { + let source_ids: Vec<_> = font.sources().iter().map(Source::id).collect(); + let glyph_names: Vec<_> = font.glyphs().keys().cloned().collect(); + + for glyph_name in glyph_names { + let Some(glyph) = font.glyph_mut(&glyph_name) else { + continue; + }; + let orphan_layer_ids: Vec<_> = glyph + .layers() + .values() + .filter(|layer| !source_ids.contains(&layer.source_id())) + .map(|layer| layer.id()) + .collect(); + for layer_id in orphan_layer_ids { + glyph.remove_layer(layer_id); + } + } } /// Derive (minimum, maximum) for an axis from norad's parsed designspace. diff --git a/crates/shift-backends/src/designspace/writer.rs b/crates/shift-backends/src/designspace/writer.rs index db762cce..01fa33e2 100644 --- a/crates/shift-backends/src/designspace/writer.rs +++ b/crates/shift-backends/src/designspace/writer.rs @@ -50,12 +50,10 @@ impl DesignspaceWriter { } fn source(source: &Source, font: &Font, filename: &str, axes: &[Axis]) -> DsSource { - let layer = if source.layer_id() == font.default_layer_id() { + let layer = if Some(source.id()) == font.default_source_id() { None } else { - font.layers() - .get(&source.layer_id()) - .map(|layer| layer.name().to_string()) + Some(source.name().to_string()) }; DsSource { @@ -69,12 +67,10 @@ impl DesignspaceWriter { } fn source_layer(source: &Source, font: &Font) -> Option { - if source.layer_id() == font.default_layer_id() { + if Some(source.id()) == font.default_source_id() { None } else { - font.layers() - .get(&source.layer_id()) - .map(|layer| layer.name().to_string()) + Some(source.name().to_string()) } } diff --git a/crates/shift-backends/src/export.rs b/crates/shift-backends/src/export.rs index 5e63afe5..b623ce8c 100644 --- a/crates/shift-backends/src/export.rs +++ b/crates/shift-backends/src/export.rs @@ -144,21 +144,21 @@ fn path_to_str<'a>(path: &'a Path, label: &'static str) -> Result<&'a str, Expor #[cfg(test)] mod tests { use super::*; - use shift_font::{Contour, Font, Glyph, GlyphLayer, PointType}; + use shift_font::{Contour, Font, Glyph, GlyphLayer, LayerId, PointType}; use skrifa::{FontRef, MetadataProvider}; fn simple_font() -> Font { let mut font = Font::new(); - let default_layer_id = font.default_layer_id(); + let default_source_id = font.default_source_id().unwrap(); let mut glyph = Glyph::with_unicode("A".to_string(), 0x0041); - let mut layer = GlyphLayer::with_width(600.0); + let mut layer = GlyphLayer::with_width(LayerId::new(), default_source_id, 600.0); let mut contour = Contour::new(); contour.add_point(100.0, 0.0, PointType::OnCurve, false); contour.add_point(300.0, 700.0, PointType::OnCurve, false); contour.add_point(500.0, 0.0, PointType::OnCurve, false); contour.close(); layer.add_contour(contour); - glyph.set_layer(default_layer_id, layer); + glyph.set_layer(layer); font.insert_glyph(glyph); font } diff --git a/crates/shift-backends/src/glyphs/reader.rs b/crates/shift-backends/src/glyphs/reader.rs index 36a973e1..5f7d91ca 100644 --- a/crates/shift-backends/src/glyphs/reader.rs +++ b/crates/shift-backends/src/glyphs/reader.rs @@ -1,7 +1,7 @@ use glyphs_reader::{FeatureSnippet, Font as GlyphsFont, NodeType, Shape}; use shift_font::{ Anchor, Axis, Component, Contour, FeatureData, Font, Glyph, GlyphLayer, KerningData, - KerningPair, KerningSide, Layer, Location, Source, Transform, + KerningPair, KerningSide, Layer, LayerId, Location, Source, Transform, }; use std::collections::HashMap; use std::path::Path; @@ -130,7 +130,6 @@ impl FontReader for GlyphsReader { .map_err(|e| FormatBackendError::Glyphs(e.to_string()))?; let mut font = Font::empty(); - let default_layer_id = font.default_layer_id(); // Metadata and metrics from default master. if let Some(family_name) = glyphs_font.names.get("familyNames") { @@ -182,14 +181,11 @@ impl FontReader for GlyphsReader { font.add_axis(axis); } - let mut layer_by_master_id = HashMap::new(); + let mut source_by_master_id = HashMap::new(); for (master_idx, master) in glyphs_font.masters.iter().enumerate() { - let layer_id = if master_idx == glyphs_font.default_master_idx { - default_layer_id - } else { - font.add_layer(Layer::new(master.name.clone())) - }; - layer_by_master_id.insert(master.id.clone(), layer_id); + if master_idx != glyphs_font.default_master_idx { + font.add_layer(Layer::new(master.name.clone())); + } let mut location = Location::new(); for (axis_idx, axis) in glyphs_font.axes.iter().enumerate() { @@ -197,7 +193,8 @@ impl FontReader for GlyphsReader { location.set(axis.tag.clone(), value.into_inner()); } } - let source_id = font.add_source(Source::new(master.name.clone(), location, layer_id)); + let source_id = font.add_source(Source::new(master.name.clone(), location)); + source_by_master_id.insert(master.id.clone(), source_id); if master_idx == glyphs_font.default_master_idx { font.set_default_source_id(source_id); } @@ -210,11 +207,12 @@ impl FontReader for GlyphsReader { } for layer in &glyph.layers { - let Some(layer_id) = layer_by_master_id.get(layer.master_id()).copied() else { + let Some(source_id) = source_by_master_id.get(layer.master_id()).copied() else { continue; }; - let mut ir_layer = GlyphLayer::with_width(layer.width.into_inner()); + let mut ir_layer = + GlyphLayer::with_width(LayerId::new(), source_id, layer.width.into_inner()); for shape in &layer.shapes { match shape { @@ -256,7 +254,7 @@ impl FontReader for GlyphsReader { ir_layer.add_anchor(Anchor::new(name, anchor.pos.x, anchor.pos.y)); } - ir_glyph.set_layer(layer_id, ir_layer); + ir_glyph.set_layer(ir_layer); } font.insert_glyph(ir_glyph); diff --git a/crates/shift-backends/src/traits.rs b/crates/shift-backends/src/traits.rs index e934a0f6..22a419e7 100644 --- a/crates/shift-backends/src/traits.rs +++ b/crates/shift-backends/src/traits.rs @@ -1,7 +1,7 @@ use crate::errors::FormatBackendResult; use shift_font::{ Axis, FeatureData, Font, FontMetadata, FontMetrics, Glyph, GlyphName, Guideline, KerningData, - Layer, LayerId, LibData, Source, + Layer, LayerId, LibData, Source, SourceId, }; pub trait FontView { @@ -9,6 +9,7 @@ pub trait FontView { fn metrics(&self) -> &FontMetrics; fn axes(&self) -> &[Axis]; fn sources(&self) -> &[Source]; + fn default_source_id(&self) -> Option; fn layers(&self) -> Vec<(LayerId, &Layer)>; fn glyphs(&self) -> Vec<&Glyph>; fn glyph(&self, name: &str) -> Option<&Glyph>; @@ -36,6 +37,10 @@ impl FontView for Font { self.sources() } + fn default_source_id(&self) -> Option { + self.default_source_id() + } + fn layers(&self) -> Vec<(LayerId, &Layer)> { self.layers() .iter() diff --git a/crates/shift-backends/src/ufo/mod.rs b/crates/shift-backends/src/ufo/mod.rs index 2776c4e1..c8c1544a 100644 --- a/crates/shift-backends/src/ufo/mod.rs +++ b/crates/shift-backends/src/ufo/mod.rs @@ -25,7 +25,7 @@ impl FontWriter for UfoBackend { #[cfg(test)] mod tests { use super::*; - use shift_font::{Contour, Glyph, GlyphLayer, PointType}; + use shift_font::{Contour, Glyph, GlyphLayer, LayerId, PointType}; use std::fs; fn create_test_font() -> Font { @@ -36,10 +36,10 @@ mod tests { font.metrics_mut().ascender = 800.0; font.metrics_mut().descender = -200.0; - let default_layer_id = font.default_layer_id(); + let default_source_id = font.default_source_id().unwrap(); let mut glyph = Glyph::with_unicode("A".to_string(), 65); - let mut layer = GlyphLayer::with_width(600.0); + let mut layer = GlyphLayer::with_width(LayerId::new(), default_source_id, 600.0); let mut contour = Contour::new(); contour.add_point(0.0, 0.0, PointType::OnCurve, false); @@ -55,7 +55,7 @@ mod tests { inner.close(); layer.add_contour(inner); - glyph.set_layer(default_layer_id, layer); + glyph.set_layer(layer); font.insert_glyph(glyph); font @@ -96,11 +96,12 @@ mod tests { loaded_glyph.primary_unicode() ); - let default_layer_id = original.default_layer_id(); - let loaded_default_layer_id = loaded.default_layer_id(); - - let original_layer = original_glyph.layer(default_layer_id).unwrap(); - let loaded_layer = loaded_glyph.layer(loaded_default_layer_id).unwrap(); + let original_layer = original_glyph + .layer_for_source(original.default_source_id().unwrap()) + .unwrap(); + let loaded_layer = loaded_glyph + .layer_for_source(loaded.default_source_id().unwrap()) + .unwrap(); assert_eq!(original_layer.width(), loaded_layer.width()); assert_eq!( @@ -114,10 +115,10 @@ mod tests { #[test] fn writer_rounds_coordinates_and_skips_empty_contours() { let mut font = Font::new(); - let default_layer_id = font.default_layer_id(); + let default_source_id = font.default_source_id().unwrap(); let mut glyph = Glyph::with_unicode("A".to_string(), 0x0041); - let mut layer = GlyphLayer::with_width(500.4); + let mut layer = GlyphLayer::with_width(LayerId::new(), default_source_id, 500.4); let mut contour = Contour::new(); contour.add_point( @@ -142,7 +143,7 @@ mod tests { layer.add_contour(contour); layer.add_contour(Contour::new()); - glyph.set_layer(default_layer_id, layer); + glyph.set_layer(layer); font.insert_glyph(glyph); let temp_dir = std::env::temp_dir().join("shift_test_ufo_writer_format"); diff --git a/crates/shift-backends/src/ufo/reader.rs b/crates/shift-backends/src/ufo/reader.rs index 36dd0564..d5cd4983 100644 --- a/crates/shift-backends/src/ufo/reader.rs +++ b/crates/shift-backends/src/ufo/reader.rs @@ -3,7 +3,8 @@ use crate::traits::FontReader; use norad::{Font as NoradFont, Line}; use shift_font::{ Anchor, Component, Contour, FeatureData, Font, Glyph, GlyphLayer, Guideline, KerningData, - KerningPair, KerningSide, Layer, LibData, LibValue, PointType, Transform, + KerningPair, KerningSide, Layer, LayerId, LibData, LibValue, Location, PointType, Source, + SourceId, Transform, }; use std::collections::HashMap; use std::path::Path; @@ -100,9 +101,10 @@ impl UfoReader { fn convert_glyph_layer( norad_glyph: &norad::Glyph, - layer_id: shift_font::LayerId, - ) -> (Glyph, GlyphLayer) { - let mut glyph_layer = GlyphLayer::with_width(norad_glyph.width); + layer_id: LayerId, + source_id: SourceId, + ) -> Glyph { + let mut glyph_layer = GlyphLayer::with_width(layer_id, source_id, norad_glyph.width); if norad_glyph.height != 0.0 { glyph_layer.set_height(Some(norad_glyph.height)); } @@ -136,8 +138,8 @@ impl UfoReader { *glyph.lib_mut() = Self::convert_lib(&norad_glyph.lib); } - glyph.set_layer(layer_id, glyph_layer); - (glyph, GlyphLayer::new()) + glyph.set_layer(glyph_layer); + glyph } fn convert_kerning(norad_font: &NoradFont) -> KerningData { @@ -207,7 +209,9 @@ impl FontReader for UfoReader { let ufo_path = Path::new(path); let mut font = Font::new(); - let default_layer_id = font.default_layer_id(); + let default_source_id = font + .default_source_id() + .expect("new font should have a default source"); if let Some(family) = &norad_font.font_info.family_name { font.metadata_mut().family_name = Some(family.clone()); @@ -243,19 +247,20 @@ impl FontReader for UfoReader { let norad_default_layer_name = norad_font.layers.default_layer().name().clone(); for layer in norad_font.layers.iter() { - let layer_id = if layer.name() == &norad_default_layer_name { - default_layer_id + let source_id = if layer.name() == &norad_default_layer_name { + default_source_id } else { let new_layer = Layer::new(layer.name().to_string()); - font.add_layer(new_layer) + font.add_layer(new_layer); + font.add_source(Source::new(layer.name().to_string(), Location::new())) }; for norad_glyph in layer.iter() { - let (glyph, _) = Self::convert_glyph_layer(norad_glyph, layer_id); + let glyph = Self::convert_glyph_layer(norad_glyph, LayerId::new(), source_id); if let Some(existing) = font.glyph_mut(glyph.name()) { - if let Some(layer_data) = glyph.layer(layer_id) { - existing.set_layer(layer_id, layer_data.clone()); + for layer_data in glyph.layers().values() { + existing.set_layer(layer_data.as_ref().clone()); } } else { font.insert_glyph(glyph); diff --git a/crates/shift-backends/src/ufo/writer.rs b/crates/shift-backends/src/ufo/writer.rs index d2d9454f..02317ae1 100644 --- a/crates/shift-backends/src/ufo/writer.rs +++ b/crates/shift-backends/src/ufo/writer.rs @@ -278,28 +278,31 @@ impl UfoWriter { norad_font.lib = Self::convert_lib(font.lib()); } - let default_layer_id = font.default_layer_id(); + let default_source_id = font.default_source_id(); let default_layer = norad_font.layers.default_layer_mut(); for glyph in font.glyphs() { - if let Some(layer_data) = glyph.layer(default_layer_id) { + let Some(source_id) = default_source_id else { + continue; + }; + if let Some(layer_data) = glyph.layer_for_source(source_id) { let norad_glyph = Self::convert_glyph(glyph, layer_data); default_layer.insert_glyph(norad_glyph); } } - for (layer_id, layer) in font.layers() { - if layer_id == default_layer_id { + for source in font.sources() { + if Some(source.id()) == default_source_id { continue; } let norad_layer = norad_font .layers - .new_layer(layer.name()) + .new_layer(source.name()) .map_err(|e| e.to_string())?; for glyph in font.glyphs() { - if let Some(layer_data) = glyph.layer(layer_id) { + if let Some(layer_data) = glyph.layer_for_source(source.id()) { let norad_glyph = Self::convert_glyph(glyph, layer_data); norad_layer.insert_glyph(norad_glyph); } diff --git a/crates/shift-bridge/index.d.ts b/crates/shift-bridge/index.d.ts index 89fee11a..4ecbe54c 100644 --- a/crates/shift-bridge/index.d.ts +++ b/crates/shift-bridge/index.d.ts @@ -55,7 +55,7 @@ export interface GlyphHandle { export interface GlyphLayerRef { glyphHandle: GlyphHandle - layerId: LayerId + sourceId: SourceId } export interface NapiFontExportRequest { @@ -230,6 +230,5 @@ export interface NapiSource { id: SourceId name: string location: NapiLocation - layerId: LayerId filename?: string } diff --git a/crates/shift-bridge/src/bridge.rs b/crates/shift-bridge/src/bridge.rs index 12efa7a9..e1df3d79 100644 --- a/crates/shift-bridge/src/bridge.rs +++ b/crates/shift-bridge/src/bridge.rs @@ -38,8 +38,8 @@ pub struct GlyphHandle { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct GlyphLayerRef { pub glyph_handle: GlyphHandle, - #[napi(ts_type = "LayerId")] - pub layer_id: String, + #[napi(ts_type = "SourceId")] + pub source_id: String, } #[napi(object)] @@ -192,6 +192,10 @@ impl FontView for FontSaveSnapshot { self.font.sources() } + fn default_source_id(&self) -> Option { + self.font.default_source_id() + } + fn layers(&self) -> Vec<(LayerId, &shift_font::Layer)> { self .font @@ -398,13 +402,12 @@ impl Bridge { #[napi(ts_arg_type = "SourceId")] source_id: String, ) -> errors::Result> { let source_id = parse::(&source_id)?; - let layer_id = self.source_layer_id(source_id)?; let glyph = match self.glyph_for_read(&glyph_handle.name)? { Some(glyph) => glyph, None => return Ok(None), }; - let layer = match glyph.layer(layer_id) { + let layer = match glyph.layer_for_source(source_id) { Some(layer) => layer, None => return Ok(None), }; @@ -482,22 +485,6 @@ impl Bridge { ) } - fn source_layer_id(&self, source_id: SourceId) -> BridgeResult { - if let Some(source) = self - .font()? - .sources() - .iter() - .find(|source| source.id() == source_id) - { - return Ok(source.layer_id()); - } - - Err(BridgeError::InvalidInput { - kind: "source ID", - value: source_id.to_string(), - }) - } - fn save_snapshot(&self) -> BridgeResult { Ok(FontSaveSnapshot::new(self.font()?.clone(), None)) } @@ -507,10 +494,30 @@ impl Bridge { } fn glyph_layer_target(&self, glyph_ref: GlyphLayerRef) -> BridgeResult { - let layer_id = parse::(&glyph_ref.layer_id)?; + let source_id = parse::(&glyph_ref.source_id)?; + if !self + .font()? + .sources() + .iter() + .any(|source| source.id() == source_id) + { + return Err(BridgeError::InvalidInput { + kind: "source ID", + value: source_id.to_string(), + }); + } + + let layer_id = self + .font()? + .glyph(&glyph_ref.glyph_handle.name) + .and_then(|glyph| glyph.layer_for_source(source_id)) + .map(GlyphLayer::id) + .unwrap_or_else(LayerId::new); + Ok(GlyphLayerTarget { glyph_name: glyph_ref.glyph_handle.name, unicode: glyph_ref.glyph_handle.unicode, + source_id, layer_id, }) } @@ -1227,7 +1234,7 @@ mod tests { fn default_layer_ref(bridge: &Bridge, name: &str, unicode: Option) -> GlyphLayerRef { GlyphLayerRef { glyph_handle: glyph_handle(name, unicode), - layer_id: bridge.get_sources().unwrap()[0].layer_id.clone(), + source_id: bridge.get_sources().unwrap()[0].id.clone(), } } @@ -1324,7 +1331,7 @@ mod tests { .glyph("A") .expect("snapshot should include edited A"); let layer = glyph - .layer(snapshot.default_layer_id()) + .layer_for_source(snapshot.default_source_id().unwrap()) .expect("edited glyph should include default layer"); assert_eq!(bridge.get_glyphs().unwrap().len(), 1); @@ -1479,9 +1486,9 @@ mod tests { let result = bridge.add_contour(GlyphLayerRef { glyph_handle: glyph_handle("A", Some(65)), - layer_id: "not-a-layer-id".to_string(), + source_id: "not-a-source-id".to_string(), }); - assert!(result.err().unwrap().reason.contains("invalid layer ID")); + assert!(result.err().unwrap().reason.contains("invalid source ID")); } } diff --git a/crates/shift-font/README.md b/crates/shift-font/README.md new file mode 100644 index 00000000..86cba124 --- /dev/null +++ b/crates/shift-font/README.md @@ -0,0 +1,38 @@ +# shift-font + +`shift-font` owns Shift's live Rust font authoring model. + +This crate defines the domain objects used by Rust code to represent and mutate authored font data. It does not own SQLite persistence, `.shift` package IO, NAPI transport, Electron state, or TypeScript editor interaction state. + +## Object Model + +- `Font` owns glyphs, sources, axes, metadata, and font-level data. +- `Source` is an editable designspace position with a name and location. +- `Glyph` is a glyph concept identified by `GlyphId`. +- `GlyphLayer` is authored editable data for one glyph at one source. +- `Contour` and `Point` describe outline geometry inside a glyph layer. + +## Identity + +Stable IDs are identity. Names and Unicode values are editable metadata. + +- `GlyphId` identifies a glyph. +- `SourceId` identifies a source. +- `LayerId` identifies a glyph layer: the authored data for one glyph at one source. + +## Responsibilities + +- define font authoring data structures; +- keep local mutation behavior near the objects it mutates; +- define semantic change records for model changes; +- provide geometry, component, and variation helpers used by the Rust model. + +## Boundaries + +`shift-font` should not expose TypeScript-facing wire contracts. Those belong in `shift-wire`. + +`shift-font` should not perform SQLite persistence. Durable working-store reads and writes belong in `shift-store` and are coordinated by `shift-workspace`. + +`shift-font` should not own `.shift` package layout. Source package IO belongs in `shift-source`. + +`shift-font` should not own Electron, NAPI, or editor state. The TypeScript editor owns UI interaction, selection, hover, camera, tools, and command history. diff --git a/crates/shift-font/docs/DOCS.md b/crates/shift-font/docs/DOCS.md index 34feed90..6ac8e8c9 100644 --- a/crates/shift-font/docs/DOCS.md +++ b/crates/shift-font/docs/DOCS.md @@ -2,11 +2,27 @@ First-class Rust font object model for Shift. +## Object Model + +- `Font` owns glyphs, sources, axes, metadata, and font-level data. +- `Source` is an editable designspace position with a name and location. +- `Glyph` is a glyph concept identified by `GlyphId`. +- `GlyphLayer` is authored editable data for one glyph at one source. +- `Contour` and `Point` describe outline geometry inside a glyph layer. + +## Identity + +Stable IDs are identity. Names and Unicode values are editable metadata. + +- `GlyphId` identifies a glyph. +- `SourceId` identifies a source. +- `LayerId` identifies a glyph layer: the authored data for one glyph at one source. + ## Responsibilities - Own font authoring data structures such as `Font`, `Glyph`, `GlyphLayer`, `Contour`, `Point`, `Source`, and `Axis`. - Keep object-level mutation behavior near the objects it mutates. -- Provide model-native helpers for layer editing, composite resolution, interpolation support, and geometry-derived behavior. +- Provide model-native helpers for layer editing, component resolution, variation behavior, and geometry-derived behavior. - Stay independent of TypeScript, NAPI, and bridge DTOs. ## Boundaries @@ -15,7 +31,7 @@ First-class Rust font object model for Shift. `shift-font` should not perform SQLite persistence. Durable working-store reads and writes belong in `shift-store`. -`shift-font` should not own Electron, renderer, or tool state. The TypeScript editor owns UI interaction, selection, hover, camera, tools, and command history. +`shift-font` should not own Electron, NAPI, or editor state. The TypeScript editor owns UI interaction, selection, hover, camera, tools, and command history. ## Editing Shape @@ -28,4 +44,4 @@ layer.remove_points(&point_ids)?; layer.apply_bulk_node_positions(updates)?; ``` -Transport layers should pass enough identity to find the model object, then call these methods. They should not introduce hidden native edit sessions. +Transport and workspace layers should pass stable identity to find the model object, then call these methods. They should not introduce hidden native edit sessions. diff --git a/crates/shift-font/src/composite.rs b/crates/shift-font/src/composite.rs index 26cc8980..2917623b 100644 --- a/crates/shift-font/src/composite.rs +++ b/crates/shift-font/src/composite.rs @@ -471,7 +471,10 @@ fn compose_transform(outer: Transform, inner: Transform) -> Transform { #[cfg(test)] mod tests { use super::*; - use crate::{Anchor, Component, Contour, Font, Glyph, GlyphLayer, PointType, Transform}; + use crate::{ + Anchor, Component, Contour, Font, Glyph, GlyphLayer, LayerId, PointType, SourceId, + Transform, + }; fn two_point_contour(x0: f64, y0: f64, x1: f64, y1: f64) -> Contour { let mut contour = Contour::new(); @@ -480,27 +483,32 @@ mod tests { contour } + fn test_layer(source_id: SourceId, width: f64) -> GlyphLayer { + GlyphLayer::with_width(LayerId::new(), source_id, width) + } + #[test] fn flatten_includes_component_contours() { let mut font = Font::new(); - let layer_id = font.default_layer_id(); + let source_id = font.default_source_id().unwrap(); let mut base = Glyph::new("base".to_string()); - let mut base_layer = GlyphLayer::with_width(500.0); + let mut base_layer = test_layer(source_id, 500.0); base_layer.add_contour(two_point_contour(0.0, 0.0, 10.0, 10.0)); - base.set_layer(layer_id, base_layer); + base.set_layer(base_layer); font.insert_glyph(base); let mut composite = Glyph::new("comp".to_string()); - let mut composite_layer = GlyphLayer::with_width(500.0); + let mut composite_layer = test_layer(source_id, 500.0); + let composite_layer_id = composite_layer.id(); composite_layer.add_component(Component::new("base".to_string())); - composite.set_layer(layer_id, composite_layer); + composite.set_layer(composite_layer); font.insert_glyph(composite); let provider = FontLayerProvider::new(&font); let layer = font .glyph("comp") - .and_then(|glyph| glyph.layer(layer_id)) + .and_then(|glyph| glyph.layer(composite_layer_id)) .unwrap(); let resolved = flatten_component_contours_for_layer(&provider, layer, "comp"); @@ -511,32 +519,33 @@ mod tests { #[test] fn flatten_skips_cycle_and_keeps_other_branches() { let mut font = Font::new(); - let layer_id = font.default_layer_id(); + let source_id = font.default_source_id().unwrap(); let mut a = Glyph::new("A".to_string()); - let mut a_layer = GlyphLayer::with_width(500.0); + let mut a_layer = test_layer(source_id, 500.0); + let a_layer_id = a_layer.id(); a_layer.add_component(Component::new("B".to_string())); a_layer.add_component(Component::new("C".to_string())); - a.set_layer(layer_id, a_layer); + a.set_layer(a_layer); font.insert_glyph(a); let mut b = Glyph::new("B".to_string()); - let mut b_layer = GlyphLayer::with_width(500.0); + let mut b_layer = test_layer(source_id, 500.0); b_layer.add_contour(two_point_contour(0.0, 0.0, 20.0, 20.0)); b_layer.add_component(Component::new("A".to_string())); - b.set_layer(layer_id, b_layer); + b.set_layer(b_layer); font.insert_glyph(b); let mut c = Glyph::new("C".to_string()); - let mut c_layer = GlyphLayer::with_width(500.0); + let mut c_layer = test_layer(source_id, 500.0); c_layer.add_contour(two_point_contour(10.0, 0.0, 30.0, 20.0)); - c.set_layer(layer_id, c_layer); + c.set_layer(c_layer); font.insert_glyph(c); let provider = FontLayerProvider::new(&font); let layer = font .glyph("A") - .and_then(|glyph| glyph.layer(layer_id)) + .and_then(|glyph| glyph.layer(a_layer_id)) .unwrap(); let resolved = flatten_component_contours_for_layer(&provider, layer, "A"); @@ -546,33 +555,34 @@ mod tests { #[test] fn primary_anchor_attachment_applies_translation() { let mut font = Font::new(); - let layer_id = font.default_layer_id(); + let source_id = font.default_source_id().unwrap(); let mut base = Glyph::new("base".to_string()); - let mut base_layer = GlyphLayer::with_width(500.0); + let mut base_layer = test_layer(source_id, 500.0); base_layer.add_contour(two_point_contour(0.0, 0.0, 10.0, 0.0)); base_layer.add_anchor(Anchor::new(Some("top".to_string()), 100.0, 200.0)); - base.set_layer(layer_id, base_layer); + base.set_layer(base_layer); font.insert_glyph(base); let mut mark = Glyph::new("mark".to_string()); - let mut mark_layer = GlyphLayer::with_width(500.0); + let mut mark_layer = test_layer(source_id, 500.0); mark_layer.add_contour(two_point_contour(0.0, 0.0, 10.0, 0.0)); mark_layer.add_anchor(Anchor::new(Some("_top".to_string()), 5.0, 0.0)); - mark.set_layer(layer_id, mark_layer); + mark.set_layer(mark_layer); font.insert_glyph(mark); let mut comp = Glyph::new("comp".to_string()); - let mut comp_layer = GlyphLayer::with_width(500.0); + let mut comp_layer = test_layer(source_id, 500.0); + let comp_layer_id = comp_layer.id(); comp_layer.add_component(Component::new("base".to_string())); comp_layer.add_component(Component::new("mark".to_string())); - comp.set_layer(layer_id, comp_layer); + comp.set_layer(comp_layer); font.insert_glyph(comp); let provider = FontLayerProvider::new(&font); let layer = font .glyph("comp") - .and_then(|glyph| glyph.layer(layer_id)) + .and_then(|glyph| glyph.layer(comp_layer_id)) .unwrap(); let resolved = flatten_component_contours_for_layer(&provider, layer, "comp"); @@ -587,26 +597,27 @@ mod tests { #[test] fn explicit_transform_applies_without_attachment() { let mut font = Font::new(); - let layer_id = font.default_layer_id(); + let source_id = font.default_source_id().unwrap(); let mut mark = Glyph::new("mark".to_string()); - let mut mark_layer = GlyphLayer::with_width(500.0); + let mut mark_layer = test_layer(source_id, 500.0); mark_layer.add_contour(two_point_contour(0.0, 0.0, 10.0, 0.0)); mark_layer.add_anchor(Anchor::new(Some("top".to_string()), 5.0, 0.0)); - mark.set_layer(layer_id, mark_layer); + mark.set_layer(mark_layer); font.insert_glyph(mark); let mut comp = Glyph::new("comp".to_string()); - let mut comp_layer = GlyphLayer::with_width(500.0); + let mut comp_layer = test_layer(source_id, 500.0); + let comp_layer_id = comp_layer.id(); let matrix = Transform::translate(30.0, 40.0); comp_layer.add_component(Component::with_matrix("mark".to_string(), &matrix)); - comp.set_layer(layer_id, comp_layer); + comp.set_layer(comp_layer); font.insert_glyph(comp); let provider = FontLayerProvider::new(&font); let layer = font .glyph("comp") - .and_then(|glyph| glyph.layer(layer_id)) + .and_then(|glyph| glyph.layer(comp_layer_id)) .unwrap(); let resolved = flatten_component_contours_for_layer(&provider, layer, "comp"); @@ -620,34 +631,35 @@ mod tests { #[test] fn parent_anchor_hints_do_not_affect_component_placement() { let mut font = Font::new(); - let layer_id = font.default_layer_id(); + let source_id = font.default_source_id().unwrap(); let mut base = Glyph::new("base".to_string()); - let mut base_layer = GlyphLayer::with_width(500.0); + let mut base_layer = test_layer(source_id, 500.0); base_layer.add_anchor(Anchor::new(Some("top".to_string()), 100.0, 200.0)); base_layer.add_contour(two_point_contour(0.0, 0.0, 10.0, 0.0)); - base.set_layer(layer_id, base_layer); + base.set_layer(base_layer); font.insert_glyph(base); let mut mark = Glyph::new("mark".to_string()); - let mut mark_layer = GlyphLayer::with_width(500.0); + let mut mark_layer = test_layer(source_id, 500.0); mark_layer.add_anchor(Anchor::new(Some("top_extra".to_string()), 5.0, 0.0)); mark_layer.add_contour(two_point_contour(0.0, 0.0, 10.0, 0.0)); - mark.set_layer(layer_id, mark_layer); + mark.set_layer(mark_layer); font.insert_glyph(mark); let mut comp = Glyph::new("comp".to_string()); - let mut comp_layer = GlyphLayer::with_width(500.0); + let mut comp_layer = test_layer(source_id, 500.0); + let comp_layer_id = comp_layer.id(); comp_layer.add_anchor(Anchor::new(Some("parent_top".to_string()), 0.0, 0.0)); comp_layer.add_component(Component::new("base".to_string())); comp_layer.add_component(Component::new("mark".to_string())); - comp.set_layer(layer_id, comp_layer); + comp.set_layer(comp_layer); font.insert_glyph(comp); let provider = FontLayerProvider::new(&font); let layer = font .glyph("comp") - .and_then(|glyph| glyph.layer(layer_id)) + .and_then(|glyph| glyph.layer(comp_layer_id)) .unwrap(); let resolved = flatten_component_contours_for_layer(&provider, layer, "comp"); @@ -662,35 +674,36 @@ mod tests { #[test] fn multiple_marks_attach_to_latest_matching_anchor() { let mut font = Font::new(); - let layer_id = font.default_layer_id(); + let source_id = font.default_source_id().unwrap(); let mut base = Glyph::new("base".to_string()); - let mut base_layer = GlyphLayer::with_width(500.0); + let mut base_layer = test_layer(source_id, 500.0); base_layer.add_anchor(Anchor::new(Some("top".to_string()), 100.0, 200.0)); base_layer.add_contour(two_point_contour(0.0, 0.0, 10.0, 0.0)); - base.set_layer(layer_id, base_layer); + base.set_layer(base_layer); font.insert_glyph(base); let mut mark = Glyph::new("mark".to_string()); - let mut mark_layer = GlyphLayer::with_width(500.0); + let mut mark_layer = test_layer(source_id, 500.0); mark_layer.add_anchor(Anchor::new(Some("_top".to_string()), 5.0, 0.0)); mark_layer.add_anchor(Anchor::new(Some("top".to_string()), 5.0, 20.0)); mark_layer.add_contour(two_point_contour(0.0, 0.0, 10.0, 0.0)); - mark.set_layer(layer_id, mark_layer); + mark.set_layer(mark_layer); font.insert_glyph(mark); let mut comp = Glyph::new("comp".to_string()); - let mut comp_layer = GlyphLayer::with_width(500.0); + let mut comp_layer = test_layer(source_id, 500.0); + let comp_layer_id = comp_layer.id(); comp_layer.add_component(Component::new("base".to_string())); comp_layer.add_component(Component::new("mark".to_string())); comp_layer.add_component(Component::new("mark".to_string())); - comp.set_layer(layer_id, comp_layer); + comp.set_layer(comp_layer); font.insert_glyph(comp); let provider = FontLayerProvider::new(&font); let layer = font .glyph("comp") - .and_then(|glyph| glyph.layer(layer_id)) + .and_then(|glyph| glyph.layer(comp_layer_id)) .unwrap(); let resolved = flatten_component_contours_for_layer(&provider, layer, "comp"); diff --git a/crates/shift-font/src/ir/font.rs b/crates/shift-font/src/ir/font.rs index 3c638063..b8ca8a4d 100644 --- a/crates/shift-font/src/ir/font.rs +++ b/crates/shift-font/src/ir/font.rs @@ -103,7 +103,7 @@ impl Default for Font { let default_layer_id = LayerId::new(); let mut layers = HashMap::new(); layers.insert(default_layer_id, Layer::default_layer()); - let default_source = Source::new("Regular".to_string(), Location::new(), default_layer_id); + let default_source = Source::new("Regular".to_string(), Location::new()); let default_source_id = default_source.id(); Self { @@ -365,11 +365,12 @@ mod tests { fn synthetic_point_heavy_font(mark: PerfFontMark) -> Font { let mut font = Font::new(); - let default_layer_id = font.default_layer_id(); + let source_id = font.default_source_id().unwrap(); for glyph_index in 0..mark.glyphs { let mut glyph = Glyph::with_unicode(format!("g{glyph_index:05}"), glyph_index as u32); - let mut layer = GlyphLayer::with_width(500.0 + glyph_index as f64); + let mut layer = + GlyphLayer::with_width(LayerId::new(), source_id, 500.0 + glyph_index as f64); for contour_index in 0..mark.contours_per_glyph { let mut contour = Contour::new(); @@ -384,7 +385,7 @@ mod tests { layer.add_contour(contour); } - glyph.set_layer(default_layer_id, layer); + glyph.set_layer(layer); font.insert_glyph(glyph); } @@ -414,8 +415,9 @@ mod tests { fn font_glyph_operations() { let mut font = Font::new(); let mut glyph = Glyph::with_unicode("A".to_string(), 65); - let layer = GlyphLayer::with_width(600.0); - glyph.set_layer(font.default_layer_id(), layer); + let layer = + GlyphLayer::with_width(LayerId::new(), font.default_source_id().unwrap(), 600.0); + glyph.set_layer(layer); font.insert_glyph(glyph); @@ -531,13 +533,13 @@ mod tests { }; let mut font = synthetic_point_heavy_font(mark); let snapshot = font.clone(); - let default_layer_id = font.default_layer_id(); + let default_source_id = font.default_source_id().unwrap(); let start = Instant::now(); font.glyph_mut("g00000") .expect("target glyph should exist") - .layer_mut(default_layer_id) - .expect("target layer should exist") + .layer_for_source_mut(default_source_id) + .expect("target source layer should exist") .set_width(777.0); let elapsed = start.elapsed(); @@ -545,7 +547,7 @@ mod tests { assert_eq!( font.glyph("g00000") .unwrap() - .layer(default_layer_id) + .layer_for_source(default_source_id) .unwrap() .width(), 777.0 @@ -554,7 +556,7 @@ mod tests { snapshot .glyph("g00000") .unwrap() - .layer(snapshot.default_layer_id()) + .layer_for_source(snapshot.default_source_id().unwrap()) .unwrap() .width(), 777.0 diff --git a/crates/shift-font/src/ir/glyph.rs b/crates/shift-font/src/ir/glyph.rs index 82fe86ad..9905f0e6 100644 --- a/crates/shift-font/src/ir/glyph.rs +++ b/crates/shift-font/src/ir/glyph.rs @@ -1,7 +1,7 @@ use crate::anchor::Anchor; use crate::component::Component; use crate::contour::Contour; -use crate::entity::{AnchorId, ComponentId, ContourId, GlyphId, LayerId}; +use crate::entity::{AnchorId, ComponentId, ContourId, GlyphId, LayerId, SourceId}; use crate::guideline::Guideline; use crate::lib_data::LibData; use crate::GlyphName; @@ -19,8 +19,10 @@ pub struct Glyph { lib: LibData, } -#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct GlyphLayer { + id: LayerId, + source_id: SourceId, width: f64, height: Option, contours: IndexMap, @@ -31,17 +33,42 @@ pub struct GlyphLayer { } impl GlyphLayer { - pub fn new() -> Self { - Self::default() + pub fn new(id: LayerId, source_id: SourceId) -> Self { + Self { + id, + source_id, + width: 0.0, + height: None, + contours: IndexMap::new(), + components: HashMap::new(), + anchors: Vec::new(), + guidelines: Vec::new(), + lib: LibData::new(), + } } - pub fn with_width(width: f64) -> Self { + pub fn with_width(id: LayerId, source_id: SourceId, width: f64) -> Self { Self { width, - ..Self::default() + ..Self::new(id, source_id) } } + pub fn id(&self) -> LayerId { + self.id + } + + pub fn source_id(&self) -> SourceId { + self.source_id + } + + pub fn clone_with_identity(&self, id: LayerId, source_id: SourceId) -> Self { + let mut layer = self.clone(); + layer.id = id; + layer.source_id = source_id; + layer + } + pub fn width(&self) -> f64 { self.width } @@ -264,12 +291,38 @@ impl Glyph { self.layers.get_mut(&id).map(Arc::make_mut) } - pub fn get_or_create_layer(&mut self, id: LayerId) -> &mut GlyphLayer { - Arc::make_mut(self.layers.entry(id).or_default()) + pub fn ensure_layer_for_source(&mut self, source_id: SourceId) -> &mut GlyphLayer { + if let Some(layer_id) = self + .layers + .values() + .find(|layer| layer.source_id() == source_id) + .map(|layer| layer.id()) + { + return self.layer_mut(layer_id).expect("layer id came from glyph"); + } + + let layer = GlyphLayer::new(LayerId::new(), source_id); + let layer_id = layer.id(); + self.layers.insert(layer_id, Arc::new(layer)); + self.layer_mut(layer_id).expect("layer was just inserted") + } + + pub fn set_layer(&mut self, layer: GlyphLayer) { + self.layers.insert(layer.id(), Arc::new(layer)); } - pub fn set_layer(&mut self, id: LayerId, layer: GlyphLayer) { - self.layers.insert(id, Arc::new(layer)); + pub fn layer_for_source(&self, source_id: SourceId) -> Option<&GlyphLayer> { + self.layers + .values() + .find(|layer| layer.source_id() == source_id) + .map(Arc::as_ref) + } + + pub fn layer_for_source_mut(&mut self, source_id: SourceId) -> Option<&mut GlyphLayer> { + self.layers + .values_mut() + .find(|layer| layer.source_id() == source_id) + .map(Arc::make_mut) } pub fn remove_layer(&mut self, id: LayerId) -> Option { @@ -301,21 +354,33 @@ mod tests { #[test] fn glyph_layer_operations() { let mut g = Glyph::new("A".to_string()); - let layer_id = LayerId::new(); + let source_id = SourceId::new(); - let layer = g.get_or_create_layer(layer_id); + let layer = g.ensure_layer_for_source(source_id); + let layer_id = layer.id(); layer.set_width(600.0); assert_eq!(g.layer(layer_id).unwrap().width(), 600.0); + assert_eq!(g.layer_for_source(source_id).unwrap().id(), layer_id); } #[test] fn cloned_glyph_shares_layers_until_one_layer_is_mutated() { let mut glyph = Glyph::new("A".to_string()); + let first_source_id = SourceId::new(); + let second_source_id = SourceId::new(); let first_layer_id = LayerId::new(); let second_layer_id = LayerId::new(); - glyph.set_layer(first_layer_id, GlyphLayer::with_width(500.0)); - glyph.set_layer(second_layer_id, GlyphLayer::with_width(600.0)); + glyph.set_layer(GlyphLayer::with_width( + first_layer_id, + first_source_id, + 500.0, + )); + glyph.set_layer(GlyphLayer::with_width( + second_layer_id, + second_source_id, + 600.0, + )); let snapshot = glyph.clone(); glyph @@ -337,7 +402,7 @@ mod tests { #[test] fn glyph_layer_contours() { - let mut layer = GlyphLayer::with_width(500.0); + let mut layer = GlyphLayer::with_width(LayerId::new(), SourceId::new(), 500.0); assert!(layer.is_empty()); let contour = Contour::new(); @@ -349,7 +414,7 @@ mod tests { #[test] fn glyph_layer_anchors_are_ordered() { - let mut layer = GlyphLayer::new(); + let mut layer = GlyphLayer::new(LayerId::new(), SourceId::new()); let a1 = layer.add_anchor(Anchor::new(Some("top".to_string()), 10.0, 20.0)); let a2 = layer.add_anchor(Anchor::new(Some("bottom".to_string()), 30.0, 40.0)); diff --git a/crates/shift-font/src/ir/source.rs b/crates/shift-font/src/ir/source.rs index 0c44f278..ddff0574 100644 --- a/crates/shift-font/src/ir/source.rs +++ b/crates/shift-font/src/ir/source.rs @@ -1,5 +1,5 @@ use crate::axis::Location; -use crate::entity::{LayerId, SourceId}; +use crate::entity::SourceId; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Serialize, Deserialize)] @@ -8,32 +8,24 @@ pub struct Source { id: SourceId, name: String, location: Location, - layer_id: LayerId, filename: Option, } impl Source { - pub fn new(name: String, location: Location, layer_id: LayerId) -> Self { + pub fn new(name: String, location: Location) -> Self { Self { id: SourceId::new(), name, location, - layer_id, filename: None, } } - pub fn with_filename( - name: String, - location: Location, - layer_id: LayerId, - filename: String, - ) -> Self { + pub fn with_filename(name: String, location: Location, filename: String) -> Self { Self { id: SourceId::new(), name, location, - layer_id, filename: Some(filename), } } @@ -50,10 +42,6 @@ impl Source { &self.location } - pub fn layer_id(&self) -> LayerId { - self.layer_id - } - pub fn filename(&self) -> Option<&str> { self.filename.as_deref() } @@ -73,11 +61,10 @@ mod tests { #[test] fn source_creation() { - let layer_id = LayerId::new(); let mut location = Location::new(); location.set("wght".to_string(), 400.0); - let source = Source::new("Regular".to_string(), location, layer_id); + let source = Source::new("Regular".to_string(), location); assert_eq!(source.name(), "Regular"); assert_eq!(source.location().get("wght"), Some(400.0)); } diff --git a/crates/shift-font/src/layer_edit.rs b/crates/shift-font/src/layer_edit.rs index f0238030..8fad9439 100644 --- a/crates/shift-font/src/layer_edit.rs +++ b/crates/shift-font/src/layer_edit.rs @@ -720,10 +720,10 @@ pub struct PasteResult { #[cfg(test)] mod tests { use super::*; - use crate::{Anchor, Component}; + use crate::{Anchor, Component, LayerId, SourceId}; fn create_session() -> GlyphLayer { - GlyphLayer::with_width(500.0) + GlyphLayer::with_width(LayerId::new(), SourceId::new(), 500.0) } fn session_with_contour() -> (GlyphLayer, ContourId) { diff --git a/crates/shift-font/src/lib.rs b/crates/shift-font/src/lib.rs index 04fa4228..25507258 100644 --- a/crates/shift-font/src/lib.rs +++ b/crates/shift-font/src/lib.rs @@ -1,3 +1,34 @@ +//! Shift's live font authoring model. +//! +//! `shift-font` owns the Rust object model for authored font data and the local +//! mutation behavior on that data. It does not own durable storage, `.shift` +//! package IO, Electron, NAPI, or TypeScript editor interaction state. +//! +//! # Object Model +//! +//! - [`Font`] owns glyphs, sources, axes, metadata, and font-level data. +//! - [`Source`] is an editable designspace position with a name and location. +//! - [`Glyph`] is a glyph concept identified by [`GlyphId`]. +//! - [`GlyphLayer`] is authored editable data for one glyph at one source. +//! - [`Contour`] and [`Point`] describe outline geometry inside a glyph layer. +//! +//! # Identity +//! +//! Stable IDs are identity. Names and Unicode values are editable metadata. +//! +//! A [`GlyphId`] identifies a glyph. A [`SourceId`] identifies a source. A +//! [`LayerId`] identifies a glyph layer: the authored data for one glyph at one +//! source. +//! +//! # Boundaries +//! +//! `shift-font` defines domain objects, local mutation methods, change records, +//! geometry helpers, component resolution, and variation helpers. +//! +//! Persistence belongs to `shift-store` and `shift-workspace`. Source package IO +//! belongs to `shift-source`. Transport belongs to `shift-bridge` and +//! `shift-wire`. UI interaction belongs to the TypeScript editor. +//! pub mod changes; pub mod composite; pub mod curve; diff --git a/crates/shift-store/src/change_set.rs b/crates/shift-store/src/change_set.rs index 3b501c6a..43ae52dd 100644 --- a/crates/shift-store/src/change_set.rs +++ b/crates/shift-store/src/change_set.rs @@ -36,15 +36,15 @@ impl ShiftStore { upsert_glyph(&tx, glyph.id(), glyph.glyph_name())?; replace_glyph_unicodes(&tx, glyph.id(), glyph.unicodes())?; - for (layer_id, layer) in glyph.layers() { + for layer in glyph.layers().values().map(|layer| layer.as_ref()) { 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, + source_id: layer.source_id(), + layer_id: layer.id(), }; upsert_layer(&tx, &target, layer.width(), layer.height())?; - replace_layer_geometry(&tx, &target, &font::GlyphLayerValue::from(layer.as_ref()))?; + replace_layer_geometry(&tx, &target, &font::GlyphLayerValue::from(layer))?; } } @@ -363,12 +363,3 @@ fn require_changed(rows_changed: usize, kind: &'static str, id: String) -> Resul 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/tests/store_test.rs b/crates/shift-store/tests/store_test.rs index 4e432c71..d6038abd 100644 --- a/crates/shift-store/tests/store_test.rs +++ b/crates/shift-store/tests/store_test.rs @@ -341,7 +341,8 @@ 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); + let mut layer = + shift_font::GlyphLayer::with_width(shift_font::LayerId::new(), target.source_id, 500.0); layer.add_contour(contour_with_point(10.0, 20.0)); store diff --git a/crates/shift-wire/src/bridges/napi/mod.rs b/crates/shift-wire/src/bridges/napi/mod.rs index f8b3b952..90e2e5b0 100644 --- a/crates/shift-wire/src/bridges/napi/mod.rs +++ b/crates/shift-wire/src/bridges/napi/mod.rs @@ -140,8 +140,6 @@ pub struct NapiSource { pub id: String, pub name: String, pub location: NapiLocation, - #[napi(ts_type = "LayerId")] - pub layer_id: String, pub filename: Option, } @@ -151,7 +149,6 @@ impl From for NapiSource { id: source.id.to_string(), name: source.name, location: source.location.into(), - layer_id: source.layer_id.to_string(), filename: source.filename, } } diff --git a/crates/shift-wire/src/interpolation.rs b/crates/shift-wire/src/interpolation.rs index 948d799c..8edf7081 100644 --- a/crates/shift-wire/src/interpolation.rs +++ b/crates/shift-wire/src/interpolation.rs @@ -162,7 +162,7 @@ pub fn build_masters(font: &Font, glyph: &Glyph) -> Option> { let mut masters: Vec = Vec::new(); for source in font.sources() { - let layer = match glyph.layer(source.layer_id()) { + let layer = match glyph.layer_for_source(source.id()) { Some(layer) if !layer.contours().is_empty() || !layer.anchors().is_empty() diff --git a/crates/shift-wire/src/lib.rs b/crates/shift-wire/src/lib.rs index 46169193..7ae771c7 100644 --- a/crates/shift-wire/src/lib.rs +++ b/crates/shift-wire/src/lib.rs @@ -10,7 +10,7 @@ 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, + GlyphName, GuidelineId, Location as IrLocation, Point as IrPoint, PointId, PointType as IrPointType, Source as IrSource, SourceId, }; @@ -134,7 +134,6 @@ pub struct Source { pub id: SourceId, pub name: String, pub location: Location, - pub layer_id: LayerId, pub filename: Option, } @@ -144,7 +143,6 @@ impl From<&IrSource> for Source { id: source.id(), name: source.name().to_string(), location: source.location().into(), - layer_id: source.layer_id(), filename: source.filename().map(str::to_string), } } diff --git a/crates/shift-wire/src/state.rs b/crates/shift-wire/src/state.rs index 4bac7b70..2091e0dc 100644 --- a/crates/shift-wire/src/state.rs +++ b/crates/shift-wire/src/state.rs @@ -5,15 +5,17 @@ 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, + ContourId, CoreError, CoreResult, DecomposedTransform as IrTransform, GlyphLayer, LayerId, + PointId, PointType as IrPointType, SourceId, }; pub fn layer_from_state( + layer_id: LayerId, + source_id: SourceId, structure: &GlyphStructure, values: &[GlyphValue], ) -> CoreResult { - let mut layer = GlyphLayer::new(); + let mut layer = GlyphLayer::new(layer_id, source_id); apply_state_to_layer(&mut layer, structure, values)?; Ok(layer) } @@ -178,10 +180,10 @@ fn restore_components( mod tests { use super::*; use crate::values_from_layer; - use shift_font::{Anchor, Component, DecomposedTransform}; + use shift_font::{Anchor, Component, DecomposedTransform, LayerId, SourceId}; fn sample_layer() -> GlyphLayer { - let mut layer = GlyphLayer::with_width(500.0); + let mut layer = GlyphLayer::with_width(LayerId::new(), SourceId::new(), 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); @@ -232,8 +234,15 @@ mod tests { 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))?; - + let restored = layer_from_state( + layer.id(), + layer.source_id(), + &structure, + &values_from_layer(&layer), + )?; + + assert_eq!(restored.id(), layer.id()); + assert_eq!(restored.source_id(), layer.source_id()); assert_eq!(restored.width(), 500.0); let contour = restored.contour(ContourId::from_raw(10)).unwrap(); @@ -264,7 +273,7 @@ mod tests { #[test] fn glyph_structure_sorts_components_by_id() { - let mut layer = GlyphLayer::new(); + let mut layer = GlyphLayer::new(LayerId::new(), SourceId::new()); layer.add_component(Component::with_id( ComponentId::from_raw(200), "later".to_string(), @@ -291,7 +300,7 @@ mod tests { }; assert!(matches!( - layer_from_state(&structure, &[]), + layer_from_state(LayerId::new(), SourceId::new(), &structure, &[]), Err(CoreError::MissingGlyphValue { index: 0 }) )); } @@ -305,7 +314,7 @@ mod tests { }; assert!(matches!( - layer_from_state(&structure, &[500.0, 1.0]), + layer_from_state(LayerId::new(), SourceId::new(), &structure, &[500.0, 1.0]), Err(CoreError::TrailingGlyphValues { expected: 1, actual: 2, @@ -326,7 +335,7 @@ mod tests { }; assert!(matches!( - layer_from_state(&structure, &[500.0]), + layer_from_state(LayerId::new(), SourceId::new(), &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..d7db1b37 100644 --- a/crates/shift-workspace/src/workspace.rs +++ b/crates/shift-workspace/src/workspace.rs @@ -47,6 +47,7 @@ pub enum WorkspaceSource { pub struct GlyphLayerTarget { pub glyph_name: String, pub unicode: Option, + pub source_id: SourceId, pub layer_id: LayerId, } @@ -202,17 +203,36 @@ impl FontWorkspace { if let Some(unicode) = target.unicode { glyph.add_unicode(unicode); } - glyph.set_layer(target.layer_id, GlyphLayer::with_width(500.0)); + glyph.set_layer(GlyphLayer::with_width( + target.layer_id, + target.source_id, + 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(), - } - })?; + if !next_font + .sources() + .iter() + .any(|source| source.id() == target.source_id) + { + return Err(WorkspaceError::InvalidInput { + kind: "source ID", + value: target.source_id.to_string(), + }); + } + + if let Some(glyph) = next_font.glyph_mut(&target.glyph_name) + && glyph.layer(target.layer_id).is_none() + { + glyph.set_layer(GlyphLayer::with_width( + LayerId::new(), + target.source_id, + 500.0, + )); + } + let glyph = next_font .glyph(&target.glyph_name) @@ -223,7 +243,7 @@ impl FontWorkspace { let change_target = GlyphLayerChangeTarget { glyph_id: glyph.id(), glyph_name: glyph.glyph_name().clone(), - source_id, + source_id: target.source_id, layer_id: target.layer_id, }; let glyph = next_font.glyph_mut(&target.glyph_name).ok_or_else(|| { @@ -237,7 +257,13 @@ impl FontWorkspace { glyph.add_unicode(unicode); } - let layer = glyph.get_or_create_layer(target.layer_id); + let layer = + glyph + .layer_mut(target.layer_id) + .ok_or_else(|| WorkspaceError::InvalidInput { + kind: "layer ID", + value: target.layer_id.to_string(), + })?; let result = edit(layer)?; let layer_changes = changes(&change_target, layer, &result); change_set.changes.extend(layer_changes.changes); @@ -345,11 +371,3 @@ 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)) -} diff --git a/packages/types/src/bridge/generated.ts b/packages/types/src/bridge/generated.ts index 3f5b9b82..1e865097 100644 --- a/packages/types/src/bridge/generated.ts +++ b/packages/types/src/bridge/generated.ts @@ -4,7 +4,6 @@ import type { AnchorId, ComponentId, GuidelineId, - LayerId, SourceId, } from "../ids"; @@ -60,7 +59,7 @@ export interface GlyphHandle { export interface GlyphLayerRef { glyphHandle: GlyphHandle - layerId: LayerId + sourceId: SourceId } export interface FontExportRequest { @@ -232,6 +231,5 @@ export interface Source { id: SourceId name: string location: Location - layerId: LayerId filename?: string } diff --git a/scripts/generate-bridge-types.mjs b/scripts/generate-bridge-types.mjs index bcc5a27d..04f9e723 100644 --- a/scripts/generate-bridge-types.mjs +++ b/scripts/generate-bridge-types.mjs @@ -21,7 +21,6 @@ const idTypeNames = new Set([ "AnchorId", "ComponentId", "GuidelineId", - "LayerId", "SourceId", ]); const scalarTypeNames = new Set(["GlyphName", "Unicode"]);