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
39 changes: 6 additions & 33 deletions Cargo.lock

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

2 changes: 0 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ members = ["crates/*"]
shift-wire = { path = "crates/shift-wire" }
shift-backends = { path = "crates/shift-backends" }
shift-bridge = { path = "crates/shift-bridge" }
shift-edit = { path = "crates/shift-edit" }
shift-font = { path = "crates/shift-font" }
shift-ir = { path = "crates/shift-ir" }
shift-document = { path = "crates/shift-document" }
shift-store = { path = "crates/shift-store" }
2 changes: 1 addition & 1 deletion Claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ This project uses **pnpm** (v9.0.0) as its package manager.
## Project Structure

- `apps/desktop/src/` - Electron app (main, preload, renderer, shared)
- `crates/` - Rust workspace (shift-core, shift-backends, shift-ir, shift-node)
- `crates/` - Rust workspace (shift-font, shift-backends, shift-bridge, shift-store)
- `packages/` - TypeScript packages (types, geo, font, ui)

## Code Organization Rules
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ These are allowed to jump around when energy is high, but they should not silent

**Edit Operations**

- [x] EditSession ownership model for glyph editing
- [x] Glyph layer mutation ownership model for glyph editing
- [x] Add/remove points
- [x] Move points (single and batch)
- [x] Add/remove contours
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/main/managers/AppLifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ export class AppLifecycle {
if (!shouldOpen) return;
}

// OK this is the problem, we are sending an open file event to the renderer to open the file from the bridge
ipc.send(window.webContents, "external:open-font", filePath);
}

Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/renderer/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ export const App = () => {
}

const unsubscribeOpen = window.electronAPI?.onMenuOpenFont(handleOpenFont);

