From 47a25fee75b2cc3b07f68af68f309d3c8909d693 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Thu, 12 Dec 2024 15:07:59 +0000 Subject: [PATCH 01/14] Abstract /api/ calls behind common Backend API --- src/fontra/client/core/backend-api.js | 133 ++++++++++++++++++ src/fontra/client/core/glyph-lines.js | 8 +- src/fontra/client/core/server-utils.js | 47 ------- src/fontra/filesystem/landing.js | 5 +- src/fontra/views/editor/editor.js | 9 +- .../views/editor/panel-related-glyphs.js | 9 +- .../views/editor/panel-transformation.js | 15 +- 7 files changed, 149 insertions(+), 77 deletions(-) create mode 100644 src/fontra/client/core/backend-api.js delete mode 100644 src/fontra/client/core/server-utils.js diff --git a/src/fontra/client/core/backend-api.js b/src/fontra/client/core/backend-api.js new file mode 100644 index 0000000000..a8df79c20a --- /dev/null +++ b/src/fontra/client/core/backend-api.js @@ -0,0 +1,133 @@ +import { fetchJSON } from "./utils.js"; +import { StaticGlyph } from "./var-glyph.js"; + +/** + * @module fontra/client/core/backend-api + * @description + * This module provides a class that can be used to interact with the backend API. + * The default Fontra backend is the Python-based web server. This class provides + * an abstraction over the functionality of the web server, so that alternative + * backends can be used. + * + * @typedef {import('./var-path.js').VarPackedPath} VarPackedPath + */ +class AbstractBackend { + /** + * Get a list of projects from the backend. + * @returns {Promise} An array of project names. + */ + static async getProjects() {} + + /** + * Get a suggested glyph name for a given code point. + * @param {number} codePoint - The code point. + * @returns {Promise} The suggested glyph name. + */ + static async getSuggestedGlyphName(codePoint) {} + + /** + * Get the code point for a given glyph name. + * @param {string} glyphName - The glyph name. + * @returns {Promise} The code point. + */ + static async getCodePointFromGlyphName(glyphName) {} + + /** + * Parse clipboard data. + * + * Returns a glyph object parsed from either a SVG string or an UFO .glif. + * @param {string} data - The clipboard data. + * @returns {Promise} - The glyph object, if parsable. + */ + static async parseClipboard(data) {} + + /** + * Remove overlaps in a path + * + * In this and all following functions, the paths are represented as + * JSON VarPackedPath objects; i.e. they have `coordinates`, `pointTypes`, + * `contourInfo`, and `pointAttrbutes` fields. + * + * @param {VarPackedPath} path - The first path. + * @returns {Promise} The union of the two paths. + */ + static async unionPath(path) {} + + /** + * Subtract one path from another. + * @param {VarPackedPath} pathA - The first path. + * @param {VarPackedPath} pathB - The second path. + * @returns {Promise} The difference of the two paths. + */ + static async subtractPath(pathA, pathB) {} + + /** + * Intersect two paths. + * @param {VarPackedPath} pathA - The first path. + * @param {VarPackedPath} pathB - The second path. + * @returns {Promise} The intersection of the two paths. + */ + static async intersectPath(pathA, pathB) {} + + /** + * Exclude one path from another. + * @param {VarPackedPath} pathA - The first path. + * @param {VarPackedPath} pathB - The second path. + * @returns {Promise} The exclusion of the two paths. + */ + static async excludePath(pathA, pathB) {} +} + +class PythonBackend extends AbstractBackend { + static async getProjects() { + return fetchJSON("/projectlist"); + } + + static async _callServerAPI(functionName, kwargs) { + const response = await fetch(`/api/${functionName}`, { + method: "POST", + body: JSON.stringify(kwargs), + }); + + const result = await response.json(); + if (result.error) { + throw new Error(result.error); + } + return result.returnValue; + } + + static async getSuggestedGlyphName(codePoint) { + return await this._callServerAPI("getSuggestedGlyphName", { codePoint }); + } + + static async getCodePointFromGlyphName(glyphName) { + return await this._callServerAPI("getCodePointFromGlyphName", { glyphName }); + } + + static async parseClipboard(data) { + let result = await this._callServerAPI("parseClipboard", { data }); + return result ? StaticGlyph.fromObject(result) : undefined; + } + + static async unionPath(path) { + const newPath = await this._callServerAPI("unionPath", { path }); + return VarPackedPath.fromObject(newPath); + } + + static async subtractPath(pathA, pathB) { + const newPath = await this._callServerAPI("subtractPath", { pathA, pathB }); + return VarPackedPath.fromObject(newPath); + } + + static async intersectPath(pathA, pathB) { + const newPath = await this._callServerAPI("intersectPath", { pathA, pathB }); + return VarPackedPath.fromObject(newPath); + } + + static async excludePath(pathA, pathB) { + const newPath = await this._callServerAPI("excludePath", { pathA, pathB }); + return VarPackedPath.fromObject(newPath); + } +} + +export const Backend = PythonBackend; diff --git a/src/fontra/client/core/glyph-lines.js b/src/fontra/client/core/glyph-lines.js index bc43c9e526..f813c70426 100644 --- a/src/fontra/client/core/glyph-lines.js +++ b/src/fontra/client/core/glyph-lines.js @@ -1,4 +1,4 @@ -import { getCodePointFromGlyphName, getSuggestedGlyphName } from "./server-utils.js"; +import { Backend } from "./backend-api.js"; import { splitGlyphNameExtension } from "./utils.js"; export async function glyphLinesFromText(text, characterMap, glyphMap) { @@ -56,7 +56,7 @@ async function glyphNamesFromText(text, characterMap, glyphMap) { // a glyph name associated with that character let properBaseGlyphName = characterMap[baseCharCode]; if (!properBaseGlyphName) { - properBaseGlyphName = await getSuggestedGlyphName(baseCharCode); + properBaseGlyphName = await Backend.getSuggestedGlyphName(baseCharCode); } if (properBaseGlyphName) { glyphName = properBaseGlyphName + extension; @@ -67,7 +67,7 @@ async function glyphNamesFromText(text, characterMap, glyphMap) { } else { // This is a regular glyph name, but it doesn't exist in the font. // Try to see if there's a code point associated with it. - const codePoint = await getCodePointFromGlyphName(glyphName); + const codePoint = await Backend.getCodePointFromGlyphName(glyphName); if (codePoint) { char = String.fromCodePoint(codePoint); } @@ -85,7 +85,7 @@ async function glyphNamesFromText(text, characterMap, glyphMap) { if (glyphName !== "") { let isUndefined = false; if (!glyphName && char) { - glyphName = await getSuggestedGlyphName(char.codePointAt(0)); + glyphName = await Backend.getSuggestedGlyphName(char.codePointAt(0)); isUndefined = true; } else if (glyphName) { isUndefined = !(glyphName in glyphMap); diff --git a/src/fontra/client/core/server-utils.js b/src/fontra/client/core/server-utils.js deleted file mode 100644 index 92ff160d28..0000000000 --- a/src/fontra/client/core/server-utils.js +++ /dev/null @@ -1,47 +0,0 @@ -import { memoize } from "./utils.js"; -import { VarPackedPath } from "./var-path.js"; - -export async function callServerAPI(functionName, kwargs) { - const response = await fetch(`/api/${functionName}`, { - method: "POST", - body: JSON.stringify(kwargs), - }); - - const result = await response.json(); - if (result.error) { - throw new Error(result.error); - } - return result.returnValue; -} - -export const getSuggestedGlyphName = memoize(async (codePoint) => { - return await callServerAPI("getSuggestedGlyphName", { codePoint }); -}); - -export const getCodePointFromGlyphName = memoize(async (glyphName) => { - return await callServerAPI("getCodePointFromGlyphName", { glyphName }); -}); - -export async function parseClipboard(data) { - return await callServerAPI("parseClipboard", { data }); -} - -export async function unionPath(path) { - const newPath = await callServerAPI("unionPath", { path }); - return VarPackedPath.fromObject(newPath); -} - -export async function subtractPath(pathA, pathB) { - const newPath = await callServerAPI("subtractPath", { pathA, pathB }); - return VarPackedPath.fromObject(newPath); -} - -export async function intersectPath(pathA, pathB) { - const newPath = await callServerAPI("intersectPath", { pathA, pathB }); - return VarPackedPath.fromObject(newPath); -} - -export async function excludePath(pathA, pathB) { - const newPath = await callServerAPI("excludePath", { pathA, pathB }); - return VarPackedPath.fromObject(newPath); -} diff --git a/src/fontra/filesystem/landing.js b/src/fontra/filesystem/landing.js index dd93ee86c4..e4f8d5d8d3 100644 --- a/src/fontra/filesystem/landing.js +++ b/src/fontra/filesystem/landing.js @@ -1,6 +1,5 @@ +import { Backend } from "/core/backend-api.js"; import { loaderSpinner } from "/core/loader-spinner.js"; -import { getRemoteProxy } from "/core/remote.js"; -import { fetchJSON } from "/core/utils.js"; export async function startupLandingPage(authenticateFunc) { if (authenticateFunc) { @@ -8,7 +7,7 @@ export async function startupLandingPage(authenticateFunc) { return; } } - const projectList = await loaderSpinner(fetchJSON("/projectlist")); + const projectList = await loaderSpinner(Backend.getProjects()); const projectListContainer = document.querySelector("#project-list"); projectListContainer.classList.remove("hidden"); diff --git a/src/fontra/views/editor/editor.js b/src/fontra/views/editor/editor.js index 41d9bd6125..cf0879ce4f 100644 --- a/src/fontra/views/editor/editor.js +++ b/src/fontra/views/editor/editor.js @@ -4,6 +4,7 @@ import { getActionIdentifierFromKeyEvent, registerAction, } from "../core/actions.js"; +import { Backend } from "../core/backend-api.js"; import { CanvasController } from "../core/canvas-controller.js"; import { recordChanges } from "../core/change-recorder.js"; import { applyChange } from "../core/changes.js"; @@ -28,7 +29,6 @@ import { } from "../core/rectangle.js"; import { getRemoteProxy } from "../core/remote.js"; import { SceneView } from "../core/scene-view.js"; -import { parseClipboard } from "../core/server-utils.js"; import { isSuperset } from "../core/set-ops.js"; import { labeledCheckbox, labeledTextInput, pickFile } from "../core/ui-utils.js"; import { @@ -2203,7 +2203,7 @@ export class EditorController { console.log("couldn't paste from JSON:", error.toString()); } } else { - const glyph = await this.parseClipboard(plainText); + const glyph = await Backend.parseClipboard(plainText); if (glyph) { pasteLayerGlyphs = [{ glyph }]; } @@ -2366,11 +2366,6 @@ export class EditorController { ); } - async parseClipboard(data) { - const result = await parseClipboard(data); - return result ? StaticGlyph.fromObject(result) : undefined; - } - canDelete() { if (this.fontController.readOnly || this.sceneModel.isSelectedGlyphLocked()) { return false; diff --git a/src/fontra/views/editor/panel-related-glyphs.js b/src/fontra/views/editor/panel-related-glyphs.js index 3f3871085b..76101c78ca 100644 --- a/src/fontra/views/editor/panel-related-glyphs.js +++ b/src/fontra/views/editor/panel-related-glyphs.js @@ -1,10 +1,7 @@ import Panel from "./panel.js"; +import { Backend } from "/core/backend-api.js"; import * as html from "/core/html-utils.js"; import { translate } from "/core/localization.js"; -import { - getCodePointFromGlyphName, - getSuggestedGlyphName, -} from "/core/server-utils.js"; import { unicodeMadeOf, unicodeUsedBy } from "/core/unicode-utils.js"; import { getCharFromCodePoint, throttleCalls } from "/core/utils.js"; @@ -128,7 +125,7 @@ export default class RelatedGlyphPanel extends Panel { const character = glyphName ? getCharFromCodePoint( this.fontController.codePointForGlyph(glyphName) || - (await getCodePointFromGlyphName(glyphName)) + (await Backend.getCodePointFromGlyphName(glyphName)) ) || "" : ""; const codePoint = character ? character.codePointAt(0) : undefined; @@ -343,7 +340,7 @@ async function _getRelatedUnicode( for (const codePoint of usedByCodePoints) { const glyphName = fontController.characterMap[codePoint] || - (await getSuggestedGlyphName(codePoint)); + (await Backend.getSuggestedGlyphName(codePoint)); glyphInfo.push({ glyphName, codePoints: [codePoint] }); } return glyphInfo; diff --git a/src/fontra/views/editor/panel-transformation.js b/src/fontra/views/editor/panel-transformation.js index d71c976506..29957a4efd 100644 --- a/src/fontra/views/editor/panel-transformation.js +++ b/src/fontra/views/editor/panel-transformation.js @@ -1,4 +1,5 @@ import { registerAction } from "../core/actions.js"; +import { Backend } from "../core/backend-api.js"; import { ChangeCollector, applyChange, consolidateChanges } from "../core/changes.js"; import { EditBehaviorFactory } from "./edit-behavior.js"; import Panel from "./panel.js"; @@ -9,12 +10,6 @@ import { getSelectionByContour, } from "/core/path-functions.js"; import { rectCenter, rectSize } from "/core/rectangle.js"; -import { - excludePath, - intersectPath, - subtractPath, - unionPath, -} from "/core/server-utils.js"; import { Transform } from "/core/transform.js"; import { enumerate, @@ -127,10 +122,10 @@ export default class TransformationPanel extends Panel { } const pathActions = [ - ["union", unionPath], - ["subtract", subtractPath], - ["intersect", intersectPath], - ["exclude", excludePath], + ["union", Backend.unionPath], + ["subtract", Backend.subtractPath], + ["intersect", Backend.intersectPath], + ["exclude", Backend.excludePath], ]; for (const [keyPart, pathOperationFunc] of pathActions) { registerAction( From 94973f80b84b50f47ef4e220acb195e6f15c9a5b Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Fri, 13 Dec 2024 14:42:22 +0000 Subject: [PATCH 02/14] Editor and FontInfo views share a lot of commonality, extract it --- src/fontra/client/core/view.js | 57 +++++++++++++++++++++++++++ src/fontra/views/editor/editor.js | 30 ++------------ src/fontra/views/fontinfo/fontinfo.js | 46 ++------------------- 3 files changed, 64 insertions(+), 69 deletions(-) create mode 100644 src/fontra/client/core/view.js diff --git a/src/fontra/client/core/view.js b/src/fontra/client/core/view.js new file mode 100644 index 0000000000..18995773ea --- /dev/null +++ b/src/fontra/client/core/view.js @@ -0,0 +1,57 @@ +import { FontController } from "../core/font-controller.js"; +import { getRemoteProxy } from "../core/remote.js"; +import { makeDisplayPath } from "../core/view-utils.js"; +import { ensureLanguageHasLoaded } from "/core/localization.js"; +import { message } from "/web-components/modal-dialog.js"; + +export class ViewController { + static titlePattern(displayPath) { + return `Fontra — ${decodeURI(displayPath)}`; + } + static async fromWebSocket() { + const pathItems = window.location.pathname.split("/").slice(3); + const displayPath = makeDisplayPath(pathItems); + document.title = this.titlePattern(displayPath); + const projectPath = pathItems.join("/"); + const protocol = window.location.protocol === "http:" ? "ws" : "wss"; + const wsURL = `${protocol}://${window.location.host}/websocket/${projectPath}`; + + await ensureLanguageHasLoaded; + + const remoteFontEngine = await getRemoteProxy(wsURL); + const controller = new this(remoteFontEngine); + remoteFontEngine.receiver = controller; + remoteFontEngine.onclose = (event) => controller.handleRemoteClose(event); + remoteFontEngine.onerror = (event) => controller.handleRemoteError(event); + await controller.start(); + return controller; + } + + constructor(font) { + this.fontController = new FontController(font); + } + + async start() { + console.error("ViewController.start() not implemented"); + } + + async externalChange(change, isLiveChange) { + await this.fontController.applyChange(change, true); + this.fontController.notifyChangeListeners(change, isLiveChange, true); + } + + async reloadData(reloadPattern) {} + + async messageFromServer(headline, msg) { + // don't await the dialog result, the server doesn't need an answer + message(headline, msg); + } + + handleRemoteClose(event) { + // + } + + handleRemoteError(event) { + // + } +} diff --git a/src/fontra/views/editor/editor.js b/src/fontra/views/editor/editor.js index cf0879ce4f..5b1b930bda 100644 --- a/src/fontra/views/editor/editor.js +++ b/src/fontra/views/editor/editor.js @@ -27,7 +27,6 @@ import { rectSize, rectToArray, } from "../core/rectangle.js"; -import { getRemoteProxy } from "../core/remote.js"; import { SceneView } from "../core/scene-view.js"; import { isSuperset } from "../core/set-ops.js"; import { labeledCheckbox, labeledTextInput, pickFile } from "../core/ui-utils.js"; @@ -93,6 +92,7 @@ import { translate, translatePlural, } from "/core/localization.js"; +import { ViewController } from "/core/view.js"; const MIN_CANVAS_SPACE = 200; @@ -101,28 +101,9 @@ const PASTE_BEHAVIOR_ADD = "add"; const EXPORT_FORMATS = ["ttf", "otf", "fontra", "designspace", "ufo", "rcjk"]; -export class EditorController { - static async fromWebSocket() { - const pathItems = window.location.pathname.split("/").slice(3); - const displayPath = makeDisplayPath(pathItems); - document.title = `Fontra — ${decodeURI(displayPath)}`; - const projectPath = pathItems.join("/"); - const protocol = window.location.protocol === "http:" ? "ws" : "wss"; - const wsURL = `${protocol}://${window.location.host}/websocket/${projectPath}`; - - await ensureLanguageHasLoaded; - - const remoteFontEngine = await getRemoteProxy(wsURL); - const editorController = new EditorController(remoteFontEngine); - remoteFontEngine.receiver = editorController; - remoteFontEngine.onclose = (event) => editorController.handleRemoteClose(event); - remoteFontEngine.onerror = (event) => editorController.handleRemoteError(event); - - await editorController.start(); - return editorController; - } - +export class EditorController extends ViewController { constructor(font) { + super(font); const canvas = document.querySelector("#edit-canvas"); canvas.focus(); @@ -3378,11 +3359,6 @@ export class EditorController { this.canvasController.requestUpdate(); } - async messageFromServer(headline, msg) { - // don't await the dialog result, the server doesn't need an answer - message(headline, msg); - } - async setupFromWindowLocation() { this.sceneSettingsController.withSenderInfo({ senderID: this }, () => this._setupFromWindowLocation() diff --git a/src/fontra/views/fontinfo/fontinfo.js b/src/fontra/views/fontinfo/fontinfo.js index e195b6ddf6..dcde40e37f 100644 --- a/src/fontra/views/fontinfo/fontinfo.js +++ b/src/fontra/views/fontinfo/fontinfo.js @@ -1,35 +1,15 @@ -import { FontController } from "../core/font-controller.js"; import * as html from "../core/html-utils.js"; -import { getRemoteProxy } from "../core/remote.js"; -import { makeDisplayPath } from "../core/view-utils.js"; import { AxesPanel } from "./panel-axes.js"; import { CrossAxisMappingPanel } from "./panel-cross-axis-mapping.js"; import { DevelopmentStatusDefinitionsPanel } from "./panel-development-status-definitions.js"; import { FontInfoPanel } from "./panel-font-info.js"; import { SourcesPanel } from "./panel-sources.js"; import { translate } from "/core/localization.js"; -import { message } from "/web-components/modal-dialog.js"; - -export class FontInfoController { - static async fromWebSocket() { - const pathItems = window.location.pathname.split("/").slice(3); - const displayPath = makeDisplayPath(pathItems); - document.title = `Fontra Font Info — ${decodeURI(displayPath)}`; - const projectPath = pathItems.join("/"); - const protocol = window.location.protocol === "http:" ? "ws" : "wss"; - const wsURL = `${protocol}://${window.location.host}/websocket/${projectPath}`; - - const remoteFontEngine = await getRemoteProxy(wsURL); - const fontInfoController = new FontInfoController(remoteFontEngine); - remoteFontEngine.receiver = fontInfoController; - remoteFontEngine.onclose = (event) => fontInfoController.handleRemoteClose(event); - remoteFontEngine.onerror = (event) => fontInfoController.handleRemoteError(event); - await fontInfoController.start(); - return fontInfoController; - } +import { ViewController } from "/core/view.js"; - constructor(font) { - this.fontController = new FontController(font); +export class FontInfoController extends ViewController { + static titlePattern(displayPath) { + return `Fontra Font Info — ${decodeURI(displayPath)}`; } async start() { @@ -107,11 +87,6 @@ export class FontInfoController { panel?.handleKeyDown?.(event); } - async externalChange(change, isLiveChange) { - await this.fontController.applyChange(change, true); - this.fontController.notifyChangeListeners(change, isLiveChange, true); - } - async reloadData(reloadPattern) { // We have currently no way to refine update behavior based on the // reloadPattern. @@ -119,19 +94,6 @@ export class FontInfoController { // reloadEverything() will trigger the appropriate listeners this.fontController.reloadEverything(); } - - async messageFromServer(headline, msg) { - // don't await the dialog result, the server doesn't need an answer - message(headline, msg); - } - - handleRemoteClose(event) { - // - } - - handleRemoteError(event) { - // - } } function setupIntersectionObserver(panelContainer, panels) { From 310d445d7806df2383880782ffac3fc1a3cff6a2 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Fri, 13 Dec 2024 15:59:30 +0000 Subject: [PATCH 03/14] Get remote font from backend, which may or may not be a websocket --- src/fontra/client/core/backend-api.js | 7 +++++++ src/fontra/client/core/view.js | 7 +++---- src/fontra/views/editor/editor.html | 2 +- src/fontra/views/fontinfo/fontinfo.html | 2 +- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/fontra/client/core/backend-api.js b/src/fontra/client/core/backend-api.js index a8df79c20a..b23561bbbd 100644 --- a/src/fontra/client/core/backend-api.js +++ b/src/fontra/client/core/backend-api.js @@ -1,3 +1,4 @@ +import { getRemoteProxy } from "../core/remote.js"; import { fetchJSON } from "./utils.js"; import { StaticGlyph } from "./var-glyph.js"; @@ -128,6 +129,12 @@ class PythonBackend extends AbstractBackend { const newPath = await this._callServerAPI("excludePath", { pathA, pathB }); return VarPackedPath.fromObject(newPath); } + + static async remoteFont(projectPath) { + const protocol = window.location.protocol === "http:" ? "ws" : "wss"; + const wsURL = `${protocol}://${window.location.host}/websocket/${projectPath}`; + return getRemoteProxy(wsURL); + } } export const Backend = PythonBackend; diff --git a/src/fontra/client/core/view.js b/src/fontra/client/core/view.js index 18995773ea..54be96fe43 100644 --- a/src/fontra/client/core/view.js +++ b/src/fontra/client/core/view.js @@ -1,6 +1,7 @@ import { FontController } from "../core/font-controller.js"; import { getRemoteProxy } from "../core/remote.js"; import { makeDisplayPath } from "../core/view-utils.js"; +import { Backend } from "./backend-api.js"; import { ensureLanguageHasLoaded } from "/core/localization.js"; import { message } from "/web-components/modal-dialog.js"; @@ -8,17 +9,15 @@ export class ViewController { static titlePattern(displayPath) { return `Fontra — ${decodeURI(displayPath)}`; } - static async fromWebSocket() { + static async fromBackend() { const pathItems = window.location.pathname.split("/").slice(3); const displayPath = makeDisplayPath(pathItems); document.title = this.titlePattern(displayPath); const projectPath = pathItems.join("/"); - const protocol = window.location.protocol === "http:" ? "ws" : "wss"; - const wsURL = `${protocol}://${window.location.host}/websocket/${projectPath}`; await ensureLanguageHasLoaded; - const remoteFontEngine = await getRemoteProxy(wsURL); + const remoteFontEngine = await Backend.remoteFont(projectPath); const controller = new this(remoteFontEngine); remoteFontEngine.receiver = controller; remoteFontEngine.onclose = (event) => controller.handleRemoteClose(event); diff --git a/src/fontra/views/editor/editor.html b/src/fontra/views/editor/editor.html index 60cd679c8c..431ec80dc1 100644 --- a/src/fontra/views/editor/editor.html +++ b/src/fontra/views/editor/editor.html @@ -106,7 +106,7 @@ import { EditorController } from "/editor/editor.js"; async function startApp() { - window.editorController = await EditorController.fromWebSocket(); + window.editorController = await EditorController.fromBackend(); } startApp(); diff --git a/src/fontra/views/fontinfo/fontinfo.html b/src/fontra/views/fontinfo/fontinfo.html index 44f226b99c..7a123f43a3 100644 --- a/src/fontra/views/fontinfo/fontinfo.html +++ b/src/fontra/views/fontinfo/fontinfo.html @@ -69,7 +69,7 @@ import { FontInfoController } from "/fontinfo/fontinfo.js"; async function startApp() { - window.fontInfoController = await FontInfoController.fromWebSocket(); + window.fontInfoController = await FontInfoController.fromBackend(); } startApp(); From 6991876d5ae3b1261cfc65805c040b970aca629b Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Mon, 16 Dec 2024 08:56:56 +0000 Subject: [PATCH 04/14] Slightly cleaner event interface --- src/fontra/client/core/remote.js | 10 ++++++++++ src/fontra/client/core/view.js | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/fontra/client/core/remote.js b/src/fontra/client/core/remote.js index 82e7d52435..eb007a5d24 100644 --- a/src/fontra/client/core/remote.js +++ b/src/fontra/client/core/remote.js @@ -53,6 +53,16 @@ export class RemoteObject { ); } + on(event, callback) { + if (event === "close") { + this.onclose = callback; + } else if (event === "error") { + this.onerror = callback; + } else { + throw new Error(`unknown event: ${event}`); + } + } + connect() { if (this._connectPromise !== undefined) { // websocket is still connecting/opening, return the same promise diff --git a/src/fontra/client/core/view.js b/src/fontra/client/core/view.js index 54be96fe43..4acba737e8 100644 --- a/src/fontra/client/core/view.js +++ b/src/fontra/client/core/view.js @@ -20,8 +20,8 @@ export class ViewController { const remoteFontEngine = await Backend.remoteFont(projectPath); const controller = new this(remoteFontEngine); remoteFontEngine.receiver = controller; - remoteFontEngine.onclose = (event) => controller.handleRemoteClose(event); - remoteFontEngine.onerror = (event) => controller.handleRemoteError(event); + remoteFontEngine.on("close", (event) => controller.handleRemoteClose(event)); + remoteFontEngine.on("error", (event) => controller.handleRemoteError(event)); await controller.start(); return controller; } From 525fee620ad0ea8acf073c53f2bc979cd75d52e7 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Mon, 16 Dec 2024 11:10:10 +0000 Subject: [PATCH 05/14] Enumerate all server->client messages over the websocket --- src/fontra/client/core/remote.js | 78 ++++++++++++++++---------------- src/fontra/client/core/view.js | 41 ++++++++++++++++- src/fontra/core/remote.py | 37 ++++++++------- 3 files changed, 99 insertions(+), 57 deletions(-) diff --git a/src/fontra/client/core/remote.js b/src/fontra/client/core/remote.js index eb007a5d24..3b0ed98f9c 100644 --- a/src/fontra/client/core/remote.js +++ b/src/fontra/client/core/remote.js @@ -35,6 +35,13 @@ export class RemoteObject { this.wsURL = wsURL; this._callReturnCallbacks = {}; + this._handlers = { + close: this._default_onclose, + error: this._default_onerror, + messageFromServer: undefined, + externalChange: undefined, + reloadData: undefined, + }; const g = _genNextClientCallID(); this._getNextClientCallID = () => { @@ -54,12 +61,18 @@ export class RemoteObject { } on(event, callback) { - if (event === "close") { - this.onclose = callback; - } else if (event === "error") { - this.onerror = callback; + if (this._handlers.hasOwnProperty(event)) { + this._handlers[event] = callback; + } else { + console.error(`Ignoring attempt to register handler for unknown event: ${event}`); + } + } + + async trigger(event, ...args) { + if (this._handlers.hasOwnProperty(event)) { + return await this._handlers[event](...args); } else { - throw new Error(`unknown event: ${event}`); + throw new Error(`Recieved unknown event from server: ${event}`); } } @@ -77,8 +90,8 @@ export class RemoteObject { this.websocket.onopen = (event) => { resolve(event); delete this._connectPromise; - this.websocket.onclose = (event) => this._onclose(event); - this.websocket.onerror = (event) => this._onerror(event); + this.websocket.onclose = (event) => this.trigger("close", event); + this.websocket.onerror = (event) => this.trigger("error", event); const message = { "client-uuid": this.clientUUID, }; @@ -89,20 +102,12 @@ export class RemoteObject { return this._connectPromise; } - _onclose(event) { - if (this.onclose) { - this.onclose(event); - } else { - console.log(`websocket closed`, event); - } + _default_onclose(event) { + console.log(`websocket closed`, event); } - _onerror(event) { - if (this.onerror) { - this.onerror(event); - } else { - console.log(`websocket error`, event); - } + _default_onerror(event) { + console.log(`websocket error`, event); } async _handleIncomingMessage(event) { @@ -123,28 +128,23 @@ export class RemoteObject { delete this._callReturnCallbacks[clientCallID]; } else if (serverCallID !== undefined) { // this is an incoming server -> client call - if (this.receiver) { - let returnMessage; - try { - let method = this.receiver[message["method-name"]]; - if (method === undefined) { - throw new Error(`undefined receiver method: ${message["method-name"]}`); - } - method = method.bind(this.receiver); - const returnValue = await method(...message["arguments"]); - returnMessage = { - "server-call-id": serverCallID, - "return-value": returnValue, - }; - } catch (error) { - console.log("exception in receiver call", error.toString()); - console.error(error, error.stack); - returnMessage = { "server-call-id": serverCallID, "error": error.toString() }; + let returnMessage; + try { + let method = message["method-name"]; + if (!this._handlers.hasOwnProperty(method)) { + throw new Error(`undefined method: ${method}`); } - this.websocket.send(JSON.stringify(returnMessage)); - } else { - console.log("no receiver in place to receive server messages", message); + const returnValue = await this.trigger(method, ...message["arguments"]); + returnMessage = { + "server-call-id": serverCallID, + "return-value": returnValue, + }; + } catch (error) { + console.log("exception in method call", error.toString()); + console.error(error, error.stack); + returnMessage = { "server-call-id": serverCallID, "error": error.toString() }; } + this.websocket.send(JSON.stringify(returnMessage)); } } diff --git a/src/fontra/client/core/view.js b/src/fontra/client/core/view.js index 4acba737e8..2569da0dcc 100644 --- a/src/fontra/client/core/view.js +++ b/src/fontra/client/core/view.js @@ -19,9 +19,18 @@ export class ViewController { const remoteFontEngine = await Backend.remoteFont(projectPath); const controller = new this(remoteFontEngine); - remoteFontEngine.receiver = controller; remoteFontEngine.on("close", (event) => controller.handleRemoteClose(event)); remoteFontEngine.on("error", (event) => controller.handleRemoteError(event)); + remoteFontEngine.on("messageFromServer", (headline, msg) => + controller.messageFromServer(headline, msg) + ); + remoteFontEngine.on("externalChange", (change, isLiveChange) => + controller.externalChange(change, isLiveChange) + ); + remoteFontEngine.on("reloadData", (reloadPattern) => + controller.reloadData(reloadPattern) + ); + await controller.start(); return controller; } @@ -34,13 +43,43 @@ export class ViewController { console.error("ViewController.start() not implemented"); } + /** + * The following methods are called by the remote object, on receipt of a + * method call from the backend. + */ + + /** + * Apply a change from the backend. + * + * Something happened to the current font outside of this controller, and we + * need to change ourselves in order to reflect that change. + * + * @param {*} change + * @param {*} isLiveChange + */ + async externalChange(change, isLiveChange) { await this.fontController.applyChange(change, true); this.fontController.notifyChangeListeners(change, isLiveChange, true); } + /** + * Reload some part of the font + * + * This is called when the backend tells us that something has changed, and + * we need to reload the font to reflect that change. + * + * @param {*} reloadPattern + */ async reloadData(reloadPattern) {} + /** + * + * Notify the user of a message from the server. + * + * @param {*} headline + * @param {*} msg + */ async messageFromServer(headline, msg) { // don't await the dialog result, the server doesn't need an answer message(headline, msg); diff --git a/src/fontra/core/remote.py b/src/fontra/core/remote.py index 62a278eb1d..95c5d901ab 100644 --- a/src/fontra/core/remote.py +++ b/src/fontra/core/remote.py @@ -120,28 +120,31 @@ async def _performCall(self, message: dict, subject: Any) -> None: async def sendMessage(self, message): await self.websocket.send_json(message) + async def callMethod(self, methodName, *args): + serverCallID = next(self.getNextServerCallID) + message = { + "server-call-id": serverCallID, + "method-name": methodName, + "arguments": args, + } + returnFuture = asyncio.get_running_loop().create_future() + self.callReturnFutures[serverCallID] = returnFuture + await self.sendMessage(message) + return await returnFuture + class RemoteClientProxy: def __init__(self, connection): self._connection = connection - def __getattr__(self, methodName): - if methodName.startswith("_"): - return super().__getattr__(methodName) - - async def methodWrapper(*args): - serverCallID = next(self._connection.getNextServerCallID) - message = { - "server-call-id": serverCallID, - "method-name": methodName, - "arguments": args, - } - returnFuture = asyncio.get_running_loop().create_future() - self._connection.callReturnFutures[serverCallID] = returnFuture - await self._connection.sendMessage(message) - return await returnFuture - - return methodWrapper + async def messageFromServer(self, text): + return await self._connection.callMethod("messageFromServer", text) + + async def externalChange(self, change, isLiveChange): + return await self._connection.callMethod("externalChange", change, isLiveChange) + + async def reloadData(self, reloadPattern): + return await self._connection.callMethod("reloadData", reloadPattern) def _genNextServerCallID() -> Generator[int, None, None]: From 5e4d684a9fb447cf429ae90f23429483a62f836d Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Mon, 16 Dec 2024 14:45:45 +0000 Subject: [PATCH 06/14] Remove connection-related methods from "public" interface --- src/fontra/client/core/remote.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/fontra/client/core/remote.js b/src/fontra/client/core/remote.js index 3b0ed98f9c..b5a662959e 100644 --- a/src/fontra/client/core/remote.js +++ b/src/fontra/client/core/remote.js @@ -2,7 +2,7 @@ import { RemoteError } from "./errors.js"; export async function getRemoteProxy(wsURL) { const remote = new RemoteObject(wsURL); - await remote.connect(); + await remote._connect(); const remoteProxy = new Proxy(remote, { get: (remote, propertyName) => { if (propertyName === "then" || propertyName === "toJSON") { @@ -14,7 +14,7 @@ export async function getRemoteProxy(wsURL) { return remote[propertyName]; } return (...args) => { - return remote.doCall(propertyName, args); + return remote._doCall(propertyName, args); }; }, set: (remote, propertyName, value) => { @@ -53,7 +53,7 @@ export class RemoteObject { (event) => { if (document.visibilityState === "visible" && this.websocket.readyState > 1) { // console.log("wake reconnect"); - this.connect(); + this._connect(); } }, false @@ -68,7 +68,7 @@ export class RemoteObject { } } - async trigger(event, ...args) { + async _trigger(event, ...args) { if (this._handlers.hasOwnProperty(event)) { return await this._handlers[event](...args); } else { @@ -76,7 +76,7 @@ export class RemoteObject { } } - connect() { + _connect() { if (this._connectPromise !== undefined) { // websocket is still connecting/opening, return the same promise return this._connectPromise; @@ -90,8 +90,8 @@ export class RemoteObject { this.websocket.onopen = (event) => { resolve(event); delete this._connectPromise; - this.websocket.onclose = (event) => this.trigger("close", event); - this.websocket.onerror = (event) => this.trigger("error", event); + this.websocket.onclose = (event) => this._trigger("close", event); + this.websocket.onerror = (event) => this._trigger("error", event); const message = { "client-uuid": this.clientUUID, }; @@ -134,7 +134,7 @@ export class RemoteObject { if (!this._handlers.hasOwnProperty(method)) { throw new Error(`undefined method: ${method}`); } - const returnValue = await this.trigger(method, ...message["arguments"]); + const returnValue = await this._trigger(method, ...message["arguments"]); returnMessage = { "server-call-id": serverCallID, "return-value": returnValue, @@ -148,7 +148,7 @@ export class RemoteObject { } } - async doCall(methodName, args) { + async _doCall(methodName, args) { // console.log("--- doCall", methodName); const clientCallID = this._getNextClientCallID(); const message = { @@ -158,7 +158,7 @@ export class RemoteObject { }; if (this.websocket.readyState !== 1) { // console.log("waiting for reconnect"); - await this.connect(); + await this._connect(); } this.websocket.send(JSON.stringify(message)); From 342faf58af5f000914161190bda4250fd9217207 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Mon, 16 Dec 2024 14:22:52 +0000 Subject: [PATCH 07/14] Document the expectations of the RemoteFont object --- src/fontra/client/core/font-controller.js | 18 +++- types/remotefont.d.ts | 110 ++++++++++++++++++++++ types/var-glyph.d.ts | 7 ++ 3 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 types/remotefont.d.ts create mode 100644 types/var-glyph.d.ts diff --git a/src/fontra/client/core/font-controller.js b/src/fontra/client/core/font-controller.js index 983cae413f..ce605d38dd 100644 --- a/src/fontra/client/core/font-controller.js +++ b/src/fontra/client/core/font-controller.js @@ -29,12 +29,18 @@ import { mapBackward, mapForward, } from "./var-model.js"; +/** + * @import { RemoteFont, FontSource } from 'remotefont'; + * */ const GLYPH_CACHE_SIZE = 2000; const BACKGROUND_IMAGE_CACHE_SIZE = 100; const NUM_TASKS = 12; export class FontController { + /** + * @param {RemoteFont} font + */ constructor(font) { this.font = font; this._glyphsPromiseCache = new LRUCache(GLYPH_CACHE_SIZE); // glyph name -> var-glyph promise @@ -78,12 +84,12 @@ export class FontController { this._resolveInitialized(); } - subscribeChanges(change, wantLiveChanges) { - this.font.subscribeChanges(change, wantLiveChanges); + subscribeChanges(pathOrPattern, wantLiveChanges) { + this.font.subscribeChanges(pathOrPattern, wantLiveChanges); } - unsubscribeChanges(change, wantLiveChanges) { - this.font.unsubscribeChanges(change, wantLiveChanges); + unsubscribeChanges(pathOrPattern, wantLiveChanges) { + this.font.unsubscribeChanges(pathOrPattern, wantLiveChanges); } getRootKeys() { @@ -1081,6 +1087,10 @@ function ensureDenseAxes(axes) { return { ...axes, axes: axes.axes || [], mappings: axes.mappings || [] }; } +/** + * @param {Record} sources + * @returns {Record} + */ function ensureDenseSources(sources) { return mapObjectValues(sources, (source) => { return { diff --git a/types/remotefont.d.ts b/types/remotefont.d.ts new file mode 100644 index 0000000000..06b093f5e1 --- /dev/null +++ b/types/remotefont.d.ts @@ -0,0 +1,110 @@ +import { IntoVariableGlyph } from "./var-glyph"; + +interface FontImageData { + type: string; + data: string; +} + +interface Metric { + value: number; + zone?: number; +} + +interface FontSource { + location: Record; + lineMetricsHorizontalLayout: Record; + lineMetricsVerticalLayout: Record; +} + +interface BackendFeatures { + "background-image": boolean; +} +interface BackendProjectManagerFeatures { + "export-as": boolean; +} + +interface BackendInfo { + features: BackendFeatures; + projectManagerFeatures: BackendProjectManagerFeatures; +} + +interface RemoteFont { + /** + * Register an event handler to deal with events from the backend. + * + * Available events are: `close`, `error`, `messageFromServer`, `externalChange`, `reloadData`. + */ + on(event, callback); + /** + * Get the mapping between glyph names and Unicode codepoints. + */ + getGlyphMap(): Promise>; + /** + * Get the axes of the font + */ + getAxes(): Promise; + /** + * Get the background image for a glyph. + */ + getBackgroundImage(identifier: string): Promise; + /** + * Tell the backend to store an image as the background for a glyph. + */ + putBackgroundImage(identifier: string, image: FontImageData): Promise; + getGlyph(identifier: string): Promise; + getSources(): Promise>; + getUnitsPerEm(): Promise; + /** + * Return any custom data that the backend has stored about this font. + */ + getCustomData(): Promise; + /** + * Return information about the backend's capabilities. + */ + getBackEndInfo(): Promise; + /** + * Is the font read-only? + */ + isReadOnly(): Promise; + /** + * Tell the backend that we are interested in receiving `externalChange` events for this font. + * @param pathOrPattern + * @param wantLiveChanges + */ + subscribeChanges(pathOrPattern: string[], wantLiveChanges: boolean): void; + /** + * Tell the backend to stop sending us `externalChange` events for this font. + * @param pathOrPattern + * @param wantLiveChanges + */ + unsubscribeChanges(pathOrPattern: string[], wantLiveChanges: boolean): void; + /** + * Notify the backend of a change that is final. + * @param finalChange + * @param rollbackChange + * @param editLabel + * @param broadcast + */ + editFinal( + finalChange: Change, + rollbackChange: Change, + editLabel: string, + broadcast: boolean + ); + /** + * Notify the backend of a change that is not yet final. + * @param change + */ + editIncremental(change: Change); + /** + * Asks the backend to export the font as a file. + * Options are dependent on the backend's project manager implementation. + */ + exportAs(options: any); + /** + * + * Which glyphs use glyph `glyphName` as a component. Non-recursive. + * @param glyphname + */ + findGlyphsThatUseGlyph(glyphname: string): Promise; +} diff --git a/types/var-glyph.d.ts b/types/var-glyph.d.ts new file mode 100644 index 0000000000..ce06079db8 --- /dev/null +++ b/types/var-glyph.d.ts @@ -0,0 +1,7 @@ +export interface IntoVariableGlyph { + name: string; + axes: object[]; + sources: object[]; + layers: object[]; + customData: any; +} From 338d91c1200796aa63cf1d2989d269205c4c3387 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Mon, 16 Dec 2024 14:46:14 +0000 Subject: [PATCH 08/14] Improve typing --- src/fontra/client/core/backend-api.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/fontra/client/core/backend-api.js b/src/fontra/client/core/backend-api.js index b23561bbbd..790a5bdf2c 100644 --- a/src/fontra/client/core/backend-api.js +++ b/src/fontra/client/core/backend-api.js @@ -1,6 +1,8 @@ import { getRemoteProxy } from "../core/remote.js"; import { fetchJSON } from "./utils.js"; import { StaticGlyph } from "./var-glyph.js"; +import { VarPackedPath } from "./var-path.js"; +/** @import { RemoteFont } from "remotefont" */ /** * @module fontra/client/core/backend-api @@ -10,7 +12,6 @@ import { StaticGlyph } from "./var-glyph.js"; * an abstraction over the functionality of the web server, so that alternative * backends can be used. * - * @typedef {import('./var-path.js').VarPackedPath} VarPackedPath */ class AbstractBackend { /** @@ -130,6 +131,11 @@ class PythonBackend extends AbstractBackend { return VarPackedPath.fromObject(newPath); } + /** + * + * @param {string} projectPath + * @returns {Promise} Proxy object representing a font on the server. + */ static async remoteFont(projectPath) { const protocol = window.location.protocol === "http:" ? "ws" : "wss"; const wsURL = `${protocol}://${window.location.host}/websocket/${projectPath}`; From cef4c09546c099cac7eb196b91d3dc47d270ae3d Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Mon, 16 Dec 2024 14:58:25 +0000 Subject: [PATCH 09/14] Missing types --- types/axes.d.ts | 14 ++++++++++++++ types/change.d.ts | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 types/axes.d.ts create mode 100644 types/change.d.ts diff --git a/types/axes.d.ts b/types/axes.d.ts new file mode 100644 index 0000000000..3f1f8b2b39 --- /dev/null +++ b/types/axes.d.ts @@ -0,0 +1,14 @@ +interface Axis { + name: string; + defaultValue: number; + minValue: number; + maxValue: number; +} +interface Mapping { + inputLocation: Record; + outputLocation: Record; +} +interface Axes { + axes: Axis[]; + mappings: Mapping[]; +} diff --git a/types/change.d.ts b/types/change.d.ts new file mode 100644 index 0000000000..0b9a6919e1 --- /dev/null +++ b/types/change.d.ts @@ -0,0 +1,32 @@ +type ChangeFunction = + | "=" + | "d" + | "-" + | "+" + | ":" + | "=xy" + | "appendPath" + | "deleteNTrailingContours" + | "insertContour" + | "deleteContour" + | "deletePoint" + | "insertPoint"; + +declare interface Change { + /** + * A list of items, eg. ["glyphs", "Aring"] + */ + p?: string[]; + /** + * Function name, eg. "appendPath" + */ + f?: ChangeFunction; + /** + * Array of arguments for the change function + */ + a?: any[]; + /** + * Array of child changes + */ + c?: Change[]; +} From 78d433ce07a32c040285b1fcf8a6085c019b78a3 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Dec 2024 07:04:32 +0000 Subject: [PATCH 10/14] Route boolean operations through backend --- src/fontra/views/editor/panel-transformation.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/fontra/views/editor/panel-transformation.js b/src/fontra/views/editor/panel-transformation.js index 29957a4efd..16c3c5bb87 100644 --- a/src/fontra/views/editor/panel-transformation.js +++ b/src/fontra/views/editor/panel-transformation.js @@ -502,7 +502,7 @@ export default class TransformationPanel extends Panel { key: "removeOverlaps", auxiliaryElement: html.createDomElement("icon-button", { "src": "/tabler-icons/layers-union.svg", - "onclick": (event) => this.doPathOperations(unionPath, "union"), + "onclick": (event) => this.doPathOperations(Backend.unionPath, "union"), "data-tooltip": translate(`${labelKeyPathOperations}.union`), "data-tooltipposition": "top-left", "class": "ui-form-icon ui-form-icon-button", @@ -513,7 +513,7 @@ export default class TransformationPanel extends Panel { key: "subtractContours", auxiliaryElement: html.createDomElement("icon-button", { "src": "/tabler-icons/layers-subtract.svg", - "onclick": (event) => this.doPathOperations(subtractPath, "subtract"), + "onclick": (event) => this.doPathOperations(Backend.subtractPath, "subtract"), "data-tooltip": translate(`${labelKeyPathOperations}.subtract`), "data-tooltipposition": "top", "class": "ui-form-icon", @@ -524,7 +524,8 @@ export default class TransformationPanel extends Panel { key: "intersectContours", auxiliaryElement: html.createDomElement("icon-button", { "src": "/tabler-icons/layers-intersect-2.svg", - "onclick": (event) => this.doPathOperations(intersectPath, "intersect"), + "onclick": (event) => + this.doPathOperations(Backend.intersectPath, "intersect"), "data-tooltip": translate(`${labelKeyPathOperations}.intersect`), "data-tooltipposition": "top-right", "class": "ui-form-icon", @@ -539,7 +540,7 @@ export default class TransformationPanel extends Panel { key: "excludeContours", auxiliaryElement: html.createDomElement("icon-button", { "src": "/tabler-icons/layers-difference.svg", - "onclick": (event) => this.doPathOperations(excludePath, "exclude"), + "onclick": (event) => this.doPathOperations(Backend.excludePath, "exclude"), "data-tooltip": translate(`${labelKeyPathOperations}.exclude`), "data-tooltipposition": "top-left", "class": "ui-form-icon ui-form-icon-button", From 72e72ad840a3a44ff49bce9b391045f005e20e5e Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Dec 2024 07:28:24 +0000 Subject: [PATCH 11/14] Add getFontInfo method to typing --- types/remotefont.d.ts | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/types/remotefont.d.ts b/types/remotefont.d.ts index 06b093f5e1..4648c85e50 100644 --- a/types/remotefont.d.ts +++ b/types/remotefont.d.ts @@ -28,6 +28,24 @@ interface BackendInfo { projectManagerFeatures: BackendProjectManagerFeatures; } +interface FontInfo { + familyName?: string; + versionMajor?: number; + versionMinor?: number; + copyright?: string; + trademark?: string; + description?: string; + sampleText?: string; + designer?: string; + designerURL?: string; + manufacturer?: string; + manufacturerURL?: string; + licenseDescription?: string; + licenseInfoURL?: string; + vendorID?: string; + customData: object; +} + interface RemoteFont { /** * Register an event handler to deal with events from the backend. @@ -35,25 +53,33 @@ interface RemoteFont { * Available events are: `close`, `error`, `messageFromServer`, `externalChange`, `reloadData`. */ on(event, callback); + /** * Get the mapping between glyph names and Unicode codepoints. */ getGlyphMap(): Promise>; + /** * Get the axes of the font */ getAxes(): Promise; + /** * Get the background image for a glyph. */ getBackgroundImage(identifier: string): Promise; + /** * Tell the backend to store an image as the background for a glyph. */ putBackgroundImage(identifier: string, image: FontImageData): Promise; + getGlyph(identifier: string): Promise; + getSources(): Promise>; + getUnitsPerEm(): Promise; + /** * Return any custom data that the backend has stored about this font. */ @@ -62,22 +88,26 @@ interface RemoteFont { * Return information about the backend's capabilities. */ getBackEndInfo(): Promise; + /** * Is the font read-only? */ isReadOnly(): Promise; + /** * Tell the backend that we are interested in receiving `externalChange` events for this font. * @param pathOrPattern * @param wantLiveChanges */ subscribeChanges(pathOrPattern: string[], wantLiveChanges: boolean): void; + /** * Tell the backend to stop sending us `externalChange` events for this font. * @param pathOrPattern * @param wantLiveChanges */ unsubscribeChanges(pathOrPattern: string[], wantLiveChanges: boolean): void; + /** * Notify the backend of a change that is final. * @param finalChange @@ -91,20 +121,28 @@ interface RemoteFont { editLabel: string, broadcast: boolean ); + /** * Notify the backend of a change that is not yet final. * @param change */ editIncremental(change: Change); + /** * Asks the backend to export the font as a file. * Options are dependent on the backend's project manager implementation. */ exportAs(options: any); + /** * * Which glyphs use glyph `glyphName` as a component. Non-recursive. * @param glyphname */ findGlyphsThatUseGlyph(glyphname: string): Promise; + + /** + * Return information about this font. + */ + getFontInfo(): Promise; } From 3321b5ef2a4a55c48a2383a0c358ee16387fe943 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Dec 2024 07:31:31 +0000 Subject: [PATCH 12/14] Rename view to view-controller --- src/fontra/client/core/{view.js => view-controller.js} | 6 +++--- src/fontra/views/editor/editor.js | 2 +- src/fontra/views/fontinfo/fontinfo.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/fontra/client/core/{view.js => view-controller.js} (93%) diff --git a/src/fontra/client/core/view.js b/src/fontra/client/core/view-controller.js similarity index 93% rename from src/fontra/client/core/view.js rename to src/fontra/client/core/view-controller.js index 2569da0dcc..bdea495852 100644 --- a/src/fontra/client/core/view.js +++ b/src/fontra/client/core/view-controller.js @@ -1,7 +1,7 @@ -import { FontController } from "../core/font-controller.js"; -import { getRemoteProxy } from "../core/remote.js"; -import { makeDisplayPath } from "../core/view-utils.js"; import { Backend } from "./backend-api.js"; +import { FontController } from "./font-controller.js"; +import { getRemoteProxy } from "./remote.js"; +import { makeDisplayPath } from "./view-utils.js"; import { ensureLanguageHasLoaded } from "/core/localization.js"; import { message } from "/web-components/modal-dialog.js"; diff --git a/src/fontra/views/editor/editor.js b/src/fontra/views/editor/editor.js index 5b1b930bda..116340a427 100644 --- a/src/fontra/views/editor/editor.js +++ b/src/fontra/views/editor/editor.js @@ -92,7 +92,7 @@ import { translate, translatePlural, } from "/core/localization.js"; -import { ViewController } from "/core/view.js"; +import { ViewController } from "/core/view-controller.js"; const MIN_CANVAS_SPACE = 200; diff --git a/src/fontra/views/fontinfo/fontinfo.js b/src/fontra/views/fontinfo/fontinfo.js index dcde40e37f..c65ec09fb3 100644 --- a/src/fontra/views/fontinfo/fontinfo.js +++ b/src/fontra/views/fontinfo/fontinfo.js @@ -5,7 +5,7 @@ import { DevelopmentStatusDefinitionsPanel } from "./panel-development-status-de import { FontInfoPanel } from "./panel-font-info.js"; import { SourcesPanel } from "./panel-sources.js"; import { translate } from "/core/localization.js"; -import { ViewController } from "/core/view.js"; +import { ViewController } from "/core/view-controller.js"; export class FontInfoController extends ViewController { static titlePattern(displayPath) { From a02a763f794c74f9e7b590dd91fb35bc2ca5aee5 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Dec 2024 13:47:30 +0000 Subject: [PATCH 13/14] Add typescript to precommit prettier hook --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 29a231b8e4..a40e8ba580 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: hooks: - id: prettier exclude: "src/fontra/client/third-party" - types_or: [css, javascript, json, xml, yaml, markdown, html] + types_or: [css, javascript, json, xml, yaml, markdown, html, ts] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 From ef4f446cedfaef6c017a867205e2bc3bb9157323 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Thu, 19 Dec 2024 11:21:04 +0000 Subject: [PATCH 14/14] Fix up path operations --- src/fontra/views/editor/panel-transformation.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/fontra/views/editor/panel-transformation.js b/src/fontra/views/editor/panel-transformation.js index 16c3c5bb87..e6ff531572 100644 --- a/src/fontra/views/editor/panel-transformation.js +++ b/src/fontra/views/editor/panel-transformation.js @@ -582,7 +582,7 @@ export default class TransformationPanel extends Panel { const undoLabel = translate( `sidebar.selection-transformation.path-operations.${key}` ); - const doUnion = pathOperationFunc === unionPath; + const doUnion = pathOperationFunc === Backend.unionPath; let { point: pointIndices } = parseSelection(this.sceneController.selection); pointIndices = pointIndices || []; @@ -629,9 +629,12 @@ export default class TransformationPanel extends Panel { } } if (doUnion) { - return await pathOperationFunc(selectedContoursPath); + return await pathOperationFunc.bind(Backend)(selectedContoursPath); } else { - return await pathOperationFunc(unselectedContoursPath, selectedContoursPath); + return await pathOperationFunc.bind(Backend)( + unselectedContoursPath, + selectedContoursPath + ); } } );