Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 9 additions & 8 deletions apps/desktop/src/renderer/src/lib/model/SourcePositionPatch.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { GlyphPositions } from "@shift/glyph-state";
import type { AnchorId, PointId } from "@shift/types";

export type BridgePositionPatchPayload = readonly [
pointIds: BigUint64Array | null,
pointIds: PointId[] | null,
pointCoords: Float64Array | null,
anchorIds: BigUint64Array | null,
anchorIds: AnchorId[] | null,
anchorCoords: Float64Array | null,
];

Expand Down Expand Up @@ -31,28 +32,28 @@ export class SourcePositionPatch {
}

toBridgePayload(): BridgePositionPatchPayload {
const pointIds: bigint[] = [];
const pointIds: PointId[] = [];
const pointCoords: number[] = [];
const anchorIds: bigint[] = [];
const anchorIds: AnchorId[] = [];
const anchorCoords: number[] = [];

for (const position of this.positions) {
switch (position.kind) {
case "point":
pointIds.push(BigInt(position.id));
pointIds.push(position.id);
pointCoords.push(position.x, position.y);
break;
case "anchor":
anchorIds.push(BigInt(position.id));
anchorIds.push(position.id);
anchorCoords.push(position.x, position.y);
break;
}
}

return [
pointIds.length > 0 ? BigUint64Array.from(pointIds) : null,
pointIds.length > 0 ? pointIds : null,
pointCoords.length > 0 ? Float64Array.from(pointCoords) : null,
anchorIds.length > 0 ? BigUint64Array.from(anchorIds) : null,
anchorIds.length > 0 ? anchorIds : null,
anchorCoords.length > 0 ? Float64Array.from(anchorCoords) : null,
];
}
Expand Down
7 changes: 5 additions & 2 deletions crates/shift-backends/src/binary/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,11 @@ fn font_from_skrifa(font: &FontRef<'_>) -> FormatBackendResult<Font> {
.unwrap_or_else(|| format!("uni{unicode:04X}"));

let mut glyph = Glyph::with_unicode(glyph_name, unicode);
let mut layer =
GlyphLayer::with_width(LayerId::new(), default_source_id, advance_width as f64);
let mut layer = GlyphLayer::with_width(
LayerId::new(),
default_source_id.clone(),
advance_width as f64,
);
let mut contours = pen.contours();
detect_smooth_points(&mut contours);
for contour in contours {
Expand Down
17 changes: 9 additions & 8 deletions crates/shift-backends/src/designspace/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ impl DesignspaceReader {
default_location,
default_ds_source.filename.clone(),
));
font.set_default_source_id(default_source_id);
font.set_default_source_id(default_source_id.clone());
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.
Expand Down Expand Up @@ -165,11 +165,12 @@ impl DesignspaceReader {

// Copy glyphs from the resolved layer into the new layer.
for source_glyph in source_font.glyphs() {
if let Some(source_layer) = source_glyph.layer_for_source(source_source_id) {
if let Some(source_layer) = source_glyph.layer_for_source(source_source_id.clone())
{
if let Some(glyph_id) = font.glyph_id_by_name(source_glyph.name()) {
font.insert_glyph_layer(
glyph_id,
source_layer.clone_with_identity(LayerId::new(), source_id),
source_layer.clone_with_identity(LayerId::new(), source_id.clone()),
)?;
}
}
Expand Down Expand Up @@ -242,7 +243,7 @@ fn load_axisless_designspace(
Location::new(),
default_source.filename.clone(),
));
font.set_default_source_id(default_source_id);
font.set_default_source_id(default_source_id.clone());
move_glyph_layers_to_source(&mut font, default_ufo_source_id, default_source_id)?;

let mut ufo_cache: HashMap<String, Font> = HashMap::new();
Expand Down Expand Up @@ -289,11 +290,11 @@ fn load_axisless_designspace(
));

for source_glyph in source_font.glyphs() {
if let Some(source_layer) = source_glyph.layer_for_source(source_source_id) {
if let Some(source_layer) = source_glyph.layer_for_source(source_source_id.clone()) {
if let Some(glyph_id) = font.glyph_id_by_name(source_glyph.name()) {
font.insert_glyph_layer(
glyph_id,
source_layer.clone_with_identity(LayerId::new(), source_id),
source_layer.clone_with_identity(LayerId::new(), source_id.clone()),
)?;
}
}
Expand Down Expand Up @@ -435,11 +436,11 @@ fn move_glyph_layers_to_source(
let layer_moves: Vec<_> = font
.glyphs()
.filter_map(|glyph| {
glyph.layer_for_source(from_source_id).map(|layer| {
glyph.layer_for_source(from_source_id.clone()).map(|layer| {
(
glyph.id(),
layer.id(),
layer.clone_with_identity(LayerId::new(), to_source_id),
layer.clone_with_identity(LayerId::new(), to_source_id.clone()),
)
})
})
Expand Down
4 changes: 2 additions & 2 deletions crates/shift-backends/src/glyphs/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ impl FontReader for GlyphsReader {
}
}
let source_id = font.add_source(Source::new(master.name.clone(), location));
source_by_master_id.insert(master.id.clone(), source_id);
source_by_master_id.insert(master.id.clone(), source_id.clone());
if master_idx == glyphs_font.default_master_idx {
font.set_default_source_id(source_id);
}
Expand All @@ -203,7 +203,7 @@ impl FontReader for GlyphsReader {
}

for layer in &glyph.layers {
let Some(source_id) = source_by_master_id.get(layer.master_id()).copied() else {
let Some(source_id) = source_by_master_id.get(layer.master_id()).cloned() else {
continue;
};

Expand Down
7 changes: 4 additions & 3 deletions crates/shift-backends/src/ufo/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,17 +248,18 @@ impl FontReader for UfoReader {
let norad_default_layer_name = norad_font.layers.default_layer().name().clone();
for layer in norad_font.layers.iter() {
let source_id = if layer.name() == &norad_default_layer_name {
default_source_id
default_source_id.clone()
} else {
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, LayerId::new(), source_id);
let glyph =
Self::convert_glyph_layer(norad_glyph, LayerId::new(), source_id.clone());

if let Some(glyph_id) = font.glyph_id_by_name(glyph.name()) {
for layer_data in glyph.layers().values() {
font.insert_glyph_layer(glyph_id, layer_data.as_ref().clone())?;
font.insert_glyph_layer(glyph_id.clone(), layer_data.as_ref().clone())?;
}
} else {
font.insert_glyph(glyph)?;
Expand Down
4 changes: 2 additions & 2 deletions crates/shift-backends/src/ufo/writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,10 +282,10 @@ impl UfoWriter {
let default_layer = norad_font.layers.default_layer_mut();

for glyph in font.glyphs() {
let Some(source_id) = default_source_id else {
let Some(ref source_id) = default_source_id else {
continue;
};
if let Some(layer_data) = glyph.layer_for_source(source_id) {
if let Some(layer_data) = glyph.layer_for_source(source_id.clone()) {
let norad_glyph = Self::convert_glyph(glyph, layer_data);
default_layer.insert_glyph(norad_glyph);
}
Expand Down
2 changes: 1 addition & 1 deletion crates/shift-backends/tests/round_trip/ufo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ fn preserves_components_anchors_layers_and_kerning() {
.layers()
.iter()
.max_by_key(|(_, layer)| layer.contours().len())
.map(|(layer_id, _)| *layer_id)
.map(|(layer_id, _)| layer_id.clone())
.expect("E should have a main layer");
original
.layer_mut(e_layer_id)
Expand Down
8 changes: 4 additions & 4 deletions crates/shift-bridge/__test__/index.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -132,15 +132,15 @@ describe("Bridge", () => {
expect(Array.from(change.values)).toEqual([500, 10, 20]);
});

it("applies point positions through the sparse typed-array hot path", () => {
it("applies point positions through the sparse bridge patch path", () => {
const glyphRef = createDefaultLayer();
const contourId = bridge.addContour(glyphRef).changed.contourIds[0];
const pointId = bridge.addPoint(glyphRef, contourId, 10, 20, "onCurve", false).changed
.pointIds[0];

bridge.applyPositionPatch(
glyphRef,
new BigUint64Array([BigInt(pointId)]),
[pointId],
new Float64Array([30, 40]),
null,
null,
Expand Down Expand Up @@ -181,11 +181,11 @@ describe("Bridge", () => {
expect(() =>
bridge.applyPositionPatch(
glyphRef,
new BigUint64Array([1n]),
["not-a-point"],
new Float64Array([10]),
null,
null,
),
).toThrow(/point positions/i);
).toThrow(/point ID/i);
});
});
4 changes: 2 additions & 2 deletions crates/shift-bridge/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ export declare class Bridge {
removePoints(glyphRef: GlyphLayerRef, pointIds: Array<PointId>): NapiGlyphStructureChange
toggleSmooth(glyphRef: GlyphLayerRef, pointId: PointId): NapiGlyphStructureChange
/**
* Bulk position sync. IDs use BigUint64Array to avoid lossy float packing.
* Bulk position sync. IDs are stable typed strings from the current glyph state.
* Coords are interleaved [x0, y0, x1, y1, ...].
*/
applyPositionPatch(glyphRef: GlyphLayerRef, pointIds?: BigUint64Array | undefined | null, pointCoords?: Float64Array | undefined | null, anchorIds?: BigUint64Array | undefined | null, anchorCoords?: Float64Array | undefined | null): void
applyPositionPatch(glyphRef: GlyphLayerRef, pointIds?: Array<PointId> | null, pointCoords?: Float64Array | undefined | null, anchorIds?: Array<AnchorId> | null, anchorCoords?: Float64Array | undefined | null): void
restoreState(glyphRef: GlyphLayerRef, structure: NapiGlyphStructure, values: Float64Array): NapiGlyphStructureChange
}

Expand Down
59 changes: 35 additions & 24 deletions crates/shift-bridge/src/bridge.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::errors::{self, BridgeError, BridgeResult};
use crate::input::parse;
use crate::input::{parse, BridgeParse};
use napi::bindgen_prelude::*;
use napi::{Error, Status};
use napi_derive::napi;
Expand Down Expand Up @@ -788,7 +788,9 @@ impl Bridge {
) -> errors::Result<NapiGlyphStructureChange> {
let contour_id = parse::<ContourId>(&contour_id)?;
let layer_id = self.existing_layer_id(glyph_ref)?;
let layer = self.workspace_mut()?.open_contour(layer_id, contour_id)?;
let layer = self
.workspace_mut()?
.open_contour(layer_id, contour_id.clone())?;
let changed = GlyphChangedEntities {
contour_ids: vec![contour_id],
..Default::default()
Expand All @@ -807,7 +809,9 @@ impl Bridge {
) -> errors::Result<NapiGlyphStructureChange> {
let contour_id = parse::<ContourId>(&contour_id)?;
let layer_id = self.existing_layer_id(glyph_ref)?;
let layer = self.workspace_mut()?.close_contour(layer_id, contour_id)?;
let layer = self
.workspace_mut()?
.close_contour(layer_id, contour_id.clone())?;
let changed = GlyphChangedEntities {
contour_ids: vec![contour_id],
..Default::default()
Expand All @@ -828,7 +832,7 @@ impl Bridge {
let layer_id = self.existing_layer_id(glyph_ref)?;
let layer = self
.workspace_mut()?
.reverse_contour(layer_id, contour_id)?;
.reverse_contour(layer_id, contour_id.clone())?;
let changed = GlyphChangedEntities {
contour_ids: vec![contour_id],
..Default::default()
Expand Down Expand Up @@ -904,7 +908,9 @@ impl Bridge {
) -> errors::Result<NapiGlyphStructureChange> {
let parsed_id = parse::<PointId>(&point_id)?;
let layer_id = self.existing_layer_id(glyph_ref)?;
let layer = self.workspace_mut()?.toggle_smooth(layer_id, parsed_id)?;
let layer = self
.workspace_mut()?
.toggle_smooth(layer_id, parsed_id.clone())?;
let changed = GlyphChangedEntities {
point_ids: vec![parsed_id],
..Default::default()
Expand All @@ -915,35 +921,31 @@ impl Bridge {
Ok(change.into())
}

/// Bulk position sync. IDs use BigUint64Array to avoid lossy float packing.
/// Bulk position sync. IDs are stable typed strings from the current glyph state.
/// Coords are interleaved [x0, y0, x1, y1, ...].
#[napi]
pub fn apply_position_patch(
&mut self,
glyph_ref: GlyphLayerRef,
point_ids: Option<BigUint64Array>,
#[napi(ts_arg_type = "Array<PointId> | null")] point_ids: Option<Vec<String>>,
point_coords: Option<Float64Array>,
anchor_ids: Option<BigUint64Array>,
#[napi(ts_arg_type = "Array<AnchorId> | null")] anchor_ids: Option<Vec<String>>,
anchor_coords: Option<Float64Array>,
) -> errors::Result<()> {
let point_position_changes = read_point_position_changes(&point_ids, &point_coords)?;
let point_ids = parse_ids::<PointId>(point_ids.as_deref())?;
let anchor_ids = parse_ids::<shift_font::AnchorId>(anchor_ids.as_deref())?;
let point_position_changes = read_point_position_changes(point_ids.as_deref(), &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());
let layer_id = self.existing_layer_id(glyph_ref)?;
let point_id_slice = point_ids.as_ref().map(|ids| {
let ids: &[u64] = ids;
ids
});
let point_id_slice = point_ids.as_deref();
let point_coord_slice = point_coords.as_ref().map(|coords| {
let coords: &[f64] = coords;
coords
});
let anchor_id_slice = anchor_ids.as_ref().map(|ids| {
let ids: &[u64] = ids;
ids
});
let anchor_id_slice = anchor_ids.as_deref();
let anchor_coord_slice = anchor_coords.as_ref().map(|coords| {
let coords: &[f64] = coords;
coords
Expand Down Expand Up @@ -977,27 +979,36 @@ impl Bridge {
let layer_id = self.existing_layer_id(glyph_ref)?;
let mut layer = self
.font()?
.layer(layer_id)
.ok_or(shift_font::error::CoreError::LayerNotFound(layer_id))?
.layer(layer_id.clone())
.ok_or(shift_font::error::CoreError::LayerNotFound(
layer_id.clone(),
))?
.clone();
apply_state_to_layer(&mut layer, &structure, values)?;
let layer = self.workspace_mut()?.replace_layer(layer_id, layer)?;
let layer = self
.workspace_mut()?
.replace_layer(layer_id.clone(), layer)?;
let change = GlyphStructureChange::from_layer(&layer, Default::default());

self.mark_font_changed();
Ok(change.into())
}
}

fn parse_ids<T: BridgeParse>(ids: Option<&[String]>) -> BridgeResult<Option<Vec<T>>> {
ids
.map(|ids| ids.iter().map(|id| parse::<T>(id)).collect())
.transpose()
}

fn read_point_position_changes(
point_ids: &Option<BigUint64Array>,
point_ids: Option<&[PointId]>,
point_coords: &Option<Float64Array>,
) -> BridgeResult<Vec<shift_font::PointPosition>> {
let Some(point_ids) = point_ids.as_ref() else {
let Some(point_ids) = point_ids else {
return Ok(Vec::new());
};

let point_ids: &[u64] = point_ids;
let point_coords = point_coords
.as_ref()
.ok_or_else(|| BridgeError::InvalidInput {
Expand All @@ -1021,7 +1032,7 @@ fn read_point_position_changes(
.iter()
.enumerate()
.map(|(index, point_id)| shift_font::PointPosition {
point_id: PointId::from_raw(*point_id as u128),
point_id: point_id.clone(),
x: point_coords[index * 2],
y: point_coords[index * 2 + 1],
})
Expand Down
1 change: 1 addition & 0 deletions crates/shift-font/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ kurbo = "0.13.0"
linesweeper = "0.3.0"
serde = { version = "1.0", features = ["derive", "rc"] }
thiserror = "2.0.18"
uuid = { version = "1.17.0", features = ["v4"] }

[dev-dependencies]
serde_json = "1.0"
Loading
Loading