// TODO: have all this annoying logic for when HMR happens to re-open the already open font
// this should be handled more gracefully.
const unsubscribeExternalOpen = window.electronAPI?.onExternalOpenFont(handleOpenFont);
const unsubscribeNew = window.electronAPI?.onDocumentNew(() => {
fontDocument.createFont();
Expand Down Expand Up @@ -106,6 +109,7 @@ export const App = () => {
dumpSelectionPatternsToConsole();
});

// TODO: I image these can get stale (or maybe not, they don't capture anything)
return () => {
if (unsubscribeNew) unsubscribeNew();
if (unsubscribeOpen) unsubscribeOpen();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ describe("Clipboard (via Editor)", () => {
await editor.paste();
await editor.paste();

const contours = (editor.currentGlyph?.contours ?? []).filter((contour) => !contour.isEmpty);
const contours = (editor.activeGlyphSource?.contours ?? []).filter(
(contour) => !contour.isEmpty,
);
expect(contours).toHaveLength(3);

// Each paste translates the original by DEFAULT_PASTE_OFFSET (20) *
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ function editableSource(): GlyphSource {

const handle = { name: "A", unicode: 65 };
const source = font.defaultSource;
bridge.startEditSession(handle, source.id);

const glyphSource = font.glyphSource(handle, source);
if (!glyphSource) throw new Error("Expected editable glyph source");
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/renderer/src/lib/commands/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export function commandSourceFixture(): CommandSourceFixture {

const handle = { name: "A", unicode: 65 };
const source = font.defaultSource;
bridge.startEditSession(handle, source.id);

const glyphSource = font.glyphSource(handle, source);
if (!glyphSource) throw new Error("Expected editable glyph source");
Expand Down
39 changes: 17 additions & 22 deletions apps/desktop/src/renderer/src/lib/editor/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ export class Editor {
* This is a read/focus API. It chooses the current active source context for
* camera metrics, asks `Font` for existing glyph state, and updates
* `editingGlyph` when the glyph can be loaded. It does not create missing
* glyph data and does not start an edit session.
* glyph data and does not select an editable source.
*
* @returns The focused glyph model, or `null` when the glyph has no readable state.
*/
Expand Down Expand Up @@ -541,10 +541,6 @@ export class Editor {
const source = this.font.source(sourceId);
if (!source) return null;

if (this.#bridge.hasEditSession()) {
this.#bridge.endEditSession();
}
this.#bridge.startEditSession(handle, source.id);
this.#glyph.edit.selectSource(source.id);

const glyph = this.getGlyph(handle);
Expand Down Expand Up @@ -593,7 +589,7 @@ export class Editor {
* TextRuns.resolveAnchor(anchor)
* │
* ▼
* native edit session + drawOffset = focused.editOrigin
* source glyph geometry + drawOffset = focused.editOrigin
*/
public setGlyphFocus(anchor: GlyphAnchor): void {
const focused = this.#textRuns.resolveAnchor(anchor);
Expand Down Expand Up @@ -625,11 +621,8 @@ export class Editor {
return this.#text.glyphPlacement.peek();
}

/** Ends the current editing session. */
/** Clears the current glyph focus and active contour selection. */
public close(): void {
if (this.#bridge.hasEditSession()) {
this.#bridge.endEditSession();
}
this.#glyph.open.glyph.set(null);
this.#glyph.open.activeContourId.set(null);
}
Expand Down Expand Up @@ -663,6 +656,20 @@ export class Editor {
return this.#glyph.edit.source.peek();
}

/**
* Returns the authored glyph source currently targeted by edit commands.
*
* @remarks
* This is the source-backed edit target, not the interpolated preview
* geometry shown at the current design location. Clipboard, command, and test
* code should use this when reading or mutating authored point data.
*
* @returns null when no glyph source is open for editing.
*/
public get activeGlyphSource(): GlyphSource | null {
return this.#glyph.edit.glyphSource.peek();
}

/** Glyph instance resolved at the current design location. */
public get glyphInstance(): GlyphInstance | null {
return this.#glyph.preview.instance.peek();
Expand Down Expand Up @@ -1025,10 +1032,6 @@ export class Editor {
* default design location.
*/
public createFont(): void {
if (this.#bridge.hasEditSession()) {
this.close();
}

this.font.create();
this.setDesignLocation(this.font.defaultLocation());
this.#events.emit("fontLoaded", { font: this.font });
Expand All @@ -1039,20 +1042,12 @@ export class Editor {
* location.
*/
public loadFont(filePath: string): void {
if (this.#bridge.hasEditSession()) {
this.close();
}

this.font.load(filePath);
this.setDesignLocation(this.font.defaultLocation());
this.#events.emit("fontLoaded", { font: this.font });
}

public closeFont(): void {
if (this.#bridge.hasEditSession()) {
this.close();
}

this.font.close();
this.setDesignLocation(emptyAxisLocation());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ function editableSource(): GlyphSource {

const handle = { name: "A", unicode: 65 };
const source = font.defaultSource;
bridge.startEditSession(handle, source.id);

const glyphSource = font.glyphSource(handle, source);
if (!glyphSource) throw new Error("Expected editable glyph source");
Expand Down
12 changes: 6 additions & 6 deletions apps/desktop/src/renderer/src/lib/editor/variation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ function flattenPoints(g: Glyph | GlyphGeometry): number[] {
return out;
}

describe("Editor.open — variation-aware edit sessions", () => {
describe("Editor.open — variation-aware glyph reads", () => {
it("opens a glyph with values interpolated at the current variation location", () => {
// Regression for 1c2c575: opening a glyph from the grid used to start an
// edit session at the master's stored coordinates, so the canvas didn't
// Regression for 1c2c575: opening a glyph from the grid used to read the
// master's stored coordinates, so the canvas didn't
// match what the user clicked when the slider was off-default.
const editor = new TestEditor();
editor.loadFont(MUTATORSANS_DESIGNSPACE);
Expand Down Expand Up @@ -63,9 +63,9 @@ describe("Editor.open — variation-aware edit sessions", () => {
expect(samePoint?.x).toBe(movedX);
});

it("edits to a glyph are visible from the registry after closing the session", () => {
// The grid renders via `font.glyph(name)` (not the editor)so after a
// session ends, the registry's Glyph must reflect the edits the user
it("edits to a glyph are visible from the registry after closing the glyph", () => {
// The grid renders via `font.glyph(name)` (not the editor), so after the
// glyph closes, the registry's Glyph must reflect the edits the user
// just made. Otherwise the grid shows the pre-edit outline.
const editor = new TestEditor();
editor.loadFont(MUTATORSANS_DESIGNSPACE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ interface Fixture {
}

function loadFixture(): Fixture {
// Generated by `cargo test -p shift-edit --test interpolation_parity`.
// Generated from the Rust interpolation parity fixture.
// vitest runs with cwd at apps/desktop; repo root is two up.
const path = resolve(process.cwd(), "../../packages/types/__fixtures__/variation_parity.json");
return JSON.parse(readFileSync(path, "utf-8")) as Fixture;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* `scalar_at_with_args` and `interpolate_from_deltas`.
*
* Parity-tested against `packages/types/__fixtures__/variation_parity.json`,
* generated by `crates/shift-edit/tests/interpolation_parity.rs`.
* generated by the Rust interpolation parity fixture.
*/

import type { Axis, AxisTent, GlyphVariationData } from "@shift/types";
Expand All @@ -16,7 +16,7 @@ import type { AxisLocation } from "@/types/variation";

export type NormalizedLocation = Record<string, number>;

/** Asymmetric normalize — mirrors `Axis::normalize` in shift-ir/src/axis.rs. */
/** Asymmetric normalize — mirrors `Axis::normalize` in `shift-font`. */
export function normalizeAxis(value: number, axis: Axis): number {
if (value < axis.default) {
const range = axis.default - axis.minimum;
Expand Down Expand Up @@ -76,7 +76,7 @@ export function scalarAt(loc: NormalizedLocation, region: AxisTent[]): number {
* Output values are absolute (not offsets from default) because the default
* master's region has empty/zero support and contributes its full delta at scalar=1.
*
* Order is the `flatten()` shape from shift-edit::interpolation:
* Order is the Rust interpolation `flatten()` shape:
* [xAdvance, p0.x, p0.y, p1.x, p1.y, ..., a0.x, a0.y, ...]
*/
export function interpolate(data: GlyphVariationData, loc: NormalizedLocation): Float64Array {
Expand Down
34 changes: 18 additions & 16 deletions apps/desktop/src/renderer/src/lib/model/Font.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,8 +422,8 @@ export class Font {
* @param fromName - Existing committed glyph name.
* @param name - New unique glyph name after trimming whitespace.
* @param unicodes - Complete Unicode assignment for the renamed glyph.
* @throws {Error} when `fromName` is missing, `name` is empty, `name` already
* exists, or an edit session is active.
* @throws {Error} when `fromName` is missing, `name` is empty, or `name`
* already exists.
*/
updateGlyphIdentity(fromName: GlyphName, name: GlyphName, unicodes: readonly Unicode[]): void {
this.#bridge.updateGlyphIdentity(fromName, name.trim() as GlyphName, [...unicodes]);
Expand All @@ -437,23 +437,25 @@ export class Font {
*
* @remarks
* If the requested name already exists, a numeric suffix is appended using
* {@link nextAvailableGlyphName}. The bridge edit session is opened and
* immediately ended so downstream save/export paths see a real committed
* glyph record, not a UI-only placeholder.
* {@link nextAvailableGlyphName}. The bridge receives an explicit glyph layer
* mutation so downstream save/export paths see a real committed glyph record,
* not a UI-only placeholder.
*
* @param name - Preferred glyph name. Blank input falls back to `newGlyph`.
* @returns The handle for the glyph that was actually created.
* @throws {Error} when the bridge rejects session creation or commit.
* @throws {Error} when the bridge rejects glyph creation.
*/
createGlyph(name: GlyphName): GlyphHandle {
const glyphName = this.nextAvailableGlyphName(name);
if (this.#bridge.hasEditSession()) {
this.#bridge.endEditSession();
}

const handle = this.glyphHandleForName(glyphName);
this.#bridge.startEditSession(handle, this.defaultSource.id);
this.#bridge.endEditSession();
this.#bridge.setXAdvance(
{
glyphHandle: handle,
layerId: this.defaultSource.layerId,
},
500,
);

this.#glyphs.clear();
this.#glyphSources.clear();
Expand Down Expand Up @@ -501,9 +503,9 @@ export class Font {
/**
* Get the cached model for an existing glyph.
*
* This is a read/data access API. It asks the bridge for committed or active
* glyph state at the font's default source and returns `null` when no state
* exists. It does not create missing glyphs and does not start an edit session.
* This is a read/data access API. It asks the bridge for glyph state at the
* font's default source and returns `null` when no state exists. It does not
* create missing glyphs or select an editable source.
*
* @example
* ```ts
Expand Down Expand Up @@ -577,8 +579,8 @@ export class Font {
* Read raw glyph state for a source from the bridge.
*
* This is the lowest-level glyph data read used by the domain model. The
* bridge may return active edit-session state, committed font state, or
* `null` when the glyph has no data for the source.
* bridge may return native glyph-layer state or `null` when the glyph has no
* data for the source.
*
* @returns Raw glyph state, or `null` when the bridge cannot provide state.
*/
Expand Down
Loading
Loading