diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 01866976..c693b690 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,10 +1,15 @@
repos:
# Standard file hygiene
+ # Generated files (ts-rs output, NAPI .d.ts, parity fixtures) are excluded
+ # because their format is owned by the generator. Re-formatting them here
+ # creates a loop with the cargo-test hook that regenerates them.
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
+ exclude: '^(packages/types/src/generated/|packages/types/__fixtures__/|crates/shift-node/index\.(d\.ts|js))'
- id: end-of-file-fixer
+ exclude: '^(packages/types/src/generated/|packages/types/__fixtures__/|crates/shift-node/index\.(d\.ts|js))'
- id: check-yaml
- id: check-added-large-files
- id: check-merge-conflict
@@ -20,7 +25,7 @@ repos:
- id: no-console-log
name: no console.log in production code
- entry: "bash -c 'for f in \"$@\"; do case \"$f\" in *.test.*|*/testing/*|*/e2e/*) continue;; esac; if grep -n \"^\\s*console\\.log\\b\" \"$f\" >/dev/null 2>&1; then echo \"$f has console.log:\"; grep -n \"^\\s*console\\.log\\b\" \"$f\"; exit 1; fi; done'"
+ entry: "bash -c 'for f in \"$@\"; do case \"$f\" in *.test.*|*/testing/*|*/e2e/*|scripts/*) continue;; esac; if grep -n \"^\\s*console\\.log\\b\" \"$f\" >/dev/null 2>&1; then echo \"$f has console.log:\"; grep -n \"^\\s*console\\.log\\b\" \"$f\"; exit 1; fi; done'"
language: system
types_or: [javascript, jsx, ts, tsx]
diff --git a/.prettierignore b/.prettierignore
index 511de71c..a8a5b92b 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -6,6 +6,7 @@ out
*.min.css
pnpm-lock.yaml
packages/types/src/generated
+packages/types/__fixtures__
crates/shift-node/index.js
crates/shift-node/index.d.ts
**/vendor
diff --git a/Cargo.lock b/Cargo.lock
index 8b7bddc7..2541747c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -138,6 +138,16 @@ version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
+[[package]]
+name = "borsh"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+]
+
[[package]]
name = "bumpalo"
version = "3.19.0"
@@ -164,6 +174,12 @@ dependencies = [
"syn",
]
+[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+
[[package]]
name = "camino"
version = "1.1.10"
@@ -211,6 +227,12 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
[[package]]
name = "chrono"
version = "0.4.41"
@@ -517,14 +539,14 @@ dependencies = [
"ansi_term",
"chrono",
"env_logger",
- "fontdrasil",
+ "fontdrasil 0.2.0",
"indexmap",
"log",
- "ordered-float",
+ "ordered-float 4.6.0",
"serde",
- "smol_str",
+ "smol_str 0.2.2",
"thiserror 1.0.69",
- "write-fonts",
+ "write-fonts 0.38.2",
]
[[package]]
@@ -555,6 +577,16 @@ dependencies = [
"serde",
]
+[[package]]
+name = "font-types"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39a654f404bbcbd48ea58c617c2993ee91d1cb63727a37bf2323a4edeed1b8c5"
+dependencies = [
+ "bytemuck",
+ "serde",
+]
+
[[package]]
name = "fontbe"
version = "0.2.0"
@@ -565,19 +597,19 @@ dependencies = [
"chrono",
"env_logger",
"fea-rs",
- "fontdrasil",
+ "fontdrasil 0.2.0",
"fontir",
"icu_properties",
"indexmap",
"kurbo 0.11.2",
"log",
- "ordered-float",
+ "ordered-float 4.6.0",
"parking_lot",
"serde",
- "smol_str",
+ "smol_str 0.2.2",
"thiserror 1.0.69",
"tinystr",
- "write-fonts",
+ "write-fonts 0.38.2",
]
[[package]]
@@ -593,7 +625,7 @@ dependencies = [
"env_logger",
"filetime",
"fontbe",
- "fontdrasil",
+ "fontdrasil 0.2.0",
"fontir",
"fontra2fontir",
"glyphs2fontir",
@@ -606,7 +638,7 @@ dependencies = [
"thiserror 1.0.69",
"ufo2fontir",
"vergen-gitcl",
- "write-fonts",
+ "write-fonts 0.38.2",
]
[[package]]
@@ -615,10 +647,26 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7486f5a01d9b86a45a53e4d2f46172d731af11e801a3c7c24af30cab055b9d0"
dependencies = [
- "ordered-float",
+ "ordered-float 4.6.0",
"serde",
- "smol_str",
- "write-fonts",
+ "smol_str 0.2.2",
+ "write-fonts 0.38.2",
+]
+
+[[package]]
+name = "fontdrasil"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa3a7a2fbc9d1817cd07298326376c8bd39868efe7c6149090c4fafea95eaf34"
+dependencies = [
+ "env_logger",
+ "kurbo 0.12.0",
+ "log",
+ "ordered-float 5.3.0",
+ "serde",
+ "smol_str 0.3.6",
+ "thiserror 2.0.12",
+ "write-fonts 0.44.1",
]
[[package]]
@@ -632,17 +680,17 @@ dependencies = [
"chrono",
"env_logger",
"filetime",
- "fontdrasil",
+ "fontdrasil 0.2.0",
"indexmap",
"kurbo 0.11.2",
"log",
- "ordered-float",
+ "ordered-float 4.6.0",
"parking_lot",
"serde",
"serde_yaml",
- "smol_str",
+ "smol_str 0.2.2",
"thiserror 1.0.69",
- "write-fonts",
+ "write-fonts 0.38.2",
]
[[package]]
@@ -652,16 +700,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4511cace96f3243fef1350e87abf326900ce3a7af6c3646e7cb6e3b26b295baf"
dependencies = [
"env_logger",
- "fontdrasil",
+ "fontdrasil 0.2.0",
"fontir",
"indexmap",
"kurbo 0.11.2",
"log",
- "ordered-float",
+ "ordered-float 4.6.0",
"serde",
"serde_json",
"thiserror 1.0.69",
- "write-fonts",
+ "write-fonts 0.38.2",
]
[[package]]
@@ -774,16 +822,16 @@ dependencies = [
"bincode",
"chrono",
"env_logger",
- "fontdrasil",
+ "fontdrasil 0.2.0",
"icu_properties",
"indexmap",
"kurbo 0.11.2",
"log",
- "ordered-float",
+ "ordered-float 4.6.0",
"quick-xml",
"regex",
"serde",
- "smol_str",
+ "smol_str 0.2.2",
"thiserror 1.0.69",
]
@@ -795,16 +843,16 @@ checksum = "d66204b6a2d374b221a9676b22b64de7a711482dc58f6a13da1c4b20978a3d43"
dependencies = [
"chrono",
"env_logger",
- "fontdrasil",
+ "fontdrasil 0.2.0",
"fontir",
"glyphs-reader",
"indexmap",
"kurbo 0.11.2",
"log",
- "ordered-float",
- "smol_str",
+ "ordered-float 4.6.0",
+ "smol_str 0.2.2",
"thiserror 1.0.69",
- "write-fonts",
+ "write-fonts 0.38.2",
]
[[package]]
@@ -988,6 +1036,18 @@ dependencies = [
"smallvec",
]
+[[package]]
+name = "kurbo"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce9729cc38c18d86123ab736fd2e7151763ba226ac2490ec092d1dd148825e32"
+dependencies = [
+ "arrayvec",
+ "euclid",
+ "serde",
+ "smallvec",
+]
+
[[package]]
name = "kurbo"
version = "0.13.0"
@@ -1235,6 +1295,17 @@ dependencies = [
"serde",
]
+[[package]]
+name = "ordered-float"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e"
+dependencies = [
+ "num-traits",
+ "rand",
+ "serde",
+]
+
[[package]]
name = "parking_lot"
version = "0.12.4"
@@ -1396,7 +1467,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04ca636dac446b5664bd16c069c00a9621806895b8bb02c2dc68542b23b8f25d"
dependencies = [
"bytemuck",
- "font-types",
+ "font-types 0.9.0",
"serde",
]
@@ -1407,7 +1478,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "192735ef611aac958468e670cb98432c925426f3cb71521fda202130f7388d91"
dependencies = [
"bytemuck",
- "font-types",
+ "font-types 0.9.0",
+]
+
+[[package]]
+name = "read-fonts"
+version = "0.36.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eaa2941a4c05443ee3a7b26ab076a553c343ad5995230cc2b1d3e993bdc6345"
+dependencies = [
+ "bytemuck",
+ "font-types 0.10.1",
+ "serde",
]
[[package]]
@@ -1587,6 +1669,7 @@ version = "0.0.0"
dependencies = [
"bitflags",
"fontc",
+ "fontdrasil 0.4.0",
"norad 0.16.0",
"serde",
"serde_json",
@@ -1601,6 +1684,8 @@ dependencies = [
name = "shift-ir"
version = "0.1.0"
dependencies = [
+ "fontdrasil 0.4.0",
+ "indexmap",
"kurbo 0.13.0",
"linesweeper",
"serde",
@@ -1659,6 +1744,16 @@ dependencies = [
"serde",
]
+[[package]]
+name = "smol_str"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523"
+dependencies = [
+ "borsh",
+ "serde_core",
+]
+
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
@@ -1838,18 +1933,18 @@ checksum = "6d6a943c02f505748a2d076447518ef08fdd6fa7d6b12fd6352681fb873c5c27"
dependencies = [
"chrono",
"env_logger",
- "fontdrasil",
+ "fontdrasil 0.2.0",
"fontir",
"indexmap",
"kurbo 0.11.2",
"log",
"norad 0.15.0",
- "ordered-float",
+ "ordered-float 4.6.0",
"plist",
"serde",
"serde_yaml",
"thiserror 1.0.69",
- "write-fonts",
+ "write-fonts 0.38.2",
]
[[package]]
@@ -2179,7 +2274,7 @@ version = "0.38.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60491c1d7e5873e721607fa05fd9b9fc66b0f0179b2b5f0f26a2022aafdcc61e"
dependencies = [
- "font-types",
+ "font-types 0.9.0",
"indexmap",
"kurbo 0.11.2",
"log",
@@ -2187,6 +2282,20 @@ dependencies = [
"serde",
]
+[[package]]
+name = "write-fonts"
+version = "0.44.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "187aca250ee00f9d9ad3a1ebc9ed65c9c921fdd4690b095ca6e5a8df7246d871"
+dependencies = [
+ "font-types 0.10.1",
+ "indexmap",
+ "kurbo 0.12.0",
+ "log",
+ "read-fonts 0.36.0",
+ "serde",
+]
+
[[package]]
name = "writeable"
version = "0.6.1"
diff --git a/ROADMAP.md b/ROADMAP.md
index b6a296c5..c439bc36 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -361,21 +361,21 @@
**Designspace Support**
-- [ ] Load `.designspace` files
-- [ ] Parse axis definitions (wght, wdth, ital, custom)
+- [x] Load `.designspace` files
+- [x] Parse axis definitions (wght, wdth, ital, custom)
- [ ] Named instances
**Masters Editing**
-- [ ] Master list panel
+- [x] Master list panel
- [ ] Switch between masters
- [ ] Add/remove masters
- [ ] Copy glyph between masters
**Interpolation**
-- [ ] Compatibility checker (point/contour count)
-- [ ] Interpolation preview slider
+- [x] Compatibility checker (point/contour count)
+- [x] Interpolation preview slider
- [ ] Intermediate master insertion
- [ ] Extrapolation warning
diff --git a/apps/desktop/src/main/managers/AppLifecycle.ts b/apps/desktop/src/main/managers/AppLifecycle.ts
index 781c31fa..76fdd021 100644
--- a/apps/desktop/src/main/managers/AppLifecycle.ts
+++ b/apps/desktop/src/main/managers/AppLifecycle.ts
@@ -207,8 +207,13 @@ export class AppLifecycle {
ipc.handle(ipcMain, "dialog:openFont", async () => {
const result = await dialog.showOpenDialog({
- properties: ["openFile"],
- filters: [{ name: "Fonts", extensions: ["ttf", "otf", "ufo", "glyphs", "glyphspackage"] }],
+ properties: ["openFile", "openDirectory"],
+ filters: [
+ {
+ name: "Fonts",
+ extensions: ["ttf", "otf", "ufo", "glyphs", "glyphspackage", "designspace"],
+ },
+ ],
});
if (!result.canceled && result.filePaths[0]) {
return result.filePaths[0];
diff --git a/apps/desktop/src/main/managers/MenuManager.ts b/apps/desktop/src/main/managers/MenuManager.ts
index f6173258..100493be 100644
--- a/apps/desktop/src/main/managers/MenuManager.ts
+++ b/apps/desktop/src/main/managers/MenuManager.ts
@@ -118,9 +118,12 @@ export class MenuManager {
accelerator: "CmdOrCtrl+O",
click: async () => {
const result = await dialog.showOpenDialog({
- properties: ["openFile"],
+ properties: ["openFile", "openDirectory"],
filters: [
- { name: "Fonts", extensions: ["ttf", "otf", "ufo", "glyphs", "glyphspackage"] },
+ {
+ name: "Fonts",
+ extensions: ["ttf", "otf", "ufo", "glyphs", "glyphspackage", "designspace"],
+ },
],
});
if (!result.canceled && result.filePaths[0]) {
diff --git a/apps/desktop/src/main/managers/openFontPath.ts b/apps/desktop/src/main/managers/openFontPath.ts
index 515b521b..ca318c03 100644
--- a/apps/desktop/src/main/managers/openFontPath.ts
+++ b/apps/desktop/src/main/managers/openFontPath.ts
@@ -1,6 +1,13 @@
import path from "node:path";
-const SUPPORTED_FONT_EXTENSIONS = new Set([".ufo", ".ttf", ".otf", ".glyphs", ".glyphspackage"]);
+const SUPPORTED_FONT_EXTENSIONS = new Set([
+ ".ufo",
+ ".ttf",
+ ".otf",
+ ".glyphs",
+ ".glyphspackage",
+ ".designspace",
+]);
export function isSupportedFontPath(filePath: string): boolean {
const ext = path.extname(filePath).toLowerCase();
diff --git a/apps/desktop/src/renderer/src/bridge/NativeBridge.ts b/apps/desktop/src/renderer/src/bridge/NativeBridge.ts
index 812c3af3..d8501215 100644
--- a/apps/desktop/src/renderer/src/bridge/NativeBridge.ts
+++ b/apps/desktop/src/renderer/src/bridge/NativeBridge.ts
@@ -6,6 +6,10 @@ import type {
ContourId,
Point2D,
AnchorId,
+ Axis,
+ Source,
+ GlyphVariationData,
+ MasterSnapshot,
} from "@shift/types";
import { signal, type WritableSignal, type Signal } from "@/lib/reactive/signal";
import type { Bounds } from "@shift/geo";
@@ -22,6 +26,11 @@ import { ContourContent } from "@/lib/clipboard";
import type { NodePositionUpdateList } from "@/types/positionUpdate";
import { Glyph, type GlyphChange } from "@/lib/model/Glyph";
+export interface InterpolationResult {
+ instance: GlyphSnapshot;
+ errors: Array<{ sourceIndex: number; sourceName: string; message: string }>;
+}
+
/**
* Owns the raw NAPI bridge and the reactive {@link $glyph} signal.
* All font queries, session lifecycle, and glyph mutations live here.
@@ -135,6 +144,34 @@ export class NativeBridge {
return JSON.parse(payload) as CompositeComponentsPayload;
}
+ /** @knipclassignore — used by VariationPanel component */
+ isVariable(): boolean {
+ return this.#raw.isVariable();
+ }
+
+ /** @knipclassignore — used by VariationPanel component */
+ getAxes(): Axis[] {
+ return JSON.parse(this.#raw.getAxes()) as Axis[];
+ }
+
+ /** @knipclassignore — used by VariationPanel component */
+ getSources(): Source[] {
+ return JSON.parse(this.#raw.getSources()) as Source[];
+ }
+
+ /** @knipclassignore — used by VariationPanel component */
+ getGlyphMasterSnapshots(glyphName: string): MasterSnapshot[] | null {
+ const json = this.#raw.getGlyphMasterSnapshots(glyphName);
+ if (!json) return null;
+ return JSON.parse(json) as MasterSnapshot[];
+ }
+
+ getGlyphVariationData(glyphName: string): GlyphVariationData | null {
+ const json = this.#raw.getGlyphVariationData(glyphName);
+ if (!json) return null;
+ return JSON.parse(json) as GlyphVariationData;
+ }
+
getSnapshot(): GlyphSnapshot {
return JSON.parse(this.#raw.getSnapshotData()) as GlyphSnapshot;
}
diff --git a/apps/desktop/src/renderer/src/components/GlyphPreview.tsx b/apps/desktop/src/renderer/src/components/GlyphPreview.tsx
index 3d3b74dd..4d1cde69 100644
--- a/apps/desktop/src/renderer/src/components/GlyphPreview.tsx
+++ b/apps/desktop/src/renderer/src/components/GlyphPreview.tsx
@@ -60,6 +60,9 @@ export const GlyphPreview = memo(function GlyphPreview({
return null;
}
+ // TODO Phase D: wire variation interpolation into glyph grid.
+ // For now grid always shows the default master; the variation slider only
+ // affects the canvas glyph via VariationPanel.
const svgPath = font.getSvgPath(name);
const advance = font.getAdvance(name);
const fontMetrics = font.getMetrics();
diff --git a/apps/desktop/src/renderer/src/components/GlyphSidebar.tsx b/apps/desktop/src/renderer/src/components/GlyphSidebar.tsx
index 9d097390..5033c4e2 100644
--- a/apps/desktop/src/renderer/src/components/GlyphSidebar.tsx
+++ b/apps/desktop/src/renderer/src/components/GlyphSidebar.tsx
@@ -9,6 +9,7 @@ import { useSignalEffect } from "@/hooks/useSignalEffect";
import { GlyphSection } from "./sidebar-right/GlyphSection";
import { AnchorSection } from "./sidebar-right/AnchorSection";
import { BooleanOps } from "./BooleanOps";
+import { VariationPanel } from "./VariationPanel";
export const GlyphSidebar = () => {
const editor = getEditor();
@@ -42,6 +43,10 @@ export const GlyphSidebar = () => {
+
+
+
+
{hasPointSelection && (
diff --git a/apps/desktop/src/renderer/src/components/VariationPanel.tsx b/apps/desktop/src/renderer/src/components/VariationPanel.tsx
new file mode 100644
index 00000000..2248fca8
--- /dev/null
+++ b/apps/desktop/src/renderer/src/components/VariationPanel.tsx
@@ -0,0 +1,180 @@
+import { useCallback, useEffect, useRef, useState } from "react";
+import type { Axis, GlyphVariationData, Source } from "@shift/types";
+import { SidebarSection } from "./sidebar-right/SidebarSection";
+import { getEditor } from "@/store/store";
+import { useSignalState } from "@/lib/reactive";
+import { interpolate, normalize } from "@/lib/interpolation/interpolate";
+import { Input } from "@shift/ui";
+
+export const VariationPanel = () => {
+ const editor = getEditor();
+ const font = editor.font;
+ const fontLoaded = useSignalState(font.$loaded);
+
+ const [axes, setAxes] = useState
([]);
+ const [sources, setSources] = useState([]);
+ const [location, setLocation] = useState>({});
+ const variationDataRef = useRef(null);
+ const [isInterpolating, setIsInterpolating] = useState(false);
+ const [editingGlyph, setEditingGlyph] = useState(null);
+
+ useEffect(() => {
+ if (!fontLoaded || !font.isVariable()) {
+ setAxes([]);
+ setSources([]);
+ return;
+ }
+
+ const fontAxes = font.getAxes();
+ setAxes(fontAxes);
+ setSources(font.getSources());
+
+ const defaults: Record = {};
+ for (const axis of fontAxes) {
+ defaults[axis.tag] = axis.default;
+ }
+ setLocation(defaults);
+ }, [fontLoaded, font]);
+
+ useEffect(() => {
+ setEditingGlyph(editor.getActiveGlyphName());
+ });
+
+ // Fetch variation data ONCE per glyph (cached in ref). Slider scrub reads from
+ // the cache and never goes back to Rust. TODO Phase D: invalidate on glyph
+ // commit (needs a commit-version signal on Glyph).
+ useEffect(() => {
+ if (axes.length === 0 || !editingGlyph) {
+ variationDataRef.current = null;
+ return;
+ }
+ variationDataRef.current = font.getGlyphVariationData(editingGlyph);
+ setIsInterpolating(false);
+ }, [axes, editingGlyph, font]);
+
+ // Pure JS math, zero NAPI calls.
+ const applyAt = useCallback(
+ (newLocation: Record) => {
+ const data = variationDataRef.current;
+ if (!data) return;
+
+ const values = interpolate(data, normalize(newLocation, axes));
+ const glyph = editor.glyph.peek();
+ if (glyph) glyph.applyValues(values);
+ font.setVariationLocation({ values: newLocation });
+ },
+ [axes, editor, font],
+ );
+
+ const handleAxisChange = useCallback(
+ (tag: string, value: number) => {
+ const newLocation = { ...location, [tag]: value };
+ setLocation(newLocation);
+ setIsInterpolating(true);
+ applyAt(newLocation);
+ },
+ [location, applyAt],
+ );
+
+ const handleMasterClick = useCallback(
+ (source: Source) => {
+ const newLocation: Record = {};
+ for (const axis of axes) {
+ newLocation[axis.tag] = source.location.values[axis.tag] ?? axis.default;
+ }
+ setLocation(newLocation);
+ setIsInterpolating(true);
+ applyAt(newLocation);
+ },
+ [axes, applyAt],
+ );
+
+ const handleResetToSession = useCallback(() => {
+ if (!isInterpolating) return;
+
+ setIsInterpolating(false);
+ font.setVariationLocation(null);
+ const glyph = editor.glyph.peek();
+ if (glyph) {
+ glyph.restoreSnapshot(glyph.toSnapshot());
+ }
+
+ const defaults: Record = {};
+ for (const axis of axes) {
+ defaults[axis.tag] = axis.default;
+ }
+ setLocation(defaults);
+ }, [isInterpolating, editor, font, axes]);
+
+ if (axes.length === 0) return null;
+
+ return (
+
+
+ {axes.map((axis) => (
+
handleAxisChange(axis.tag, value)}
+ />
+ ))}
+ {sources.length > 0 && (
+
+ {sources.map((s) => (
+
+ ))}
+
+ )}
+ {isInterpolating && (
+
+ )}
+
+
+ );
+};
+
+interface AxisSliderProps {
+ axis: Axis;
+ value: number;
+ onChange: (value: number) => void;
+}
+
+const AxisSlider = ({ axis, value, onChange }: AxisSliderProps) => {
+ const displayValue = Math.round(value);
+
+ return (
+
+
+ {axis.name}
+ {displayValue}
+
+
onChange(Number(e.target.value))}
+ className="w-full h-1.5 bg-[#e0e0e0] rounded-full appearance-none cursor-pointer
+ [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3.5 [&::-webkit-slider-thumb]:h-3.5
+ [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-[#333]
+ [&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:border-2
+ [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:shadow-sm"
+ />
+
+ );
+};
diff --git a/apps/desktop/src/renderer/src/lib/editor/Editor.ts b/apps/desktop/src/renderer/src/lib/editor/Editor.ts
index 21beff61..77fd0bd1 100644
--- a/apps/desktop/src/renderer/src/lib/editor/Editor.ts
+++ b/apps/desktop/src/renderer/src/lib/editor/Editor.ts
@@ -857,8 +857,7 @@ export class Editor {
/** Resolve a unicode codepoint to a glyph ref and insert into the text run. */
public insertTextCodepoint(codepoint: number): void {
- const glyphName = this.font.nameForUnicode(codepoint);
- if (!glyphName) return;
+ const glyphName = this.font.glyphName(codepoint);
this.#textRunController.insert({ glyphName, unicode: codepoint });
}
diff --git a/apps/desktop/src/renderer/src/lib/interpolation/interpolate.test.ts b/apps/desktop/src/renderer/src/lib/interpolation/interpolate.test.ts
new file mode 100644
index 00000000..aaf5cde0
--- /dev/null
+++ b/apps/desktop/src/renderer/src/lib/interpolation/interpolate.test.ts
@@ -0,0 +1,143 @@
+import { describe, it, expect } from "vitest";
+import { readFileSync } from "node:fs";
+import { resolve } from "node:path";
+import type { GlyphVariationData } from "@shift/types";
+import { interpolate, scalarAt, type NormalizedLocation } from "./interpolate";
+
+interface MasterEntry {
+ sourceName: string;
+ isDefaultSource: boolean;
+ designspaceLocation: Record;
+ normalisedLocation: NormalizedLocation;
+ expected: number[];
+}
+
+interface Fixture {
+ glyphName: string;
+ designspaceTarget: Record;
+ normalisedLocation: NormalizedLocation;
+ data: GlyphVariationData;
+ expected: number[];
+ masters: MasterEntry[];
+}
+
+function loadFixture(): Fixture {
+ // Generated by `cargo test -p shift-core --test interpolation_parity`.
+ // 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;
+}
+
+describe("interpolate — parity with fontdrasil", () => {
+ it("matches Rust interpolate_from_deltas at the fixture target", () => {
+ const fx = loadFixture();
+ const result = interpolate(fx.data, fx.normalisedLocation);
+
+ expect(result.length).toBe(fx.expected.length);
+ const tolerance = 1e-9;
+ for (let i = 0; i < result.length; i++) {
+ const diff = Math.abs(result[i] - fx.expected[i]);
+ expect(
+ diff,
+ `index ${i}: got ${result[i]}, expected ${fx.expected[i]}, diff ${diff}`,
+ ).toBeLessThan(tolerance);
+ }
+ });
+
+ it("returns the default master values at the origin", () => {
+ const fx = loadFixture();
+ const origin: NormalizedLocation = {};
+ for (const tag of Object.keys(fx.normalisedLocation)) origin[tag] = 0;
+
+ const result = interpolate(fx.data, origin);
+ // Default master = first deltas entry (its region has all-zero tents → scalar 1
+ // at origin and elsewhere; other regions have non-zero scalar only off-origin).
+ // At origin, only the default's contribution should remain.
+ expect(result.length).toBe(fx.data.deltas[0].length);
+ for (let i = 0; i < result.length; i++) {
+ expect(Math.abs(result[i] - fx.data.deltas[0][i])).toBeLessThan(1e-9);
+ }
+ });
+});
+
+describe("interpolate — round-trip at master locations", () => {
+ // Strongest defence against unpacking drift: at each master's location, the
+ // interpolated values must match that master's stored flat geometry. This
+ // exercises every region's tent in turn (each master sits at exactly one
+ // off-default region's peak, plus the default region with scalar=1).
+ it("recovers each master's flat values when interpolating at its own location", () => {
+ const fx = loadFixture();
+ expect(fx.masters.length).toBeGreaterThan(0);
+
+ for (const master of fx.masters) {
+ const result = interpolate(fx.data, master.normalisedLocation);
+
+ expect(result.length, `${master.sourceName}: length`).toBe(master.expected.length);
+ for (let i = 0; i < result.length; i++) {
+ const diff = Math.abs(result[i] - master.expected[i]);
+ expect(
+ diff,
+ `${master.sourceName} index ${i}: got ${result[i]}, expected ${master.expected[i]}, diff ${diff}`,
+ ).toBeLessThan(1e-9);
+ }
+ }
+ });
+
+ it("identifies exactly one default master in the fixture", () => {
+ // Sanity check on the fixture itself — fontdrasil's invariant.
+ const fx = loadFixture();
+ const defaults = fx.masters.filter((m) => m.isDefaultSource);
+ expect(defaults.length).toBe(1);
+ });
+});
+
+describe("scalarAt — boundary semantics", () => {
+ it("returns 1 at the peak", () => {
+ expect(scalarAt({ wght: 0.5 }, [{ axisTag: "wght", lower: 0, peak: 0.5, upper: 1 }])).toBe(1);
+ });
+
+ it("returns 0 at the lower boundary (inclusive)", () => {
+ expect(scalarAt({ wght: 0 }, [{ axisTag: "wght", lower: 0, peak: 0.5, upper: 1 }])).toBe(0);
+ });
+
+ it("returns 0 at the upper boundary (inclusive)", () => {
+ expect(scalarAt({ wght: 1 }, [{ axisTag: "wght", lower: 0, peak: 0.5, upper: 1 }])).toBe(0);
+ });
+
+ it("returns 0 outside the tent", () => {
+ expect(scalarAt({ wght: 1.5 }, [{ axisTag: "wght", lower: 0, peak: 0.5, upper: 1 }])).toBe(0);
+ expect(scalarAt({ wght: -0.5 }, [{ axisTag: "wght", lower: 0, peak: 0.5, upper: 1 }])).toBe(0);
+ });
+
+ it("ramps linearly from lower to peak", () => {
+ // halfway between lower=0 and peak=0.5 → scalar 0.5
+ expect(
+ scalarAt({ wght: 0.25 }, [{ axisTag: "wght", lower: 0, peak: 0.5, upper: 1 }]),
+ ).toBeCloseTo(0.5, 9);
+ });
+
+ it("ramps linearly from peak to upper", () => {
+ // halfway between peak=0.5 and upper=1 → scalar 0.5
+ expect(
+ scalarAt({ wght: 0.75 }, [{ axisTag: "wght", lower: 0, peak: 0.5, upper: 1 }]),
+ ).toBeCloseTo(0.5, 9);
+ });
+
+ it("treats (0,0,0) tent as always-on", () => {
+ // Default-region tents — full influence regardless of location
+ expect(scalarAt({ wght: 0.7 }, [{ axisTag: "wght", lower: 0, peak: 0, upper: 0 }])).toBe(1);
+ });
+
+ it("multiplies scalars across multiple axes", () => {
+ // Each axis halfway up its ramp → 0.5 × 0.5 = 0.25
+ const region = [
+ { axisTag: "wght", lower: 0, peak: 0.5, upper: 1 },
+ { axisTag: "wdth", lower: 0, peak: 0.5, upper: 1 },
+ ];
+ expect(scalarAt({ wght: 0.25, wdth: 0.25 }, region)).toBeCloseTo(0.25, 9);
+ });
+
+ it("returns 0 for invalid tents (peak < lower)", () => {
+ expect(scalarAt({ wght: 0.5 }, [{ axisTag: "wght", lower: 0.5, peak: 0, upper: 1 }])).toBe(1); // invalid tent skipped, no other tents → scalar stays 1
+ });
+});
diff --git a/apps/desktop/src/renderer/src/lib/interpolation/interpolate.ts b/apps/desktop/src/renderer/src/lib/interpolation/interpolate.ts
new file mode 100644
index 00000000..fac5dd33
--- /dev/null
+++ b/apps/desktop/src/renderer/src/lib/interpolation/interpolate.ts
@@ -0,0 +1,91 @@
+/**
+ * Variable-font interpolation — per-tick eval path.
+ *
+ * The hard math (region construction + delta decomposition) lives in Rust via
+ * `fontdrasil`. This module is the trivial per-tick eval — region tent function
+ * and a dot product against cached deltas. Ported faithfully from fontdrasil's
+ * `scalar_at_with_args` and `interpolate_from_deltas`.
+ *
+ * Parity-tested against `packages/types/__fixtures__/variation_parity.json`,
+ * generated by `crates/shift-core/tests/interpolation_parity.rs`.
+ */
+
+import type { Axis, AxisTent, GlyphVariationData } from "@shift/types";
+
+export type NormalizedLocation = Record;
+export type DesignspaceLocation = Record;
+
+/** Asymmetric normalize — mirrors `Axis::normalize` in shift-ir/src/axis.rs. */
+export function normalizeAxis(value: number, axis: Axis): number {
+ if (value < axis.default) {
+ const range = axis.default - axis.minimum;
+ return range === 0 ? 0 : (value - axis.default) / range;
+ }
+ if (value > axis.default) {
+ const range = axis.maximum - axis.default;
+ return range === 0 ? 0 : (value - axis.default) / range;
+ }
+ return 0;
+}
+
+export function normalize(loc: DesignspaceLocation, axes: Axis[]): NormalizedLocation {
+ const out: NormalizedLocation = {};
+ for (const axis of axes) {
+ out[axis.tag] = normalizeAxis(loc[axis.tag] ?? axis.default, axis);
+ }
+ return out;
+}
+
+/** Mirrors fontdrasil's `Tent::validate` — used as a per-tent filter. */
+function isValidTent(t: AxisTent): boolean {
+ if (t.lower > t.peak || t.peak > t.upper) return false;
+ if (t.lower < 0 && t.upper > 0) return false; // can't span across default
+ return true;
+}
+
+/**
+ * Scalar contribution of a region at a normalized location.
+ * Exact port of fontdrasil's `VariationRegion::scalar_at_with_args` (no extrapolation).
+ */
+export function scalarAt(loc: NormalizedLocation, region: AxisTent[]): number {
+ let scalar = 1;
+ for (const t of region) {
+ if (!isValidTent(t)) continue;
+
+ const v = loc[t.axisTag] ?? 0;
+
+ if (v === t.peak) continue;
+
+ if (t.lower === 0 && t.peak === 0 && t.upper === 0) {
+ continue; // (0,0,0) tent = always-on (default region)
+ }
+
+ if (v <= t.lower || t.upper <= v) return 0; // outside, boundary inclusive
+
+ const edge = v < t.peak ? t.lower : t.upper;
+ scalar *= (v - edge) / (t.peak - edge);
+ }
+ return scalar;
+}
+
+/**
+ * Evaluate per-region deltas at a normalized location.
+ * Exact port of fontdrasil's `VariationModel::interpolate_from_deltas` (no extrapolation).
+ *
+ * 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-core::interpolation:
+ * [xAdvance, p0.x, p0.y, p1.x, p1.y, ..., a0.x, a0.y, ...]
+ */
+export function interpolate(data: GlyphVariationData, loc: NormalizedLocation): Float64Array {
+ const len = data.deltas[0]?.length ?? 0;
+ const result = new Float64Array(len);
+ for (let r = 0; r < data.regions.length; r++) {
+ const s = scalarAt(loc, data.regions[r]);
+ if (s === 0) continue;
+ const d = data.deltas[r];
+ for (let i = 0; i < len; i++) result[i] += s * d[i];
+ }
+ return result;
+}
diff --git a/apps/desktop/src/renderer/src/lib/model/Font.ts b/apps/desktop/src/renderer/src/lib/model/Font.ts
index 48287038..3a05da47 100644
--- a/apps/desktop/src/renderer/src/lib/model/Font.ts
+++ b/apps/desktop/src/renderer/src/lib/model/Font.ts
@@ -1,4 +1,13 @@
-import type { FontMetrics, FontMetadata, CompositeGlyph } from "@shift/types";
+import type {
+ FontMetrics,
+ FontMetadata,
+ CompositeGlyph,
+ Axis,
+ Source,
+ Location,
+ GlyphVariationData,
+} from "@shift/types";
+import type { MasterSnapshot } from "@shift/types";
import type { Bounds } from "@shift/geo";
import { signal, type WritableSignal, type Signal } from "@/lib/reactive/signal";
import type { NativeBridge } from "@/bridge";
@@ -15,12 +24,14 @@ export class Font {
readonly #$loaded: WritableSignal;
readonly #$unicodes: WritableSignal;
readonly #$metrics: WritableSignal;
+ readonly #$variationLocation: WritableSignal;
constructor(bridge: NativeBridge) {
this.#bridge = bridge;
this.#$loaded = signal(false);
this.#$unicodes = signal([]);
this.#$metrics = signal(null);
+ this.#$variationLocation = signal(null);
}
/** @knipclassignore */
@@ -93,6 +104,41 @@ export class Font {
return this.#bridge.getSvgPath(name);
}
+ /** @knipclassignore — used by GlyphPreview for variation interpolation */
+ get $variationLocation(): Signal {
+ return this.#$variationLocation;
+ }
+
+ /** @knipclassignore — used by VariationPanel */
+ setVariationLocation(location: Location | null): void {
+ this.#$variationLocation.set(location);
+ }
+
+ /** @knipclassignore — used by VariationPanel component */
+ isVariable(): boolean {
+ return this.#bridge.isVariable();
+ }
+
+ /** @knipclassignore — used by VariationPanel component */
+ getAxes(): Axis[] {
+ return this.#bridge.getAxes();
+ }
+
+ /** @knipclassignore — used by VariationPanel component */
+ getSources(): Source[] {
+ return this.#bridge.getSources();
+ }
+
+ /** @knipclassignore — used by VariationPanel component */
+ getGlyphMasterSnapshots(glyphName: string): MasterSnapshot[] | null {
+ return this.#bridge.getGlyphMasterSnapshots(glyphName);
+ }
+
+ /** @knipclassignore — used by VariationPanel component */
+ getGlyphVariationData(glyphName: string): GlyphVariationData | null {
+ return this.#bridge.getGlyphVariationData(glyphName);
+ }
+
composites(glyphName: string): CompositeGlyph | null {
return this.#bridge.getGlyphCompositeComponents(glyphName) as CompositeGlyph | null;
}
diff --git a/apps/desktop/src/renderer/src/lib/model/Glyph.ts b/apps/desktop/src/renderer/src/lib/model/Glyph.ts
index 30a3ae4e..5330fe1f 100644
--- a/apps/desktop/src/renderer/src/lib/model/Glyph.ts
+++ b/apps/desktop/src/renderer/src/lib/model/Glyph.ts
@@ -522,6 +522,62 @@ export class Glyph {
}
}
+ /**
+ * @knipclassignore — used by VariationPanel for live interpolation
+ *
+ * Apply interpolated values from variation math.
+ *
+ * `values` order MUST match `flatten()` in crates/shift-core/src/interpolation.rs:
+ * [xAdvance, p0.x, p0.y, p1.x, p1.y, ..., a0.x, a0.y, ...]
+ *
+ * In-place patch — reuses Point/Contour/Anchor identities, fires per-contour
+ * signals via a single batch. No struct allocation tree, no JSON parse, no
+ * NAPI hop on the hot path.
+ *
+ * Length-checked at runtime to catch drift between Rust's flatten() walk and
+ * this one. Round-trip-tested in interpolate.test.ts (parity test ensures
+ * the values themselves are correct).
+ */
+ applyValues(values: Float64Array): void {
+ const contours = this.#contours.peek();
+ const anchors = this.#anchors.peek();
+
+ let expected = 1; // xAdvance
+ for (const c of contours) expected += c.points.length * 2;
+ expected += anchors.length * 2;
+
+ if (values.length !== expected) {
+ throw new Error(
+ `Glyph.applyValues: length mismatch — got ${values.length}, expected ${expected}. ` +
+ `flatten() in shift-core::interpolation may have drifted from this walk.`,
+ );
+ }
+
+ batch(() => {
+ let i = 0;
+ this.#xAdvance.set(values[i++]);
+
+ for (const contour of contours) {
+ contour._setPoints(
+ contour.points.map((pt) => {
+ const x = values[i++];
+ const y = values[i++];
+ return { ...pt, x, y };
+ }),
+ );
+ }
+ this.#contours.set([...contours]);
+
+ this.#anchors.set(
+ anchors.map((a) => {
+ const x = values[i++];
+ const y = values[i++];
+ return { ...a, x, y };
+ }),
+ );
+ });
+ }
+
/** Extract current reactive state as a plain snapshot (for undo, Rust sync). */
toSnapshot(): GlyphSnapshot {
return {
diff --git a/crates/shift-backends/src/designspace/mod.rs b/crates/shift-backends/src/designspace/mod.rs
new file mode 100644
index 00000000..df76ba27
--- /dev/null
+++ b/crates/shift-backends/src/designspace/mod.rs
@@ -0,0 +1,3 @@
+mod reader;
+
+pub use reader::DesignspaceReader;
diff --git a/crates/shift-backends/src/designspace/reader.rs b/crates/shift-backends/src/designspace/reader.rs
new file mode 100644
index 00000000..49e98897
--- /dev/null
+++ b/crates/shift-backends/src/designspace/reader.rs
@@ -0,0 +1,321 @@
+use crate::traits::FontReader;
+use crate::ufo::UfoReader;
+use norad::designspace::DesignSpaceDocument;
+use shift_ir::{Axis, Font, Layer, LayerId, Location, Source};
+use std::collections::HashMap;
+use std::path::Path;
+
+pub struct DesignspaceReader;
+
+impl DesignspaceReader {
+ pub fn new() -> Self {
+ Self
+ }
+}
+
+impl Default for DesignspaceReader {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl FontReader for DesignspaceReader {
+ fn load(&self, path: &str) -> Result {
+ let ds_path = Path::new(path);
+ let ds_dir = ds_path
+ .parent()
+ .ok_or_else(|| format!("Cannot determine directory of '{path}'"))?;
+
+ let doc = DesignSpaceDocument::load(ds_path)
+ .map_err(|e| format!("Failed to load designspace '{path}': {e}"))?;
+
+ if doc.sources.is_empty() {
+ return Err("Designspace has no sources".to_string());
+ }
+
+ let default_idx = find_default_source_index(&doc);
+
+ // Load the default source first to establish the base font.
+ let default_ds_source = &doc.sources[default_idx];
+ let default_ufo_path = ds_dir.join(&default_ds_source.filename);
+ let default_ufo_str = default_ufo_path
+ .to_str()
+ .ok_or_else(|| "Invalid UTF-8 in default UFO path".to_string())?;
+
+ let ufo_reader = UfoReader::new();
+ let mut font = ufo_reader.load(default_ufo_str)?;
+
+ if let Some(ref family) = default_ds_source.familyname {
+ font.metadata_mut().family_name = Some(family.clone());
+ }
+
+ // Add axes.
+ for ds_axis in &doc.axes {
+ let (minimum, maximum) = derive_axis_range(ds_axis);
+ let mut axis = Axis::new(
+ ds_axis.tag.clone(),
+ ds_axis.name.clone(),
+ minimum,
+ ds_axis.default as f64,
+ maximum,
+ );
+ axis.set_hidden(ds_axis.hidden);
+ font.add_axis(axis);
+ }
+
+ // 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);
+
+ // Cache loaded UFO fonts so we don't re-read the same file for support layers.
+ let mut ufo_cache: HashMap = HashMap::new();
+
+ // Load each non-default source.
+ for (idx, ds_source) in doc.sources.iter().enumerate() {
+ if idx == default_idx {
+ continue;
+ }
+
+ let ufo_path = ds_dir.join(&ds_source.filename);
+ let ufo_str = ufo_path
+ .to_str()
+ .ok_or_else(|| format!("Invalid UTF-8 in UFO path: {ufo_path:?}"))?
+ .to_string();
+
+ let source_font = match ufo_cache.get(&ufo_str) {
+ Some(f) => f,
+ None => {
+ let loaded = ufo_reader.load(&ufo_str)?;
+ ufo_cache.insert(ufo_str.clone(), loaded);
+ ufo_cache.get(&ufo_str).unwrap()
+ }
+ };
+
+ // 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(|| {
+ format!(
+ "Layer '{}' not found in '{}'",
+ layer_name, ds_source.filename
+ )
+ })?
+ }
+ None => source_font.default_layer_id(),
+ };
+
+ let name = source_name(ds_source, idx);
+ let layer = Layer::new(name.clone());
+ let layer_id = font.add_layer(layer);
+
+ // 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(existing_glyph) = font.glyph_mut(glyph_name) {
+ existing_glyph.set_layer(layer_id, source_layer.clone());
+ }
+ }
+ }
+
+ let location = location_from_dimensions(&ds_source.location, &doc);
+ font.add_source(Source::with_filename(
+ name,
+ location,
+ layer_id,
+ ds_source.filename.clone(),
+ ));
+ }
+
+ Ok(font)
+ }
+}
+
+fn source_name(source: &norad::designspace::Source, index: usize) -> String {
+ source
+ .name
+ .clone()
+ .or_else(|| source.stylename.clone())
+ .unwrap_or_else(|| format!("Source {index}"))
+}
+
+fn location_from_dimensions(
+ dimensions: &[norad::designspace::Dimension],
+ doc: &DesignSpaceDocument,
+) -> Location {
+ let mut location = Location::new();
+ for dim in dimensions {
+ let value = dim.xvalue.unwrap_or(0.0) as f64;
+ if let Some(axis) = doc.axes.iter().find(|a| a.name == dim.name) {
+ location.set(axis.tag.clone(), value);
+ }
+ }
+ location
+}
+
+fn find_default_source_index(doc: &DesignSpaceDocument) -> usize {
+ for (idx, source) in doc.sources.iter().enumerate() {
+ // Skip support layer sources.
+ if source.layer.is_some() {
+ continue;
+ }
+
+ let is_default = doc.axes.iter().all(|axis| {
+ source
+ .location
+ .iter()
+ .find(|d| d.name == axis.name)
+ .map(|d| {
+ let val = d.xvalue.unwrap_or(0.0);
+ (val - axis.default).abs() < 0.001
+ })
+ .unwrap_or(false)
+ });
+ if is_default {
+ return idx;
+ }
+ }
+ 0
+}
+
+fn find_layer_by_name(font: &Font, name: &str) -> Option {
+ font.layers()
+ .iter()
+ .find(|(_, layer)| layer.name() == name)
+ .map(|(&id, _)| id)
+}
+
+/// Derive (minimum, maximum) for an axis from norad's parsed designspace.
+///
+/// Designspace axis edge cases handled:
+/// - **Continuous** (both min/max present): use as-is.
+/// - **Discrete** (`values="0 1"` with no min/max attrs): min/max are the
+/// smallest/largest values in the list. e.g. `ital`, our `SLAB`.
+/// - **One-sided** (only min OR max specified): the missing side falls back
+/// to `default`. Common with slant axes (`min=-15, default=0, max=0`).
+/// - **Degenerate** (no min/max/values): all three collapse to default.
+fn derive_axis_range(ds_axis: &norad::designspace::Axis) -> (f64, f64) {
+ let values_range = || {
+ ds_axis
+ .values
+ .as_ref()
+ .filter(|v| !v.is_empty())
+ .map(|values| {
+ let min = values.iter().cloned().fold(f32::INFINITY, f32::min) as f64;
+ let max = values.iter().cloned().fold(f32::NEG_INFINITY, f32::max) as f64;
+ (min, max)
+ })
+ };
+
+ match (ds_axis.minimum, ds_axis.maximum) {
+ (Some(min), Some(max)) => (min as f64, max as f64),
+ (None, None) => values_range().unwrap_or((ds_axis.default as f64, ds_axis.default as f64)),
+ (Some(min), None) => (min as f64, ds_axis.default as f64),
+ (None, Some(max)) => (ds_axis.default as f64, max as f64),
+ }
+}
+
+#[cfg(test)]
+mod axis_range_tests {
+ use super::*;
+ use norad::designspace::Axis as DsAxis;
+
+ fn axis(min: Option, max: Option, default: f32, values: Option>) -> DsAxis {
+ DsAxis {
+ name: "test".into(),
+ tag: "TEST".into(),
+ minimum: min,
+ maximum: max,
+ default,
+ hidden: false,
+ values,
+ ..Default::default()
+ }
+ }
+
+ #[test]
+ fn continuous_uses_explicit_min_max() {
+ let a = axis(Some(100.0), Some(900.0), 400.0, None);
+ assert_eq!(derive_axis_range(&a), (100.0, 900.0));
+ }
+
+ #[test]
+ fn discrete_two_values_derives_range() {
+ // SLAB axis pattern:
+ let a = axis(None, None, 0.0, Some(vec![0.0, 1.0]));
+ assert_eq!(derive_axis_range(&a), (0.0, 1.0));
+ }
+
+ #[test]
+ fn discrete_three_values_derives_range_from_extremes() {
+ let a = axis(None, None, 1.0, Some(vec![0.5, 1.0, 1.5]));
+ assert_eq!(derive_axis_range(&a), (0.5, 1.5));
+ }
+
+ #[test]
+ fn discrete_unsorted_values_still_finds_extremes() {
+ let a = axis(None, None, 1.0, Some(vec![1.5, 0.5, 1.0]));
+ assert_eq!(derive_axis_range(&a), (0.5, 1.5));
+ }
+
+ #[test]
+ fn explicit_min_max_takes_precedence_over_values() {
+ let a = axis(Some(0.0), Some(2.0), 1.0, Some(vec![0.5, 1.5]));
+ assert_eq!(derive_axis_range(&a), (0.0, 2.0));
+ }
+
+ #[test]
+ fn one_sided_min_only_falls_back_to_default_for_max() {
+ // slant-like, half-spec'd: min=-15, default=0, no max attr
+ let a = axis(Some(-15.0), None, 0.0, None);
+ assert_eq!(derive_axis_range(&a), (-15.0, 0.0));
+ }
+
+ #[test]
+ fn one_sided_max_only_falls_back_to_default_for_min() {
+ let a = axis(None, Some(900.0), 400.0, None);
+ assert_eq!(derive_axis_range(&a), (400.0, 900.0));
+ }
+
+ #[test]
+ fn no_min_max_no_values_collapses_to_default() {
+ let a = axis(None, None, 400.0, None);
+ assert_eq!(derive_axis_range(&a), (400.0, 400.0));
+ }
+
+ #[test]
+ fn empty_values_list_collapses_to_default() {
+ let a = axis(None, None, 0.0, Some(vec![]));
+ assert_eq!(derive_axis_range(&a), (0.0, 0.0));
+ }
+
+ #[test]
+ fn asymmetric_default_at_minimum() {
+ // Older fonts where the Light is the default
+ let a = axis(Some(400.0), Some(900.0), 400.0, None);
+ assert_eq!(derive_axis_range(&a), (400.0, 900.0));
+ // The Axis itself should still normalise sensibly:
+ let axis = Axis::new("wght".into(), "Weight".into(), 400.0, 400.0, 900.0);
+ assert_eq!(axis.normalize(400.0), 0.0);
+ assert_eq!(axis.normalize(900.0), 1.0);
+ // Below default: range is zero, must return 0 (no negative ramp).
+ assert_eq!(axis.normalize(100.0), 0.0);
+ }
+
+ #[test]
+ fn asymmetric_one_sided_negative_axis() {
+ // slnt-like: min=-15, default=0, max=0
+ let axis = Axis::new("slnt".into(), "Slant".into(), -15.0, 0.0, 0.0);
+ assert_eq!(axis.normalize(0.0), 0.0);
+ assert_eq!(axis.normalize(-15.0), -1.0);
+ // Above default: range is zero, returns 0 (no positive ramp).
+ assert_eq!(axis.normalize(5.0), 0.0);
+ }
+}
diff --git a/crates/shift-backends/src/glyphs/reader.rs b/crates/shift-backends/src/glyphs/reader.rs
index 351f7218..b9f0032b 100644
--- a/crates/shift-backends/src/glyphs/reader.rs
+++ b/crates/shift-backends/src/glyphs/reader.rs
@@ -196,7 +196,10 @@ impl FontReader for GlyphsReader {
location.set(axis.tag.clone(), value.into_inner());
}
}
- font.add_source(Source::new(master.name.clone(), location, layer_id));
+ let source_id = font.add_source(Source::new(master.name.clone(), location, layer_id));
+ if master_idx == glyphs_font.default_master_idx {
+ font.set_default_source_id(source_id);
+ }
}
for glyph in glyphs_font.glyphs.values() {
diff --git a/crates/shift-backends/src/lib.rs b/crates/shift-backends/src/lib.rs
index f0019a39..92c9d740 100644
--- a/crates/shift-backends/src/lib.rs
+++ b/crates/shift-backends/src/lib.rs
@@ -1,3 +1,4 @@
+pub mod designspace;
pub mod glyphs;
mod traits;
pub mod ufo;
diff --git a/crates/shift-backends/src/ufo/reader.rs b/crates/shift-backends/src/ufo/reader.rs
index 3d8c6431..6937074a 100644
--- a/crates/shift-backends/src/ufo/reader.rs
+++ b/crates/shift-backends/src/ufo/reader.rs
@@ -239,8 +239,9 @@ impl FontReader for UfoReader {
font.metrics_mut().x_height = norad_font.font_info.x_height;
font.metrics_mut().italic_angle = norad_font.font_info.italic_angle;
+ 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().as_str() == "public.default" {
+ let layer_id = if layer.name() == &norad_default_layer_name {
default_layer_id
} else {
let new_layer = Layer::new(layer.name().to_string());
diff --git a/crates/shift-core/Cargo.toml b/crates/shift-core/Cargo.toml
index 0dcc6033..28cb1cab 100644
--- a/crates/shift-core/Cargo.toml
+++ b/crates/shift-core/Cargo.toml
@@ -9,6 +9,7 @@ crate-type = ["rlib"]
[dependencies]
bitflags = "2.9.1"
fontc = "0.2.0"
+fontdrasil = "0.4.0"
norad = "0.16.0"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0"
diff --git a/crates/shift-core/src/font_loader.rs b/crates/shift-core/src/font_loader.rs
index d0e88a88..f618efc3 100644
--- a/crates/shift-core/src/font_loader.rs
+++ b/crates/shift-core/src/font_loader.rs
@@ -2,6 +2,7 @@ use std::collections::HashMap;
use std::path::Path;
use crate::binary::BytesFontAdaptor;
+use shift_backends::designspace::DesignspaceReader;
use shift_backends::glyphs::GlyphsReader;
use shift_backends::ufo::UfoReader;
use shift_backends::FontReader;
@@ -11,6 +12,7 @@ use shift_ir::Font;
pub enum FontFormat {
Ufo,
Glyphs,
+ Designspace,
Ttf,
Otf,
}
@@ -22,6 +24,7 @@ pub trait FontAdaptor {
struct UfoFontAdaptor;
struct GlyphsFontAdaptor;
+struct DesignspaceFontAdaptor;
impl FontAdaptor for UfoFontAdaptor {
fn read_font(&self, path: &str) -> Result {
@@ -45,6 +48,16 @@ impl FontAdaptor for GlyphsFontAdaptor {
}
}
+impl FontAdaptor for DesignspaceFontAdaptor {
+ fn read_font(&self, path: &str) -> Result {
+ DesignspaceReader::new().load(path)
+ }
+
+ fn write_font(&self, _font: &Font, _path: &str) -> Result<(), String> {
+ Err("Designspace writing is not supported; save as .ufo instead".to_string())
+ }
+}
+
pub struct FontLoader {
adaptors: HashMap>,
}
@@ -60,6 +73,7 @@ fn format_from_extension(ext: &str) -> Result {
"ufo" => Ok(FontFormat::Ufo),
"glyphs" => Ok(FontFormat::Glyphs),
"glyphspackage" => Ok(FontFormat::Glyphs),
+ "designspace" => Ok(FontFormat::Designspace),
"ttf" => Ok(FontFormat::Ttf),
"otf" => Ok(FontFormat::Otf),
_ => Err(format!("Unsupported font format: {ext}")),
@@ -78,6 +92,7 @@ impl FontLoader {
let mut adaptors: HashMap> = HashMap::new();
adaptors.insert(FontFormat::Ufo, Box::new(UfoFontAdaptor));
adaptors.insert(FontFormat::Glyphs, Box::new(GlyphsFontAdaptor));
+ adaptors.insert(FontFormat::Designspace, Box::new(DesignspaceFontAdaptor));
adaptors.insert(FontFormat::Ttf, Box::new(BytesFontAdaptor));
adaptors.insert(FontFormat::Otf, Box::new(BytesFontAdaptor));
@@ -130,6 +145,14 @@ mod tests {
));
}
+ #[test]
+ fn supports_designspace_extension() {
+ assert!(matches!(
+ format_from_extension("designspace"),
+ Ok(FontFormat::Designspace)
+ ));
+ }
+
#[test]
fn extension_matching_is_case_insensitive() {
assert!(matches!(format_from_extension("UFO"), Ok(FontFormat::Ufo)));
@@ -138,5 +161,9 @@ mod tests {
Ok(FontFormat::Glyphs)
));
assert!(matches!(format_from_extension("OTF"), Ok(FontFormat::Otf)));
+ assert!(matches!(
+ format_from_extension("DESIGNSPACE"),
+ Ok(FontFormat::Designspace)
+ ));
}
}
diff --git a/crates/shift-core/src/interpolation.rs b/crates/shift-core/src/interpolation.rs
new file mode 100644
index 00000000..6f47c21f
--- /dev/null
+++ b/crates/shift-core/src/interpolation.rs
@@ -0,0 +1,191 @@
+use std::collections::{HashMap, HashSet};
+use std::str::FromStr;
+
+use fontdrasil::coords::NormalizedLocation;
+use fontdrasil::types::Tag;
+use fontdrasil::variations::VariationModel;
+use serde::{Deserialize, Serialize};
+use shift_ir::variation::to_fd_location;
+use shift_ir::Axis;
+use ts_rs::TS;
+
+use crate::snapshot::{AnchorSnapshot, ContourSnapshot, GlyphGeometry, MasterSnapshot};
+use crate::{Font, Glyph};
+
+#[derive(Debug, Clone, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export, export_to = "../../../packages/types/src/generated/")]
+pub struct AxisTent {
+ pub axis_tag: String,
+ pub lower: f64,
+ pub peak: f64,
+ pub upper: f64,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export, export_to = "../../../packages/types/src/generated/")]
+pub struct GlyphVariationData {
+ /// One entry per region. Inner = tents on the axes the region depends on.
+ pub regions: Vec>,
+ /// Same length as `regions`. Each entry = flat values matching `flatten()` order:
+ /// [xAdvance, p0.x, p0.y, ..., a0.x, a0.y, ...].
+ pub deltas: Vec>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export, export_to = "../../../packages/types/src/generated/")]
+pub struct SourceError {
+ #[ts(type = "number")]
+ pub source_index: usize,
+ pub source_name: String,
+ pub message: String,
+}
+
+fn flatten(geom: &GlyphGeometry) -> Vec {
+ let mut values = vec![geom.x_advance];
+ for contour in &geom.contours {
+ for point in &contour.points {
+ values.push(point.x);
+ values.push(point.y);
+ }
+ }
+ for anchor in &geom.anchors {
+ values.push(anchor.x);
+ values.push(anchor.y);
+ }
+ values
+}
+
+fn check_compatibility(a: &GlyphGeometry, b: &GlyphGeometry) -> Result<(), String> {
+ if a.contours.len() != b.contours.len() {
+ return Err(format!(
+ "contour count mismatch: {} vs {}",
+ a.contours.len(),
+ b.contours.len()
+ ));
+ }
+ for (i, (ca, cb)) in a.contours.iter().zip(b.contours.iter()).enumerate() {
+ if ca.points.len() != cb.points.len() {
+ return Err(format!(
+ "contour {} point count mismatch: {} vs {}",
+ i,
+ ca.points.len(),
+ cb.points.len()
+ ));
+ }
+ }
+ if a.anchors.len() != b.anchors.len() {
+ return Err(format!(
+ "anchor count mismatch: {} vs {}",
+ a.anchors.len(),
+ b.anchors.len()
+ ));
+ }
+ Ok(())
+}
+
+/// Build per-master snapshots of a single glyph.
+///
+/// Pure: no editing-session knowledge. Caller passes the `Glyph` it wants the
+/// snapshots from (could be the disk copy or an in-progress editing copy with
+/// the live session layer patched in — `shift-node` handles that detour).
+///
+/// Returns `None` if the font isn't variable or no source has a non-empty layer
+/// for this glyph.
+pub fn build_master_snapshots(font: &Font, glyph: &Glyph) -> Option> {
+ if !font.is_variable() {
+ return None;
+ }
+
+ let default_source_id = font.default_source_id();
+ let mut masters: Vec = Vec::new();
+
+ for source in font.sources() {
+ let layer = match glyph.layer(source.layer_id()) {
+ Some(l) if !l.contours().is_empty() => l,
+ _ => continue,
+ };
+
+ let contours: Vec = layer
+ .contours()
+ .values()
+ .filter(|c| !c.points().is_empty())
+ .map(ContourSnapshot::from)
+ .collect();
+
+ let anchors: Vec = layer.anchors_iter().map(AnchorSnapshot::from).collect();
+
+ masters.push(MasterSnapshot {
+ source_id: source.id().raw().to_string(),
+ source_name: source.name().to_string(),
+ is_default_source: default_source_id == Some(source.id()),
+ location: source.location().clone(),
+ geometry: GlyphGeometry {
+ x_advance: layer.width(),
+ contours,
+ anchors,
+ },
+ });
+ }
+
+ if masters.is_empty() {
+ None
+ } else {
+ Some(masters)
+ }
+}
+
+pub fn get_glyph_variation_data(
+ masters: &[MasterSnapshot],
+ axes: &[Axis],
+) -> Option {
+ let ordered_axes: Vec = axes
+ .iter()
+ .filter_map(|a| Tag::from_str(a.tag()).ok())
+ .collect();
+
+ let default_master = masters.iter().find(|master| master.is_default_source)?;
+
+ let mut errors = Vec::new();
+ let mut points: HashMap> = HashMap::new();
+ for (source_index, master) in masters.iter().enumerate() {
+ match check_compatibility(&master.geometry, &default_master.geometry) {
+ Ok(()) => {
+ let loc = to_fd_location(&master.location, axes);
+ points.insert(loc, flatten(&master.geometry));
+ }
+ Err(message) => {
+ errors.push(SourceError {
+ source_index,
+ source_name: master.source_name.clone(),
+ message,
+ });
+ }
+ }
+ }
+
+ let locations_set: HashSet = points.keys().cloned().collect();
+ let model = VariationModel::new(locations_set, ordered_axes);
+ let model_deltas = model.deltas::(&points).ok()?;
+
+ let regions: Vec> = model_deltas
+ .iter()
+ .map(|(region, _)| {
+ region
+ .iter()
+ .map(|(tag, tent)| AxisTent {
+ axis_tag: tag.to_string(),
+ lower: tent.min.into_inner().into_inner(),
+ peak: tent.peak.into_inner().into_inner(),
+ upper: tent.max.into_inner().into_inner(),
+ })
+ .collect()
+ })
+ .collect();
+
+ let deltas: Vec> = model_deltas.into_iter().map(|(_, d)| d).collect();
+
+ Some(GlyphVariationData { regions, deltas })
+}
diff --git a/crates/shift-core/src/lib.rs b/crates/shift-core/src/lib.rs
index f4d1aa0b..ae75a7dd 100644
--- a/crates/shift-core/src/lib.rs
+++ b/crates/shift-core/src/lib.rs
@@ -5,13 +5,14 @@ pub mod curve;
pub mod dependency_graph;
pub mod edit_session;
pub mod font_loader;
+pub mod interpolation;
pub mod snapshot;
pub mod vec2;
pub use shift_ir::{
- Anchor, AnchorId, BooleanOp, Contour, ContourId, CurveSegment, CurveSegmentIter, Font,
- FontMetadata, FontMetrics, Glyph, GlyphLayer, GlyphName, GuidelineId, LayerId, Point, PointId,
- PointType, Transform,
+ Anchor, AnchorId, Axis, BooleanOp, Contour, ContourId, CurveSegment, CurveSegmentIter, Font,
+ FontMetadata, FontMetrics, Glyph, GlyphLayer, GlyphName, GuidelineId, LayerId, Location, Point,
+ PointId, PointType, Source, SourceId, Transform,
};
pub use shift_backends::ufo::{UfoReader, UfoWriter};
diff --git a/crates/shift-core/src/snapshot.rs b/crates/shift-core/src/snapshot.rs
index 4e80a63f..30df70f5 100644
--- a/crates/shift-core/src/snapshot.rs
+++ b/crates/shift-core/src/snapshot.rs
@@ -1,7 +1,9 @@
use serde::{Deserialize, Serialize};
use ts_rs::TS;
-use crate::{edit_session::EditSession, Anchor, Contour, Point, PointId, PointType as IrPointType};
+use crate::{
+ edit_session::EditSession, Anchor, Contour, Location, Point, PointId, PointType as IrPointType,
+};
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
@@ -129,6 +131,26 @@ impl From<&Contour> for RenderContourSnapshot {
}
}
+#[derive(Debug, Clone, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export, export_to = "../../../packages/types/src/generated/")]
+pub struct GlyphGeometry {
+ #[ts(rename = "xAdvance")]
+ pub x_advance: f64,
+ pub contours: Vec,
+ pub anchors: Vec,
+}
+
+impl From<&GlyphSnapshot> for GlyphGeometry {
+ fn from(snap: &GlyphSnapshot) -> Self {
+ Self {
+ x_advance: snap.x_advance,
+ contours: snap.contours.clone(),
+ anchors: snap.anchors.clone(),
+ }
+ }
+}
+
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../../../packages/types/src/generated/")]
@@ -220,6 +242,17 @@ impl CommandResult {
}
}
+#[derive(Debug, Clone, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export, export_to = "../../../packages/types/src/generated/")]
+pub struct MasterSnapshot {
+ pub source_id: String,
+ pub source_name: String,
+ pub is_default_source: bool,
+ pub location: Location,
+ pub geometry: GlyphGeometry,
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/crates/shift-core/tests/font_loading.rs b/crates/shift-core/tests/font_loading.rs
index 6c773cc2..5864f4ab 100644
--- a/crates/shift-core/tests/font_loading.rs
+++ b/crates/shift-core/tests/font_loading.rs
@@ -483,3 +483,186 @@ fn test_otf_has_glyph_contours() {
"OTF glyph 'A' should have contours"
);
}
+
+// --- Variable font (multi-master .glyphs) tests ---
+
+fn mutatorsans_variable_glyphs_path() -> PathBuf {
+ fixtures_path().join("fonts/MutatorSansVariable.glyphs")
+}
+
+#[test]
+fn test_variable_glyphs_is_variable() {
+ let path = mutatorsans_variable_glyphs_path();
+ let loader = FontLoader::new();
+ let font = loader
+ .read_font(path.to_str().unwrap())
+ .expect("Failed to load variable .glyphs font");
+
+ assert!(font.is_variable(), "Multi-master font should be variable");
+}
+
+#[test]
+fn test_variable_glyphs_axes() {
+ let path = mutatorsans_variable_glyphs_path();
+ let loader = FontLoader::new();
+ let font = loader.read_font(path.to_str().unwrap()).unwrap();
+
+ let axes = font.axes();
+ assert_eq!(axes.len(), 1, "Should have 1 axis");
+ assert_eq!(axes[0].tag(), "wght");
+ assert_eq!(axes[0].name(), "Weight");
+ assert_eq!(axes[0].minimum(), 100.0);
+ assert_eq!(axes[0].maximum(), 900.0);
+ assert_eq!(axes[0].default(), 100.0);
+}
+
+#[test]
+fn test_variable_glyphs_sources() {
+ let path = mutatorsans_variable_glyphs_path();
+ let loader = FontLoader::new();
+ let font = loader.read_font(path.to_str().unwrap()).unwrap();
+
+ let sources = font.sources();
+ assert_eq!(sources.len(), 2, "Should have 2 sources (Light and Bold)");
+
+ let light = &sources[0];
+ assert_eq!(light.location().get("wght"), Some(100.0));
+
+ let bold = &sources[1];
+ assert_eq!(bold.location().get("wght"), Some(900.0));
+}
+
+#[test]
+fn test_variable_glyphs_glyph_has_multiple_layers() {
+ let path = mutatorsans_variable_glyphs_path();
+ let loader = FontLoader::new();
+ let font = loader.read_font(path.to_str().unwrap()).unwrap();
+
+ let glyph_a = font.glyph("A").expect("Glyph 'A' should exist");
+ assert_eq!(
+ glyph_a.layers().len(),
+ 2,
+ "Variable glyph 'A' should have 2 layers (one per master)"
+ );
+}
+
+#[test]
+fn test_variable_glyphs_masters_are_compatible() {
+ let path = mutatorsans_variable_glyphs_path();
+ let loader = FontLoader::new();
+ let font = loader.read_font(path.to_str().unwrap()).unwrap();
+
+ let glyph_a = font.glyph("A").expect("Glyph 'A' should exist");
+ let layers: Vec<_> = glyph_a.layers().values().collect();
+ assert_eq!(layers.len(), 2);
+
+ // Both layers should have the same number of contours
+ assert_eq!(
+ layers[0].contours().len(),
+ layers[1].contours().len(),
+ "Masters should have the same number of contours"
+ );
+
+ // Both outer contours should have the same number of points
+ let contour0_points: usize = layers[0]
+ .contours()
+ .values()
+ .map(|c| c.points().len())
+ .sum();
+ let contour1_points: usize = layers[1]
+ .contours()
+ .values()
+ .map(|c| c.points().len())
+ .sum();
+ assert_eq!(
+ contour0_points, contour1_points,
+ "Masters should have the same total point count"
+ );
+}
+
+// --- Designspace (.designspace) tests ---
+
+fn mutatorsans_designspace_path() -> PathBuf {
+ fixtures_path().join("fonts/mutatorsans-variable/MutatorSans.designspace")
+}
+
+#[test]
+fn test_designspace_loads() {
+ let path = mutatorsans_designspace_path();
+ let loader = FontLoader::new();
+ let font = loader
+ .read_font(path.to_str().unwrap())
+ .expect("Failed to load designspace");
+
+ assert!(font.is_variable(), "Designspace font should be variable");
+ assert!(
+ font.glyph_count() > 10,
+ "MutatorSans should have many glyphs"
+ );
+}
+
+#[test]
+fn test_designspace_axes() {
+ let path = mutatorsans_designspace_path();
+ let loader = FontLoader::new();
+ let font = loader.read_font(path.to_str().unwrap()).unwrap();
+
+ let axes = font.axes();
+ assert_eq!(axes.len(), 2, "MutatorSans has width + weight axes");
+ assert_eq!(axes[0].tag(), "wdth");
+ assert_eq!(axes[0].name(), "width");
+ assert_eq!(axes[0].minimum(), 0.0);
+ assert_eq!(axes[0].maximum(), 1000.0);
+ assert_eq!(axes[1].tag(), "wght");
+ assert_eq!(axes[1].name(), "weight");
+}
+
+#[test]
+fn test_designspace_sources() {
+ let path = mutatorsans_designspace_path();
+ let loader = FontLoader::new();
+ let font = loader.read_font(path.to_str().unwrap()).unwrap();
+
+ let sources = font.sources();
+ // 4 main masters + 3 support layer sources = 7
+ assert_eq!(
+ sources.len(),
+ 7,
+ "Should have 7 sources (4 masters + 3 support)"
+ );
+
+ // Default source (LightCondensed) at (0, 0)
+ let default = &sources[0];
+ assert_eq!(default.location().get("wdth"), Some(0.0));
+ assert_eq!(default.location().get("wght"), Some(0.0));
+ assert!(default.filename().is_some());
+}
+
+#[test]
+fn test_designspace_glyph_has_multiple_layers() {
+ let path = mutatorsans_designspace_path();
+ let loader = FontLoader::new();
+ let font = loader.read_font(path.to_str().unwrap()).unwrap();
+
+ let glyph_a = font.glyph("A").expect("Glyph 'A' should exist");
+ // At least 4 layers from the 4 main masters
+ assert!(
+ glyph_a.layers().len() >= 4,
+ "Glyph A should have at least 4 layers, got {}",
+ glyph_a.layers().len()
+ );
+}
+
+#[test]
+fn test_designspace_metadata_from_default_source() {
+ let path = mutatorsans_designspace_path();
+ let loader = FontLoader::new();
+ let font = loader.read_font(path.to_str().unwrap()).unwrap();
+
+ let metadata = font.metadata();
+ assert_eq!(
+ metadata.family_name.as_deref(),
+ Some("MutatorMathTest"),
+ "Family name should come from designspace source"
+ );
+}
diff --git a/crates/shift-core/tests/interpolation_parity.rs b/crates/shift-core/tests/interpolation_parity.rs
new file mode 100644
index 00000000..1234f0c1
--- /dev/null
+++ b/crates/shift-core/tests/interpolation_parity.rs
@@ -0,0 +1,206 @@
+//! Parity-test fixture writer.
+//!
+//! Loads the real MutatorSans designspace, builds variation data for a glyph,
+//! computes the expected interpolated values via fontdrasil at a known target,
+//! and writes a JSON fixture for the TS parity test (in
+//! `apps/desktop/src/renderer/src/lib/interpolation/interpolate.test.ts`) to
+//! read back and assert the TS port agrees to within f64 precision.
+//!
+//! Run: `cargo test -p shift-core --test interpolation_parity`.
+
+use std::collections::{BTreeMap, HashMap};
+use std::fs;
+use std::path::PathBuf;
+use std::str::FromStr;
+
+use fontdrasil::coords::NormalizedLocation;
+use fontdrasil::types::Tag;
+use fontdrasil::variations::VariationModel;
+use serde::Serialize;
+
+use shift_core::font_loader::FontLoader;
+use shift_core::interpolation::{
+ build_master_snapshots, get_glyph_variation_data, GlyphVariationData,
+};
+use shift_core::snapshot::GlyphGeometry;
+use shift_ir::variation::to_fd_location;
+use shift_ir::Location;
+
+fn workspace_root() -> PathBuf {
+ PathBuf::from(env!("CARGO_MANIFEST_DIR"))
+ .parent()
+ .unwrap()
+ .parent()
+ .unwrap()
+ .to_path_buf()
+}
+
+fn fixture_designspace() -> PathBuf {
+ workspace_root().join("fixtures/fonts/mutatorsans-variable/MutatorSans.designspace")
+}
+
+fn fixture_output() -> PathBuf {
+ workspace_root().join("packages/types/__fixtures__/variation_parity.json")
+}
+
+/// Local copy of the `flatten` walk used by `get_glyph_variation_data`.
+/// Kept private to the test so production `flatten` can stay private.
+/// Order MUST match shift-core::interpolation::flatten exactly.
+fn flatten_geometry(g: &GlyphGeometry) -> Vec {
+ let mut v = vec![g.x_advance];
+ for c in &g.contours {
+ for p in &c.points {
+ v.push(p.x);
+ v.push(p.y);
+ }
+ }
+ for a in &g.anchors {
+ v.push(a.x);
+ v.push(a.y);
+ }
+ v
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+struct MasterEntry {
+ source_name: String,
+ is_default_source: bool,
+ designspace_location: BTreeMap,
+ normalised_location: BTreeMap,
+ /// Flat values at this master, in `flatten()` order.
+ /// `interpolate(data, normalisedLocation)` must equal this within 1e-9.
+ expected: Vec,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+struct Fixture {
+ /// For diagnostics — which glyph we sampled.
+ glyph_name: String,
+
+ /// Mid-designspace target (for the headline parity assertion).
+ designspace_target: BTreeMap,
+ normalised_location: BTreeMap,
+
+ /// What the TS `interpolate()` consumes — same shape Rust ships over NAPI.
+ data: GlyphVariationData,
+
+ /// Ground truth from fontdrasil's `interpolate_from_deltas` at the mid target.
+ /// TS port must match within ~1e-9.
+ expected: Vec,
+
+ /// Per-master round-trip data: at each master's location, interpolation must
+ /// recover that master's exact flat values. Catches unpacking drift between
+ /// Rust's flatten() and TS's applyValues walk.
+ masters: Vec,
+}
+
+#[test]
+fn write_parity_fixture() {
+ let designspace = fixture_designspace();
+ let loader = FontLoader::new();
+ let font = loader
+ .read_font(designspace.to_str().unwrap())
+ .expect("load MutatorSans designspace");
+
+ // "A" is present in all four corner masters of MutatorSans — safe choice.
+ const GLYPH: &str = "A";
+ let glyph = font.glyph(GLYPH).expect("glyph A missing");
+
+ let masters = build_master_snapshots(&font, glyph).expect("not variable / no masters");
+ assert!(
+ masters.len() >= 4,
+ "expected ≥ 4 masters for A, got {}",
+ masters.len()
+ );
+
+ let axes = font.axes();
+ let data = get_glyph_variation_data(&masters, axes).expect("variation data");
+
+ // Pick a non-trivial target — middle of designspace on each axis.
+ let mut target = Location::new();
+ for axis in axes {
+ let mid = (axis.minimum() + axis.maximum()) / 2.0;
+ target.set(axis.tag().to_string(), mid);
+ }
+ let target_norm = to_fd_location(&target, axes);
+
+ // Compute expected via fontdrasil directly — this is the ground truth.
+ let ordered_axes: Vec = axes
+ .iter()
+ .filter_map(|a| Tag::from_str(a.tag()).ok())
+ .collect();
+
+ let mut points: HashMap> = HashMap::new(); // fontdrasil API takes HashMap
+ for m in &masters {
+ let loc = to_fd_location(&m.location, axes);
+ points.insert(loc, flatten_geometry(&m.geometry));
+ }
+ let model = VariationModel::new(points.keys().cloned().collect(), ordered_axes);
+ let model_deltas = model.deltas::(&points).expect("compute deltas");
+ let expected: Vec = model.interpolate_from_deltas(&target_norm, &model_deltas);
+
+ let designspace_target: BTreeMap = target
+ .iter()
+ .map(|(tag, value)| (tag.clone(), *value))
+ .collect();
+ let normalised_location: BTreeMap = target_norm
+ .iter()
+ .map(|(tag, coord)| (tag.to_string(), coord.into_inner().into_inner()))
+ .collect();
+
+ // Per-master round-trip data — at each master's location, interpolation
+ // must equal that master's flat values.
+ let master_entries: Vec = masters
+ .iter()
+ .map(|m| {
+ let m_norm = to_fd_location(&m.location, axes);
+ let m_expected = model.interpolate_from_deltas(&m_norm, &model_deltas);
+ MasterEntry {
+ source_name: m.source_name.clone(),
+ is_default_source: m.is_default_source,
+ designspace_location: m
+ .location
+ .iter()
+ .map(|(k, v)| (k.clone(), *v))
+ .collect::>(),
+ normalised_location: m_norm
+ .iter()
+ .map(|(t, c)| (t.to_string(), c.into_inner().into_inner()))
+ .collect::>(),
+ expected: m_expected,
+ }
+ })
+ .collect();
+
+ let fixture = Fixture {
+ glyph_name: GLYPH.to_string(),
+ designspace_target,
+ normalised_location,
+ data,
+ expected,
+ masters: master_entries,
+ };
+
+ let out = fixture_output();
+ // Trailing newline to match what pre-commit's end-of-file-fixer expects;
+ // without it the fixer would re-add it, breaking idempotency on re-runs.
+ let new_json = format!("{}\n", serde_json::to_string_pretty(&fixture).unwrap());
+
+ // Idempotent: only write when content actually changes. This keeps the
+ // fixture stable across CI runs and pre-commit hooks (which run cargo
+ // test) — without it, every run would rewrite the file and the hook
+ // would flag "files modified after staging."
+ let needs_write = match fs::read_to_string(&out) {
+ Ok(existing) => existing != new_json,
+ Err(_) => true,
+ };
+ if needs_write {
+ fs::create_dir_all(out.parent().unwrap()).expect("mkdir __fixtures__");
+ fs::write(&out, &new_json).expect("write fixture");
+ println!("wrote parity fixture → {}", out.display());
+ } else {
+ println!("parity fixture up to date ({})", out.display());
+ }
+}
diff --git a/crates/shift-core/tests/round_trip.rs b/crates/shift-core/tests/round_trip.rs
index d729795d..98fdaa5d 100644
--- a/crates/shift-core/tests/round_trip.rs
+++ b/crates/shift-core/tests/round_trip.rs
@@ -755,12 +755,11 @@ fn test_ufo_round_trip_layer_glyph_counts() {
}
}
- let reload_layer = reloaded
+ let (&reload_layer_id, _) = reloaded
.layers()
- .values()
- .find(|l| l.name() == orig_name)
+ .iter()
+ .find(|(_, l)| l.name() == orig_name)
.unwrap_or_else(|| panic!("Layer '{orig_name}' should exist"));
- let reload_layer_id = reload_layer.id();
let mut reload_glyph_count = 0;
for glyph in reloaded.glyphs().values() {
diff --git a/crates/shift-ir/Cargo.toml b/crates/shift-ir/Cargo.toml
index b3adb4a7..706c6f48 100644
--- a/crates/shift-ir/Cargo.toml
+++ b/crates/shift-ir/Cargo.toml
@@ -9,6 +9,8 @@ license = "MIT OR Apache-2.0"
crate-type = ["rlib"]
[dependencies]
+fontdrasil = "0.4.0"
+indexmap = { version = "2", features = ["serde"] }
kurbo = "0.13.0"
linesweeper = "0.3.0"
serde = { version = "1.0", features = ["derive"] }
diff --git a/crates/shift-ir/src/axis.rs b/crates/shift-ir/src/axis.rs
index 75cc3d20..d7b5bdfc 100644
--- a/crates/shift-ir/src/axis.rs
+++ b/crates/shift-ir/src/axis.rs
@@ -1,7 +1,10 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
+use ts_rs::TS;
-#[derive(Clone, Debug, Serialize, Deserialize)]
+#[derive(Clone, Debug, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export, export_to = "../../../packages/types/src/generated/")]
pub struct Axis {
tag: String,
name: String,
@@ -94,7 +97,8 @@ impl Axis {
}
}
-#[derive(Clone, Debug, Default, Serialize, Deserialize)]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, TS)]
+#[ts(export, export_to = "../../../packages/types/src/generated/")]
pub struct Location {
values: HashMap,
}
@@ -133,6 +137,13 @@ impl Location {
}
Location::from_map(normalized)
}
+
+ pub fn is_default_axis(&self, axes: &[Axis]) -> bool {
+ axes.iter().all(|axis| {
+ let value = self.get(axis.tag()).unwrap_or(axis.default());
+ (value - axis.default()).abs() < f64::EPSILON
+ })
+ }
}
#[cfg(test)]
diff --git a/crates/shift-ir/src/font.rs b/crates/shift-ir/src/font.rs
index 974f38a9..85d95a2f 100644
--- a/crates/shift-ir/src/font.rs
+++ b/crates/shift-ir/src/font.rs
@@ -1,5 +1,5 @@
use crate::axis::Axis;
-use crate::entity::LayerId;
+use crate::entity::{LayerId, SourceId};
use crate::features::FeatureData;
use crate::glyph::Glyph;
use crate::guideline::Guideline;
@@ -83,6 +83,8 @@ pub struct Font {
metrics: FontMetrics,
axes: Vec,
sources: Vec,
+ #[serde(default)]
+ default_source_id: Option,
layers: HashMap,
glyphs: HashMap,
kerning: KerningData,
@@ -103,6 +105,7 @@ impl Default for Font {
metrics: FontMetrics::default(),
axes: Vec::new(),
sources: Vec::new(),
+ default_source_id: None,
layers,
glyphs: HashMap::new(),
kerning: KerningData::new(),
@@ -147,8 +150,28 @@ impl Font {
&self.sources
}
- pub fn add_source(&mut self, source: Source) {
+ pub fn add_source(&mut self, source: Source) -> SourceId {
+ let source_id = source.id();
+ if self.default_source_id.is_none() {
+ self.default_source_id = Some(source_id);
+ }
self.sources.push(source);
+ source_id
+ }
+
+ pub fn default_source_id(&self) -> Option {
+ self.default_source_id
+ }
+
+ pub fn set_default_source_id(&mut self, source_id: SourceId) {
+ self.default_source_id = Some(source_id);
+ }
+
+ pub fn default_source(&self) -> Option<&Source> {
+ let default_source_id = self.default_source_id?;
+ self.sources
+ .iter()
+ .find(|source| source.id() == default_source_id)
}
pub fn is_variable(&self) -> bool {
diff --git a/crates/shift-ir/src/glyph.rs b/crates/shift-ir/src/glyph.rs
index 28f05fd5..6363c0e4 100644
--- a/crates/shift-ir/src/glyph.rs
+++ b/crates/shift-ir/src/glyph.rs
@@ -5,6 +5,7 @@ use crate::entity::{AnchorId, ComponentId, ContourId, GlyphId, LayerId};
use crate::guideline::Guideline;
use crate::lib_data::LibData;
use crate::GlyphName;
+use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@@ -21,7 +22,7 @@ pub struct Glyph {
pub struct GlyphLayer {
width: f64,
height: Option,
- contours: HashMap,
+ contours: IndexMap,
components: HashMap,
anchors: Vec,
guidelines: Vec,
@@ -56,7 +57,7 @@ impl GlyphLayer {
self.height = height;
}
- pub fn contours(&self) -> &HashMap {
+ pub fn contours(&self) -> &IndexMap {
&self.contours
}
@@ -79,7 +80,7 @@ impl GlyphLayer {
}
pub fn remove_contour(&mut self, id: ContourId) -> Option {
- self.contours.remove(&id)
+ self.contours.shift_remove(&id)
}
pub fn clear_contours(&mut self) {
diff --git a/crates/shift-ir/src/lib.rs b/crates/shift-ir/src/lib.rs
index a4e911c4..b22c658f 100644
--- a/crates/shift-ir/src/lib.rs
+++ b/crates/shift-ir/src/lib.rs
@@ -15,6 +15,7 @@ mod metrics;
mod point;
mod segment;
mod source;
+pub mod variation;
pub use anchor::Anchor;
pub use axis::{Axis, Location};
diff --git a/crates/shift-ir/src/source.rs b/crates/shift-ir/src/source.rs
index bbe49a6a..aa593e29 100644
--- a/crates/shift-ir/src/source.rs
+++ b/crates/shift-ir/src/source.rs
@@ -1,12 +1,17 @@
use crate::axis::Location;
use crate::entity::{LayerId, SourceId};
use serde::{Deserialize, Serialize};
+use ts_rs::TS;
-#[derive(Clone, Debug, Serialize, Deserialize)]
+#[derive(Clone, Debug, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export, export_to = "../../../packages/types/src/generated/")]
pub struct Source {
+ #[ts(type = "string")]
id: SourceId,
name: String,
location: Location,
+ #[ts(type = "string")]
layer_id: LayerId,
filename: Option,
}
diff --git a/crates/shift-ir/src/variation.rs b/crates/shift-ir/src/variation.rs
new file mode 100644
index 00000000..59851249
--- /dev/null
+++ b/crates/shift-ir/src/variation.rs
@@ -0,0 +1,24 @@
+use std::str::FromStr;
+
+use fontdrasil::{
+ coords::{NormalizedCoord, NormalizedLocation},
+ types::Tag,
+};
+
+use crate::{Axis, Location};
+
+pub fn to_fd_location(loc: &Location, axes: &[Axis]) -> NormalizedLocation {
+ let mut result = NormalizedLocation::new();
+
+ for axis in axes {
+ let value = loc.get(axis.tag()).unwrap_or(axis.default());
+ let n = axis.normalize(value);
+ let Ok(tag) = Tag::from_str(axis.tag()) else {
+ continue;
+ };
+
+ result.insert(tag, NormalizedCoord::new(n));
+ }
+
+ result
+}
diff --git a/crates/shift-node/__test__/font_integration.spec.mjs b/crates/shift-node/__test__/font_integration.spec.mjs
index d239cd23..e6d08828 100644
--- a/crates/shift-node/__test__/font_integration.spec.mjs
+++ b/crates/shift-node/__test__/font_integration.spec.mjs
@@ -23,7 +23,6 @@ function startEditSessionByUnicode(engine, unicode) {
describe("FontEngine Integration - UFO Loading", () => {
it("loads MutatorSans UFO successfully", () => {
if (!existsSync(MUTATORSANS_UFO)) {
- console.log("Skipping: MutatorSans UFO not found");
return;
}
@@ -232,7 +231,6 @@ describe("FontEngine Integration - Round Trip", () => {
describe("FontEngine Integration - TTF Loading", () => {
it("loads MutatorSans TTF successfully", () => {
if (!existsSync(MUTATORSANS_TTF)) {
- console.log("Skipping: MutatorSans TTF not found");
return;
}
@@ -499,3 +497,166 @@ describe("FontEngine Integration - Extended Round Trip", () => {
expect(reloadedTypes).toEqual(originalTypes);
});
});
+
+// --- Variable font (.glyphs with multiple masters) tests ---
+
+const MUTATORSANS_VARIABLE = join(FIXTURES_PATH, "fonts/MutatorSansVariable.glyphs");
+
+describe("FontEngine Integration - Variable Font (.glyphs)", () => {
+ it("detects variable font", () => {
+ const engine = new FontEngine();
+ engine.loadFont(MUTATORSANS_VARIABLE);
+ expect(engine.isVariable()).toBe(true);
+ });
+
+ it("returns axes", () => {
+ const engine = new FontEngine();
+ engine.loadFont(MUTATORSANS_VARIABLE);
+ const axes = JSON.parse(engine.getAxes());
+ expect(axes).toHaveLength(1);
+ expect(axes[0].tag).toBe("wght");
+ expect(axes[0].name).toBe("Weight");
+ expect(axes[0].minimum).toBe(100);
+ expect(axes[0].maximum).toBe(900);
+ expect(axes[0].default).toBe(100);
+ });
+
+ it("returns sources", () => {
+ const engine = new FontEngine();
+ engine.loadFont(MUTATORSANS_VARIABLE);
+ const sources = JSON.parse(engine.getSources());
+ expect(sources).toHaveLength(2);
+ expect(sources[0].location.values.wght).toBe(100);
+ expect(sources[1].location.values.wght).toBe(900);
+ });
+
+ it("returns master snapshots for glyph A", () => {
+ const engine = new FontEngine();
+ engine.loadFont(MUTATORSANS_VARIABLE);
+ const json = engine.getGlyphMasterSnapshots("A");
+ expect(json).not.toBeNull();
+ const masters = JSON.parse(json);
+ expect(masters).toHaveLength(2);
+
+ // Each master snapshot carries source metadata plus interpolation geometry.
+ for (const m of masters) {
+ expect(m).toHaveProperty("sourceId");
+ expect(m).toHaveProperty("sourceName");
+ expect(m).toHaveProperty("location");
+ expect(m).toHaveProperty("isDefaultSource");
+ expect(m).toHaveProperty("geometry");
+ expect(m).not.toHaveProperty("snapshot");
+ expect(m.geometry.contours).toHaveLength(2);
+ expect(m.geometry.xAdvance).toBeGreaterThan(0);
+ }
+
+ // Both masters should have matching total point counts
+ const lightTotal = masters[0].geometry.contours.reduce((s, c) => s + c.points.length, 0);
+ const boldTotal = masters[1].geometry.contours.reduce((s, c) => s + c.points.length, 0);
+ expect(lightTotal).toBe(boldTotal);
+ });
+
+ it("non-variable font returns isVariable false", () => {
+ const engine = new FontEngine();
+ engine.loadFont(MUTATORSANS_UFO);
+ expect(engine.isVariable()).toBe(false);
+ });
+
+ it("returns null for non-existent glyph master snapshots", () => {
+ const engine = new FontEngine();
+ engine.loadFont(MUTATORSANS_VARIABLE);
+ const json = engine.getGlyphMasterSnapshots("nonexistent");
+ expect(json).toBeNull();
+ });
+});
+
+// --- Designspace (.designspace) tests ---
+
+const MUTATORSANS_DESIGNSPACE = join(
+ FIXTURES_PATH,
+ "fonts/mutatorsans-variable/MutatorSans.designspace",
+);
+
+describe("FontEngine Integration - Designspace", () => {
+ it("loads designspace and detects variable font", () => {
+ const engine = new FontEngine();
+ engine.loadFont(MUTATORSANS_DESIGNSPACE);
+ expect(engine.isVariable()).toBe(true);
+ expect(engine.getGlyphCount()).toBeGreaterThan(10);
+ });
+
+ it("returns axes from designspace", () => {
+ const engine = new FontEngine();
+ engine.loadFont(MUTATORSANS_DESIGNSPACE);
+ const axes = JSON.parse(engine.getAxes());
+ expect(axes).toHaveLength(2);
+ expect(axes[0].tag).toBe("wdth");
+ expect(axes[0].minimum).toBe(0);
+ expect(axes[0].maximum).toBe(1000);
+ expect(axes[1].tag).toBe("wght");
+ });
+
+ it("returns sources from designspace", () => {
+ const engine = new FontEngine();
+ engine.loadFont(MUTATORSANS_DESIGNSPACE);
+ const sources = JSON.parse(engine.getSources());
+ // 4 main masters + 3 support layers
+ expect(sources).toHaveLength(7);
+ expect(sources[0].location.values.wdth).toBe(0);
+ expect(sources[0].location.values.wght).toBe(0);
+ });
+
+ it("returns master snapshots for glyph A", () => {
+ const engine = new FontEngine();
+ engine.loadFont(MUTATORSANS_DESIGNSPACE);
+ const json = engine.getGlyphMasterSnapshots("A");
+ expect(json).not.toBeNull();
+ const masters = JSON.parse(json);
+ expect(masters.length).toBeGreaterThanOrEqual(4);
+ expect(masters.filter((m) => m.isDefaultSource)).toHaveLength(1);
+ for (const m of masters) {
+ expect(m).toHaveProperty("geometry");
+ expect(m).not.toHaveProperty("snapshot");
+ expect(m.geometry.contours.length).toBeGreaterThan(0);
+ expect(m.geometry.xAdvance).toBeGreaterThan(0);
+ }
+ });
+
+ it("excludes empty contours from master snapshots", () => {
+ const engine = new FontEngine();
+ engine.loadFont(MUTATORSANS_DESIGNSPACE);
+ const json = engine.getGlyphMasterSnapshots("A");
+ const masters = JSON.parse(json);
+ for (const m of masters) {
+ for (const contour of m.geometry.contours) {
+ expect(contour.points.length).toBeGreaterThan(0);
+ }
+ }
+ });
+
+ it("returns consistent contour order across masters", () => {
+ const engine = new FontEngine();
+ engine.loadFont(MUTATORSANS_DESIGNSPACE);
+ const json = engine.getGlyphMasterSnapshots("A");
+ const masters = JSON.parse(json);
+
+ // All masters should have the same contour signature (point counts per contour)
+ const sigs = masters.map((m) => m.geometry.contours.map((c) => c.points.length).join(","));
+ const uniqueSigs = new Set(sigs);
+ expect(uniqueSigs.size).toBe(1);
+ });
+
+ it("returns master snapshots for the currently editing glyph", () => {
+ const engine = new FontEngine();
+ engine.loadFont(MUTATORSANS_DESIGNSPACE);
+ engine.startEditSession({ glyphName: "A", unicode: 65 });
+
+ // Glyph is taken from font during session, but snapshots should still work
+ const json = engine.getGlyphMasterSnapshots("A");
+ expect(json).not.toBeNull();
+ const masters = JSON.parse(json);
+ expect(masters.length).toBeGreaterThanOrEqual(4);
+
+ engine.endEditSession();
+ });
+});
diff --git a/crates/shift-node/index.d.ts b/crates/shift-node/index.d.ts
index c2525d2b..048a6d72 100644
--- a/crates/shift-node/index.d.ts
+++ b/crates/shift-node/index.d.ts
@@ -19,6 +19,12 @@ export declare class FontEngine {
getGlyphAdvanceByName(glyphName: string): number | null
getGlyphBboxByName(glyphName: string): Array | null
getGlyphCompositeComponents(glyphName: string): string | null
+ isVariable(): boolean
+ getAxes(): string
+ getSources(): string
+ /** Returns a JSON array of master snapshots for a glyph. */
+ getGlyphMasterSnapshots(glyphName: string): string | null
+ getGlyphVariationData(glyphName: string): string | null
startEditSession(glyphRef: JsGlyphRef): void
endEditSession(): void
hasEditSession(): boolean
diff --git a/crates/shift-node/src/font_engine.rs b/crates/shift-node/src/font_engine.rs
index 9b2f9a69..142af615 100644
--- a/crates/shift-node/src/font_engine.rs
+++ b/crates/shift-node/src/font_engine.rs
@@ -1,16 +1,17 @@
use napi::bindgen_prelude::*;
use napi::{Error, Result, Status};
use napi_derive::napi;
+use shift_core::interpolation::get_glyph_variation_data;
use shift_core::{
composite::{
flatten_component_contours_for_layer as flatten_component_contours, layer_bbox,
- layer_complexity, layer_to_svg_path, preferred_layer_for_glyph,
- resolve_component_instances_for_layer, resolved_to_render_contours, GlyphLayerProvider,
+ layer_to_svg_path, resolve_component_instances_for_layer, resolved_to_render_contours,
+ GlyphLayerProvider,
},
dependency_graph::DependencyGraph,
edit_session::EditSession,
font_loader::FontLoader,
- snapshot::{CommandResult, GlyphSnapshot, RenderContourSnapshot},
+ snapshot::{CommandResult, GlyphSnapshot, MasterSnapshot, RenderContourSnapshot},
AnchorId, BooleanOp, ContourId, Font, FontWriter, Glyph, GlyphLayer, GuidelineId, LayerId,
NodePositionUpdate, NodeRef, PasteContour, PointId, PointType, UfoWriter,
};
@@ -59,10 +60,8 @@ impl GlyphLayerProvider for EngineLayerProvider<'_> {
}
}
- self
- .font
- .glyph(glyph_name)
- .and_then(preferred_layer_for_glyph)
+ let glyph = self.font.glyph(glyph_name)?;
+ glyph.layer(self.font.default_layer_id())
}
}
@@ -233,6 +232,10 @@ impl FontEngine {
}
}
+ fn default_layer_for_glyph<'a>(&'a self, glyph: &'a Glyph) -> Option<&'a GlyphLayer> {
+ glyph.layer(self.font.default_layer_id())
+ }
+
fn editing_target_for_unicode(&self, unicode: u32) -> Option<(&str, &GlyphLayer)> {
if let Some(session) = &self.current_edit_session {
if session.unicode() == unicode {
@@ -249,7 +252,7 @@ impl FontEngine {
}
let glyph = self.font.glyph_by_unicode(unicode)?;
- let layer = preferred_layer_for_glyph(glyph)?;
+ let layer = self.default_layer_for_glyph(glyph)?;
composite_debug!(
"editing_target_for_unicode U+{:04X}: from font glyph='{}' (contours={}, components={}, anchors={})",
unicode,
@@ -269,7 +272,7 @@ impl FontEngine {
}
let glyph = self.font.glyph(glyph_name)?;
- let layer = preferred_layer_for_glyph(glyph)?;
+ let layer = self.default_layer_for_glyph(glyph)?;
Some((glyph.name(), layer))
}
@@ -280,10 +283,8 @@ impl FontEngine {
}
}
- self
- .font
- .glyph(glyph_name)
- .and_then(preferred_layer_for_glyph)
+ let glyph = self.font.glyph(glyph_name)?;
+ self.default_layer_for_glyph(glyph)
}
fn flatten_component_contours_for_layer(
@@ -481,6 +482,62 @@ impl FontEngine {
}))
}
+ #[napi]
+ pub fn is_variable(&self) -> bool {
+ self.font.is_variable()
+ }
+
+ #[napi]
+ pub fn get_axes(&self) -> String {
+ to_json(&self.font.axes())
+ }
+
+ #[napi]
+ pub fn get_sources(&self) -> String {
+ to_json(&self.font.sources())
+ }
+
+ /// Returns a JSON array of master snapshots for a glyph.
+ #[napi]
+ pub fn get_glyph_master_snapshots(&self, glyph_name: String) -> Option {
+ let masters = self.build_master_snapshots(&glyph_name)?;
+ Some(to_json(&masters))
+ }
+
+ #[napi]
+ pub fn get_glyph_variation_data(&self, glyph_name: String) -> Option {
+ let masters = self.build_master_snapshots(&glyph_name)?;
+ let axes = self.font.axes();
+ let variation_data = get_glyph_variation_data(&masters, axes)?;
+
+ Some(to_json(&variation_data))
+ }
+
+ fn build_master_snapshots(&self, glyph_name: &str) -> Option> {
+ let editing = self.editing_glyph_for(glyph_name);
+ let glyph: &Glyph = match &editing {
+ Some(g) => g,
+ None => self.font.glyph(glyph_name)?,
+ };
+ shift_core::interpolation::build_master_snapshots(&self.font, glyph)
+ }
+
+ /// If the glyph currently being edited is `glyph_name`, return a copy with the
+ /// in-progress session layer patched in. Otherwise `None` — caller falls back
+ /// to the disk copy.
+ fn editing_glyph_for(&self, glyph_name: &str) -> Option {
+ let editing = self.editing_glyph.as_ref()?;
+ if editing.name() != glyph_name {
+ return None;
+ }
+ let session = self.current_edit_session.as_ref()?;
+ let layer_id = *self.editing_layer_id.as_ref()?;
+
+ let mut temp = editing.clone();
+ temp.set_layer(layer_id, session.layer().clone());
+ Some(temp)
+ }
+
// ═══════════════════════════════════════════════════════════
// EDIT SESSIONS
// ═══════════════════════════════════════════════════════════
@@ -516,19 +573,16 @@ impl FontEngine {
glyph.layers().len(),
primary_unicode
);
- let (layer_id, layer) = glyph
- .layers()
- .iter()
- .max_by_key(|(_, l)| layer_complexity(l))
- .map(|(id, _)| *id)
- .and_then(|id| glyph.remove_layer(id).map(|l| (id, l)))
- .unwrap_or_else(|| (self.font.default_layer_id(), GlyphLayer::with_width(500.0)));
+ let default_layer_id = self.font.default_layer_id();
+ let layer = glyph
+ .remove_layer(default_layer_id)
+ .unwrap_or_else(|| GlyphLayer::with_width(500.0));
let edit_session = EditSession::new(glyph.name().to_string(), primary_unicode, layer);
self.current_edit_session = Some(edit_session);
self.editing_glyph = Some(glyph);
- self.editing_layer_id = Some(layer_id);
+ self.editing_layer_id = Some(default_layer_id);
Ok(())
}
diff --git a/fixtures/fonts/MutatorSansVariable.glyphs b/fixtures/fonts/MutatorSansVariable.glyphs
new file mode 100644
index 00000000..e5b09fe3
--- /dev/null
+++ b/fixtures/fonts/MutatorSansVariable.glyphs
@@ -0,0 +1,143 @@
+{
+.appVersion = "1225";
+familyName = MutatorSans;
+axes = (
+{
+name = Weight;
+tag = wght;
+}
+);
+fontMaster = (
+{
+ascender = 800;
+capHeight = 700;
+descender = -200;
+id = master-light;
+customName = Light;
+weightValue = 100;
+xHeight = 500;
+},
+{
+ascender = 800;
+capHeight = 700;
+descender = -200;
+id = master-bold;
+customName = Bold;
+weightValue = 900;
+xHeight = 500;
+}
+);
+glyphs = (
+{
+glyphname = A;
+unicode = 0041;
+layers = (
+{
+layerId = master-light;
+width = 500;
+paths = (
+{
+closed = 1;
+nodes = (
+"0 0 LINE",
+"200 700 LINE",
+"250 700 LINE",
+"450 0 LINE",
+"375 0 LINE",
+"325 200 LINE",
+"125 200 LINE",
+"75 0 LINE"
+);
+},
+{
+closed = 1;
+nodes = (
+"150 270 LINE",
+"300 270 LINE",
+"225 560 LINE"
+);
+}
+);
+},
+{
+layerId = master-bold;
+width = 600;
+paths = (
+{
+closed = 1;
+nodes = (
+"0 0 LINE",
+"180 700 LINE",
+"320 700 LINE",
+"500 0 LINE",
+"380 0 LINE",
+"330 200 LINE",
+"170 200 LINE",
+"120 0 LINE"
+);
+},
+{
+closed = 1;
+nodes = (
+"200 270 LINE",
+"300 270 LINE",
+"250 520 LINE"
+);
+}
+);
+}
+);
+},
+{
+glyphname = space;
+unicode = 0020;
+layers = (
+{
+layerId = master-light;
+width = 250;
+},
+{
+layerId = master-bold;
+width = 300;
+}
+);
+},
+{
+glyphname = I;
+unicode = 0049;
+layers = (
+{
+layerId = master-light;
+width = 200;
+paths = (
+{
+closed = 1;
+nodes = (
+"50 0 LINE",
+"50 700 LINE",
+"100 700 LINE",
+"100 0 LINE"
+);
+}
+);
+},
+{
+layerId = master-bold;
+width = 300;
+paths = (
+{
+closed = 1;
+nodes = (
+"50 0 LINE",
+"50 700 LINE",
+"200 700 LINE",
+"200 0 LINE"
+);
+}
+);
+}
+);
+}
+);
+unitsPerEm = 1000;
+}
diff --git a/fixtures/fonts/mutatorsans-variable/LICENSE b/fixtures/fonts/mutatorsans-variable/LICENSE
new file mode 100644
index 00000000..affdc0ee
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 Erik van Blokland
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSans.designspace b/fixtures/fonts/mutatorsans-variable/MutatorSans.designspace
new file mode 100644
index 00000000..fc76d7db
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSans.designspace
@@ -0,0 +1,325 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.letterror.designspaceEditor.previewLocation
+
+ weight
+ 146.73033142089844
+ width
+ 58.626346588134766
+
+ com.letterror.designspaceEditor.previewText
+ FISH
+ com.letterror.longboard.interactionSources
+
+
+ width
+ horizontal
+
+
+ weight
+ vertical
+
+
+ com.letterror.mathModelPref
+ previewMutatorMath
+ com.letterror.skateboard.interactionSources
+
+ horizontal
+
+ width
+
+ ignore
+
+ vertical
+
+ weight
+
+
+ com.letterror.skateboard.interestingLocation
+
+
+
+ weight
+ 775.609
+ width
+ 794.522
+
+ S1
+
+
+
+ weight
+ 855.549
+ width
+ 795.978
+
+ S2
+
+
+
+ weight
+ 1194.939375384999
+ width
+ 898.8087507107668
+
+ S3
+
+
+
+ weight
+ 161.67457510442006
+ width
+ 404.05720203707176
+
+ This is horrible
+
+
+
+ weight
+ 467.73409948979594
+ width
+ 538.7016581632644
+
+ Stem == 200?
+
+
+
+ weight
+ 658.597
+ width
+ 93.05199999999985
+
+ My_new_location
+
+
+ com.letterror.skateboard.previewLocation
+
+ weight
+ 177.33442834877678
+ width
+ 747.3306156281592
+
+ com.superpolator.data
+
+ axiscolors
+
+ weight
+
+ 0.5
+ 0.5
+ 0.5
+ 1.0
+
+ width
+
+ 0.5
+ 0.5
+ 0.5
+ 1.0
+
+
+ instancefolder
+ instances
+ lineInverted
+
+ lineStacked
+ sequence
+ lineViewFilled
+
+ previewtext
+ SUPER
+ snippets
+
+
+ designspaceEdit.notes
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/features.fea b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/features.fea
new file mode 100644
index 00000000..f5f829a8
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/features.fea
@@ -0,0 +1,3 @@
+# this is the feature from boldcondensed.
+languagesystem DFLT dflt;
+languagesystem latn dflt;
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/fontinfo.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/fontinfo.plist
new file mode 100644
index 00000000..eba66dec
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/fontinfo.plist
@@ -0,0 +1,68 @@
+
+
+
+
+ ascender
+ 800
+ capHeight
+ 800
+ copyright
+ License same as MutatorMath. BSD 3-clause. [test-token: A]
+ descender
+ -200
+ familyName
+ MutatorSans
+ guidelines
+
+ italicAngle
+ 0
+ openTypeNameLicense
+ License same as MutatorMath. BSD 3-clause. [test-token: A]
+ openTypeOS2VendorID
+ LTTR
+ postscriptBlueValues
+
+ -10
+ 0
+ 800
+ 810
+
+ postscriptDefaultWidthX
+ 500
+ postscriptFamilyBlues
+
+ postscriptFamilyOtherBlues
+
+ postscriptFontName
+ MutatorSans-BoldCondensed
+ postscriptOtherBlues
+
+ 500
+ 520
+
+ postscriptSlantAngle
+ 0
+ postscriptStemSnapH
+
+ postscriptStemSnapV
+
+ postscriptWindowsCharacterSet
+ 1
+ styleMapFamilyName
+
+ styleMapStyleName
+ regular
+ styleName
+ BoldCondensed
+ unitsPerEm
+ 1000
+ versionMajor
+ 1
+ versionMinor
+ 2
+ xHeight
+ 500
+ year
+ 2004
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs.background/S_.closed.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs.background/S_.closed.glif
new file mode 100644
index 00000000..12ce1bbc
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs.background/S_.closed.glif
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs.background/contents.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs.background/contents.plist
new file mode 100644
index 00000000..0ef1afc6
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs.background/contents.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ S.closed
+ S_.closed.glif
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs.background/layerinfo.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs.background/layerinfo.plist
new file mode 100644
index 00000000..7e385c7f
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs.background/layerinfo.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ color
+ 0.5,1,0,0.7
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/A_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/A_.glif
new file mode 100644
index 00000000..306d8ee3
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/A_.glif
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.typemytype.robofont.Image.Brightness
+ 0
+ com.typemytype.robofont.Image.Contrast
+ 1
+ com.typemytype.robofont.Image.Saturation
+ 1
+ com.typemytype.robofont.Image.Sharpness
+ 0.4
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/A_acute.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/A_acute.glif
new file mode 100644
index 00000000..f0415223
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/A_acute.glif
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+ public.markColor
+ 0.6567,0.6903,1,1
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/A_dieresis.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/A_dieresis.glif
new file mode 100644
index 00000000..7109f724
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/A_dieresis.glif
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+ public.markColor
+ 0.6567,0.6903,1,1
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/B_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/B_.glif
new file mode 100644
index 00000000..e22f432c
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/B_.glif
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/C_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/C_.glif
new file mode 100644
index 00000000..284dcf64
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/C_.glif
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.typemytype.robofont.Image.Brightness
+ 0
+ com.typemytype.robofont.Image.Contrast
+ 1
+ com.typemytype.robofont.Image.Saturation
+ 1
+ com.typemytype.robofont.Image.Sharpness
+ 0.4
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/D_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/D_.glif
new file mode 100644
index 00000000..a1da8180
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/D_.glif
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/E_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/E_.glif
new file mode 100644
index 00000000..d127056b
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/E_.glif
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.typemytype.robofont.Image.Brightness
+ 0
+ com.typemytype.robofont.Image.Contrast
+ 1
+ com.typemytype.robofont.Image.Saturation
+ 1
+ com.typemytype.robofont.Image.Sharpness
+ 0.4
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/F_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/F_.glif
new file mode 100644
index 00000000..8ed63e71
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/F_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/G_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/G_.glif
new file mode 100644
index 00000000..48fd1a19
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/G_.glif
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/H_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/H_.glif
new file mode 100644
index 00000000..e66768ac
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/H_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/I_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/I_.glif
new file mode 100644
index 00000000..718d4872
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/I_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/I_.narrow.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/I_.narrow.glif
new file mode 100644
index 00000000..d5643b85
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/I_.narrow.glif
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.typemytype.robofont.Image.Brightness
+ 0
+ com.typemytype.robofont.Image.Contrast
+ 1
+ com.typemytype.robofont.Image.Saturation
+ 1
+ com.typemytype.robofont.Image.Sharpness
+ 0.4
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/I_J_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/I_J_.glif
new file mode 100644
index 00000000..940e47c9
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/I_J_.glif
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/J_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/J_.glif
new file mode 100644
index 00000000..c4314c24
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/J_.glif
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/J_.narrow.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/J_.narrow.glif
new file mode 100644
index 00000000..7a338337
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/J_.narrow.glif
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/K_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/K_.glif
new file mode 100644
index 00000000..6ab9e08e
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/K_.glif
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/L_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/L_.glif
new file mode 100644
index 00000000..a42f84a9
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/L_.glif
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/M_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/M_.glif
new file mode 100644
index 00000000..fb238308
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/M_.glif
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/N_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/N_.glif
new file mode 100644
index 00000000..cb50a6d5
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/N_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/O_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/O_.glif
new file mode 100644
index 00000000..27def6a1
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/O_.glif
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/P_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/P_.glif
new file mode 100644
index 00000000..66ed04c7
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/P_.glif
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/Q_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/Q_.glif
new file mode 100644
index 00000000..dd1417eb
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/Q_.glif
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/R_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/R_.glif
new file mode 100644
index 00000000..952b71e0
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/R_.glif
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.typemytype.robofont.Image.Brightness
+ 0
+ com.typemytype.robofont.Image.Contrast
+ 1
+ com.typemytype.robofont.Image.Saturation
+ 1
+ com.typemytype.robofont.Image.Sharpness
+ 0.4
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/S_.closed.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/S_.closed.glif
new file mode 100644
index 00000000..d5e09976
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/S_.closed.glif
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.letterror.skateboard.navigator
+
+ location
+
+ weight
+ 1000.0
+ width
+ 108.00694056919657
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/S_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/S_.glif
new file mode 100644
index 00000000..a13b97f0
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/S_.glif
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.typemytype.robofont.Image.Brightness
+ 0
+ com.typemytype.robofont.Image.Contrast
+ 1
+ com.typemytype.robofont.Image.Saturation
+ 1
+ com.typemytype.robofont.Image.Sharpness
+ 0.4
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/T_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/T_.glif
new file mode 100644
index 00000000..3d3e2c8f
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/T_.glif
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/U_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/U_.glif
new file mode 100644
index 00000000..6752b492
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/U_.glif
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/V_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/V_.glif
new file mode 100644
index 00000000..01087515
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/V_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/W_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/W_.glif
new file mode 100644
index 00000000..77d4b763
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/W_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/X_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/X_.glif
new file mode 100644
index 00000000..ab7b8723
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/X_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/Y_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/Y_.glif
new file mode 100644
index 00000000..1a549a79
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/Y_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/Z_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/Z_.glif
new file mode 100644
index 00000000..7e544c78
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/Z_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/_notdef.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/_notdef.glif
new file mode 100644
index 00000000..7bd7a779
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/_notdef.glif
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ public.markColor
+ 0.6567,0.6903,1,1
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/acute.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/acute.glif
new file mode 100644
index 00000000..ce8be622
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/acute.glif
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/arrowdown.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/arrowdown.glif
new file mode 100644
index 00000000..015ae983
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/arrowdown.glif
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/arrowleft.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/arrowleft.glif
new file mode 100644
index 00000000..9e425897
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/arrowleft.glif
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/arrowright.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/arrowright.glif
new file mode 100644
index 00000000..2211aee8
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/arrowright.glif
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/arrowup.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/arrowup.glif
new file mode 100644
index 00000000..ca8b87bc
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/arrowup.glif
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/b.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/b.glif
new file mode 100644
index 00000000..16accedd
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/b.glif
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/c.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/c.glif
new file mode 100644
index 00000000..0b4b3ccf
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/c.glif
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/colon.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/colon.glif
new file mode 100644
index 00000000..8c164fcb
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/colon.glif
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/comma.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/comma.glif
new file mode 100644
index 00000000..c43e1904
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/comma.glif
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ public.markColor
+ 0,0.95,0.95,1
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/contents.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/contents.plist
new file mode 100644
index 00000000..45273dee
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/contents.plist
@@ -0,0 +1,104 @@
+
+
+
+
+ .notdef
+ _notdef.glif
+ A
+ A_.glif
+ Aacute
+ A_acute.glif
+ Adieresis
+ A_dieresis.glif
+ B
+ B_.glif
+ C
+ C_.glif
+ D
+ D_.glif
+ E
+ E_.glif
+ F
+ F_.glif
+ G
+ G_.glif
+ H
+ H_.glif
+ I
+ I_.glif
+ I.narrow
+ I_.narrow.glif
+ IJ
+ I_J_.glif
+ J
+ J_.glif
+ J.narrow
+ J_.narrow.glif
+ K
+ K_.glif
+ L
+ L_.glif
+ M
+ M_.glif
+ N
+ N_.glif
+ O
+ O_.glif
+ P
+ P_.glif
+ Q
+ Q_.glif
+ R
+ R_.glif
+ S
+ S_.glif
+ S.closed
+ S_.closed.glif
+ T
+ T_.glif
+ U
+ U_.glif
+ V
+ V_.glif
+ W
+ W_.glif
+ X
+ X_.glif
+ Y
+ Y_.glif
+ Z
+ Z_.glif
+ acute
+ acute.glif
+ arrowdown
+ arrowdown.glif
+ arrowleft
+ arrowleft.glif
+ arrowright
+ arrowright.glif
+ arrowup
+ arrowup.glif
+ colon
+ colon.glif
+ comma
+ comma.glif
+ dieresis
+ dieresis.glif
+ dot
+ dot.glif
+ period
+ period.glif
+ quotedblbase
+ quotedblbase.glif
+ quotedblleft
+ quotedblleft.glif
+ quotedblright
+ quotedblright.glif
+ quotesinglbase
+ quotesinglbase.glif
+ semicolon
+ semicolon.glif
+ space
+ space.glif
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/d.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/d.glif
new file mode 100644
index 00000000..8fc09610
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/d.glif
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/dieresis.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/dieresis.glif
new file mode 100644
index 00000000..52693962
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/dieresis.glif
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+ public.markColor
+ 0.6567,0.6903,1,1
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/dot.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/dot.glif
new file mode 100644
index 00000000..8db5cc99
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/dot.glif
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/layerinfo.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/layerinfo.plist
new file mode 100644
index 00000000..aeeb1b2a
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/layerinfo.plist
@@ -0,0 +1,13 @@
+
+
+
+
+ color
+ 1,0.75,0,0.7
+ lib
+
+ com.typemytype.robofont.segmentType
+ curve
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/period.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/period.glif
new file mode 100644
index 00000000..0a0c3be1
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/period.glif
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ public.markColor
+ 0,0.95,0.95,1
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/quotedblbase.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/quotedblbase.glif
new file mode 100644
index 00000000..d312caeb
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/quotedblbase.glif
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+ com.typemytype.robofont.Image.Brightness
+ 0
+ com.typemytype.robofont.Image.Contrast
+ 1
+ com.typemytype.robofont.Image.Saturation
+ 1
+ com.typemytype.robofont.Image.Sharpness
+ 0.4
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/quotedblleft.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/quotedblleft.glif
new file mode 100644
index 00000000..3e2676ee
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/quotedblleft.glif
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/quotedblright.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/quotedblright.glif
new file mode 100644
index 00000000..a65d160b
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/quotedblright.glif
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/quotesinglbase.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/quotesinglbase.glif
new file mode 100644
index 00000000..93c283a3
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/quotesinglbase.glif
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+ com.typemytype.robofont.Image.Brightness
+ 0
+ com.typemytype.robofont.Image.Contrast
+ 1
+ com.typemytype.robofont.Image.Saturation
+ 1
+ com.typemytype.robofont.Image.Sharpness
+ 0.4
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/semicolon.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/semicolon.glif
new file mode 100644
index 00000000..c912a8d1
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/semicolon.glif
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/space.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/space.glif
new file mode 100644
index 00000000..80c661f7
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/glyphs/space.glif
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/groups.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/groups.plist
new file mode 100644
index 00000000..7dc9fccd
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/groups.plist
@@ -0,0 +1,20 @@
+
+
+
+
+ public.kern1.@MMK_L_A
+
+ A
+
+ public.kern2.@MMK_R_A
+
+ A
+
+ testGroup
+
+ E
+ F
+ H
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/kerning.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/kerning.plist
new file mode 100644
index 00000000..f5fa18fa
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/kerning.plist
@@ -0,0 +1,210 @@
+
+
+
+
+ A
+
+ J
+ -20
+ O
+ -30
+ T
+ -70
+ U
+ -30
+ V
+ -50
+
+ B
+
+ A
+ -20
+ J
+ -50
+ O
+ -20
+ S
+ -10
+ T
+ -10
+ U
+ -20
+ V
+ -30
+
+ C
+
+ A
+ -20
+ J
+ -50
+ T
+ -20
+ V
+ -20
+
+ E
+
+ J
+ -20
+ T
+ -10
+ V
+ -10
+
+ F
+
+ A
+ -40
+ J
+ -80
+ O
+ -10
+ S
+ -20
+ U
+ -10
+ V
+ -10
+
+ G
+
+ J
+ -20
+ S
+ -10
+ T
+ -40
+ U
+ -10
+ V
+ -30
+
+ H
+
+ J
+ -30
+ S
+ -10
+ T
+ -10
+
+ J
+
+ J
+ -70
+
+ L
+
+ J
+ -20
+ O
+ -20
+ T
+ -110
+ U
+ -20
+ V
+ -60
+
+ O
+
+ A
+ -30
+ J
+ -60
+ S
+ -10
+ T
+ -30
+ V
+ -30
+
+ P
+
+ A
+ -50
+ J
+ -100
+ S
+ -10
+ T
+ -10
+ U
+ -10
+ V
+ -20
+
+ R
+
+ H
+ -10
+ J
+ -20
+ O
+ -30
+ S
+ -20
+ T
+ -30
+ U
+ -30
+ V
+ -40
+
+ S
+
+ A
+ -20
+ H
+ -20
+ J
+ -40
+ O
+ -10
+ S
+ -10
+ T
+ -30
+ U
+ -10
+ V
+ -30
+ W
+ -10
+
+ T
+
+ A
+ -65
+ H
+ -10
+ J
+ -130
+ O
+ -20
+
+ U
+
+ A
+ -30
+ J
+ -60
+ S
+ -10
+ V
+ -10
+
+ V
+
+ J
+ -100
+ O
+ -30
+ S
+ -20
+ U
+ -10
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/layercontents.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/layercontents.plist
new file mode 100644
index 00000000..e9a336b2
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/layercontents.plist
@@ -0,0 +1,14 @@
+
+
+
+
+
+ foreground
+ glyphs
+
+
+ background
+ glyphs.background
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/lib.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/lib.plist
new file mode 100644
index 00000000..f3fa5b5f
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/lib.plist
@@ -0,0 +1,229 @@
+
+
+
+
+ com.defcon.sortDescriptor
+
+
+ allowPseudoUnicode
+
+ type
+ cannedDesign
+
+
+ com.letterror.lightMeter.prefs
+
+ chunkSize
+ 5
+ diameter
+ 200
+ drawTail
+
+ invert
+
+ toolDiameter
+ 30
+ toolStyle
+ fluid
+
+ com.typemytype.robofont.background.layerStrokeColor
+
+ 0.0
+ 0.8
+ 0.2
+ 0.7
+
+ com.typemytype.robofont.compileSettings.autohint
+
+ com.typemytype.robofont.compileSettings.checkOutlines
+
+ com.typemytype.robofont.compileSettings.createDummyDSIG
+
+ com.typemytype.robofont.compileSettings.decompose
+
+ com.typemytype.robofont.compileSettings.generateFormat
+ 0
+ com.typemytype.robofont.compileSettings.releaseMode
+
+ com.typemytype.robofont.foreground.layerStrokeColor
+
+ 0.5
+ 0.0
+ 0.5
+ 0.7
+
+ com.typemytype.robofont.italicSlantOffset
+ 0
+ com.typemytype.robofont.segmentType
+ curve
+ com.typemytype.robofont.shouldAddPointsInSplineConversion
+ 1
+ com.typesupply.defcon.sortDescriptor
+
+
+ ascending
+
+ space
+ A
+ B
+ C
+ D
+ E
+ F
+ G
+ H
+ I
+ J
+ K
+ L
+ M
+ N
+ O
+ P
+ Q
+ R
+ S
+ T
+ U
+ V
+ W
+ X
+ Y
+ Z
+ a
+ b
+ c
+ d
+ e
+ f
+ g
+ h
+ i
+ j
+ k
+ l
+ m
+ n
+ ntilde
+ o
+ p
+ q
+ r
+ s
+ t
+ u
+ v
+ w
+ x
+ y
+ z
+ zcaron
+ zero
+ one
+ two
+ three
+ four
+ five
+ six
+ seven
+ eight
+ nine
+ underscore
+ hyphen
+ endash
+ emdash
+ parenleft
+ parenright
+ bracketleft
+ bracketright
+ braceleft
+ braceright
+ numbersign
+ percent
+ period
+ comma
+ colon
+ semicolon
+ exclam
+ question
+ slash
+ backslash
+ bar
+ at
+ ampersand
+ paragraph
+ bullet
+ dollar
+ trademark
+ fi
+ fl
+ .notdef
+ a_b_c
+ Atilde
+ Adieresis
+ Acircumflex
+ Aring
+ Ccedilla
+ Agrave
+ Aacute
+ quotedblright
+ quotedblleft
+
+ type
+ glyphList
+
+
+ public.glyphOrder
+
+ space
+ A
+ Aacute
+ Adieresis
+ B
+ C
+ D
+ E
+ F
+ G
+ H
+ I
+ J
+ K
+ L
+ M
+ N
+ O
+ P
+ Q
+ R
+ S
+ T
+ U
+ V
+ W
+ X
+ Y
+ Z
+ IJ
+ quotedblleft
+ quotedblright
+ quotesinglbase
+ quotedblbase
+ period
+ comma
+ colon
+ semicolon
+ dot
+ acute
+ dieresis
+ arrowdown
+ arrowleft
+ arrowright
+ arrowup
+ I.narrow
+ J.narrow
+ S.closed
+ .notdef
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/metainfo.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/metainfo.plist
new file mode 100644
index 00000000..7b8b34ac
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldCondensed.ufo/metainfo.plist
@@ -0,0 +1,10 @@
+
+
+
+
+ creator
+ com.github.fonttools.ufoLib
+ formatVersion
+ 3
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/features.fea b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/features.fea
new file mode 100644
index 00000000..70ab5da3
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/features.fea
@@ -0,0 +1,3 @@
+# this is the feature from BoldWide
+languagesystem DFLT dflt;
+languagesystem latn dflt;
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/fontinfo.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/fontinfo.plist
new file mode 100644
index 00000000..760fc3da
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/fontinfo.plist
@@ -0,0 +1,68 @@
+
+
+
+
+ ascender
+ 800
+ capHeight
+ 800
+ copyright
+ License same as MutatorMath. BSD 3-clause. [test-token: B]
+ descender
+ -200
+ familyName
+ MutatorSans
+ guidelines
+
+ italicAngle
+ 0
+ openTypeNameLicense
+ License same as MutatorMath. BSD 3-clause. [test-token: B]
+ openTypeOS2VendorID
+ LTTR
+ postscriptBlueValues
+
+ -10
+ 0
+ 800
+ 810
+
+ postscriptDefaultWidthX
+ 500
+ postscriptFamilyBlues
+
+ postscriptFamilyOtherBlues
+
+ postscriptFontName
+ MutatorSans-BoldWide
+ postscriptOtherBlues
+
+ 400
+ 420
+
+ postscriptSlantAngle
+ 0
+ postscriptStemSnapH
+
+ postscriptStemSnapV
+
+ postscriptWindowsCharacterSet
+ 1
+ styleMapFamilyName
+
+ styleMapStyleName
+ regular
+ styleName
+ BoldWide
+ unitsPerEm
+ 1000
+ versionMajor
+ 1
+ versionMinor
+ 2
+ xHeight
+ 500
+ year
+ 2004
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs.background/S_.closed.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs.background/S_.closed.glif
new file mode 100644
index 00000000..71c82094
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs.background/S_.closed.glif
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs.background/S_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs.background/S_.glif
new file mode 100644
index 00000000..5e0ed4d8
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs.background/S_.glif
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs.background/contents.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs.background/contents.plist
new file mode 100644
index 00000000..701b513a
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs.background/contents.plist
@@ -0,0 +1,10 @@
+
+
+
+
+ S
+ S_.glif
+ S.closed
+ S_.closed.glif
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs.background/layerinfo.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs.background/layerinfo.plist
new file mode 100644
index 00000000..7e385c7f
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs.background/layerinfo.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ color
+ 0.5,1,0,0.7
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs.crayon/S_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs.crayon/S_.glif
new file mode 100644
index 00000000..3e708495
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs.crayon/S_.glif
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs.crayon/contents.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs.crayon/contents.plist
new file mode 100644
index 00000000..41c1aaf9
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs.crayon/contents.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ S
+ S_.glif
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs.crayon/layerinfo.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs.crayon/layerinfo.plist
new file mode 100644
index 00000000..321e50a8
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs.crayon/layerinfo.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ color
+ 0,1,0.25,0.7
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/A_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/A_.glif
new file mode 100644
index 00000000..58f75c00
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/A_.glif
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/A_acute.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/A_acute.glif
new file mode 100644
index 00000000..d220f280
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/A_acute.glif
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+ public.markColor
+ 0.6567,0.6903,1,1
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/A_dieresis.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/A_dieresis.glif
new file mode 100644
index 00000000..7862d2f1
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/A_dieresis.glif
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+ public.markColor
+ 0.6567,0.6903,1,1
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/B_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/B_.glif
new file mode 100644
index 00000000..70bc5a7c
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/B_.glif
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.typemytype.robofont.Image.Brightness
+ 0
+ com.typemytype.robofont.Image.Contrast
+ 1
+ com.typemytype.robofont.Image.Saturation
+ 1
+ com.typemytype.robofont.Image.Sharpness
+ 0.4
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/C_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/C_.glif
new file mode 100644
index 00000000..92eea0f1
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/C_.glif
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/D_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/D_.glif
new file mode 100644
index 00000000..5a50cb1a
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/D_.glif
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/E_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/E_.glif
new file mode 100644
index 00000000..1330903b
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/E_.glif
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/F_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/F_.glif
new file mode 100644
index 00000000..c62c5a01
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/F_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/G_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/G_.glif
new file mode 100644
index 00000000..5f1b1813
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/G_.glif
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/H_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/H_.glif
new file mode 100644
index 00000000..f0a95e47
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/H_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/I_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/I_.glif
new file mode 100644
index 00000000..a013c998
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/I_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/I_.narrow.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/I_.narrow.glif
new file mode 100644
index 00000000..111c8b61
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/I_.narrow.glif
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.typemytype.robofont.Image.Brightness
+ 0
+ com.typemytype.robofont.Image.Contrast
+ 1
+ com.typemytype.robofont.Image.Saturation
+ 1
+ com.typemytype.robofont.Image.Sharpness
+ 0.4
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/I_J_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/I_J_.glif
new file mode 100644
index 00000000..072b87d8
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/I_J_.glif
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/J_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/J_.glif
new file mode 100644
index 00000000..1e41faac
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/J_.glif
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/J_.narrow.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/J_.narrow.glif
new file mode 100644
index 00000000..393a0310
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/J_.narrow.glif
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/K_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/K_.glif
new file mode 100644
index 00000000..73f5683c
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/K_.glif
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/L_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/L_.glif
new file mode 100644
index 00000000..bc9a4d94
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/L_.glif
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/M_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/M_.glif
new file mode 100644
index 00000000..c16bdd6a
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/M_.glif
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/N_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/N_.glif
new file mode 100644
index 00000000..ab808746
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/N_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/O_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/O_.glif
new file mode 100644
index 00000000..acd36a1b
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/O_.glif
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/P_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/P_.glif
new file mode 100644
index 00000000..1cc7ce1f
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/P_.glif
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/Q_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/Q_.glif
new file mode 100644
index 00000000..99f124b3
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/Q_.glif
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/R_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/R_.glif
new file mode 100644
index 00000000..74b245b1
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/R_.glif
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/S_.closed.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/S_.closed.glif
new file mode 100644
index 00000000..0796d29e
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/S_.closed.glif
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.typemytype.robofont.Image.Brightness
+ 0
+ com.typemytype.robofont.Image.Contrast
+ 1
+ com.typemytype.robofont.Image.Saturation
+ 1
+ com.typemytype.robofont.Image.Sharpness
+ 0.4
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/S_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/S_.glif
new file mode 100644
index 00000000..c64d01f7
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/S_.glif
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.typemytype.robofont.Image.Brightness
+ 0
+ com.typemytype.robofont.Image.Contrast
+ 1
+ com.typemytype.robofont.Image.Saturation
+ 1
+ com.typemytype.robofont.Image.Sharpness
+ 0.4
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/T_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/T_.glif
new file mode 100644
index 00000000..36a192c8
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/T_.glif
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/U_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/U_.glif
new file mode 100644
index 00000000..fcbb5284
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/U_.glif
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/V_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/V_.glif
new file mode 100644
index 00000000..21a3bbef
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/V_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/W_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/W_.glif
new file mode 100644
index 00000000..6d07876a
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/W_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/X_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/X_.glif
new file mode 100644
index 00000000..0cdb44c4
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/X_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/Y_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/Y_.glif
new file mode 100644
index 00000000..a3175182
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/Y_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/Z_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/Z_.glif
new file mode 100644
index 00000000..3b9ef90e
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/Z_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/_notdef.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/_notdef.glif
new file mode 100644
index 00000000..7bd7a779
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/_notdef.glif
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ public.markColor
+ 0.6567,0.6903,1,1
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/acute.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/acute.glif
new file mode 100644
index 00000000..a61baaec
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/acute.glif
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/arrowdown.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/arrowdown.glif
new file mode 100644
index 00000000..e3c8cd28
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/arrowdown.glif
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/arrowleft.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/arrowleft.glif
new file mode 100644
index 00000000..764a5b40
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/arrowleft.glif
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/arrowright.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/arrowright.glif
new file mode 100644
index 00000000..e107939b
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/arrowright.glif
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/arrowup.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/arrowup.glif
new file mode 100644
index 00000000..f5dacf21
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/arrowup.glif
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/colon.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/colon.glif
new file mode 100644
index 00000000..91e80845
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/colon.glif
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/comma.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/comma.glif
new file mode 100644
index 00000000..b26d079b
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/comma.glif
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/contents.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/contents.plist
new file mode 100644
index 00000000..45273dee
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/contents.plist
@@ -0,0 +1,104 @@
+
+
+
+
+ .notdef
+ _notdef.glif
+ A
+ A_.glif
+ Aacute
+ A_acute.glif
+ Adieresis
+ A_dieresis.glif
+ B
+ B_.glif
+ C
+ C_.glif
+ D
+ D_.glif
+ E
+ E_.glif
+ F
+ F_.glif
+ G
+ G_.glif
+ H
+ H_.glif
+ I
+ I_.glif
+ I.narrow
+ I_.narrow.glif
+ IJ
+ I_J_.glif
+ J
+ J_.glif
+ J.narrow
+ J_.narrow.glif
+ K
+ K_.glif
+ L
+ L_.glif
+ M
+ M_.glif
+ N
+ N_.glif
+ O
+ O_.glif
+ P
+ P_.glif
+ Q
+ Q_.glif
+ R
+ R_.glif
+ S
+ S_.glif
+ S.closed
+ S_.closed.glif
+ T
+ T_.glif
+ U
+ U_.glif
+ V
+ V_.glif
+ W
+ W_.glif
+ X
+ X_.glif
+ Y
+ Y_.glif
+ Z
+ Z_.glif
+ acute
+ acute.glif
+ arrowdown
+ arrowdown.glif
+ arrowleft
+ arrowleft.glif
+ arrowright
+ arrowright.glif
+ arrowup
+ arrowup.glif
+ colon
+ colon.glif
+ comma
+ comma.glif
+ dieresis
+ dieresis.glif
+ dot
+ dot.glif
+ period
+ period.glif
+ quotedblbase
+ quotedblbase.glif
+ quotedblleft
+ quotedblleft.glif
+ quotedblright
+ quotedblright.glif
+ quotesinglbase
+ quotesinglbase.glif
+ semicolon
+ semicolon.glif
+ space
+ space.glif
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/dieresis.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/dieresis.glif
new file mode 100644
index 00000000..67e2f61b
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/dieresis.glif
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+ public.markColor
+ 0.6567,0.6903,1,1
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/dot.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/dot.glif
new file mode 100644
index 00000000..8db5cc99
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/dot.glif
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/layerinfo.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/layerinfo.plist
new file mode 100644
index 00000000..aeeb1b2a
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/layerinfo.plist
@@ -0,0 +1,13 @@
+
+
+
+
+ color
+ 1,0.75,0,0.7
+ lib
+
+ com.typemytype.robofont.segmentType
+ curve
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/period.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/period.glif
new file mode 100644
index 00000000..e00bdfa1
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/period.glif
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/quotedblbase.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/quotedblbase.glif
new file mode 100644
index 00000000..cc87c1ce
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/quotedblbase.glif
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/quotedblleft.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/quotedblleft.glif
new file mode 100644
index 00000000..aee8eba1
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/quotedblleft.glif
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/quotedblright.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/quotedblright.glif
new file mode 100644
index 00000000..54e4dbe1
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/quotedblright.glif
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/quotesinglbase.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/quotesinglbase.glif
new file mode 100644
index 00000000..65a94da6
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/quotesinglbase.glif
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/semicolon.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/semicolon.glif
new file mode 100644
index 00000000..3d21bb11
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/semicolon.glif
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/space.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/space.glif
new file mode 100644
index 00000000..80c661f7
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/glyphs/space.glif
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/groups.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/groups.plist
new file mode 100644
index 00000000..7dc9fccd
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/groups.plist
@@ -0,0 +1,20 @@
+
+
+
+
+ public.kern1.@MMK_L_A
+
+ A
+
+ public.kern2.@MMK_R_A
+
+ A
+
+ testGroup
+
+ E
+ F
+ H
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/images/image.png b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/images/image.png
new file mode 100644
index 00000000..1168dae6
Binary files /dev/null and b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/images/image.png differ
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/kerning.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/kerning.plist
new file mode 100644
index 00000000..1d469e77
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/kerning.plist
@@ -0,0 +1,11 @@
+
+
+
+
+ T
+
+ public.kern2.@MMK_R_A
+ -150
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/layercontents.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/layercontents.plist
new file mode 100644
index 00000000..e9a336b2
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/layercontents.plist
@@ -0,0 +1,14 @@
+
+
+
+
+
+ foreground
+ glyphs
+
+
+ background
+ glyphs.background
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/lib.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/lib.plist
new file mode 100644
index 00000000..1a869874
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/lib.plist
@@ -0,0 +1,355 @@
+
+
+
+
+ com.defcon.sortDescriptor
+
+
+ allowPseudoUnicode
+
+ type
+ cannedDesign
+
+
+ com.letterror.lightMeter.prefs
+
+ chunkSize
+ 5
+ diameter
+ 200
+ drawTail
+
+ invert
+
+ toolDiameter
+ 30
+ toolStyle
+ fluid
+
+ com.typemytype.robofont.background.layerStrokeColor
+
+ 0.0
+ 0.8
+ 0.2
+ 0.7
+
+ com.typemytype.robofont.compileSettings.autohint
+
+ com.typemytype.robofont.compileSettings.checkOutlines
+
+ com.typemytype.robofont.compileSettings.createDummyDSIG
+
+ com.typemytype.robofont.compileSettings.decompose
+
+ com.typemytype.robofont.compileSettings.generateFormat
+ 0
+ com.typemytype.robofont.compileSettings.releaseMode
+
+ com.typemytype.robofont.foreground.layerStrokeColor
+
+ 0.5
+ 0.0
+ 0.5
+ 0.7
+
+ com.typemytype.robofont.italicSlantOffset
+ 0
+ com.typemytype.robofont.segmentType
+ curve
+ com.typemytype.robofont.shouldAddPointsInSplineConversion
+ 1
+ com.typesupply.defcon.sortDescriptor
+
+
+ ascending
+
+ space
+ A
+ Agrave
+ Aacute
+ Acircumflex
+ Atilde
+ Adieresis
+ Aring
+ B
+ C
+ Ccedilla
+ D
+ E
+ Egrave
+ Eacute
+ Ecircumflex
+ Edieresis
+ F
+ G
+ H
+ I
+ Igrave
+ Iacute
+ Icircumflex
+ Idieresis
+ J
+ K
+ L
+ M
+ N
+ Ntilde
+ O
+ Ograve
+ Oacute
+ Ocircumflex
+ Otilde
+ Odieresis
+ P
+ Q
+ R
+ S
+ Scaron
+ T
+ U
+ Ugrave
+ Uacute
+ Ucircumflex
+ Udieresis
+ V
+ W
+ X
+ Y
+ Yacute
+ Ydieresis
+ Z
+ Zcaron
+ AE
+ Eth
+ Oslash
+ Thorn
+ Lslash
+ OE
+ a
+ agrave
+ aacute
+ acircumflex
+ atilde
+ adieresis
+ aring
+ b
+ c
+ ccedilla
+ d
+ e
+ egrave
+ eacute
+ ecircumflex
+ edieresis
+ f
+ g
+ h
+ i
+ igrave
+ iacute
+ icircumflex
+ idieresis
+ j
+ k
+ l
+ m
+ n
+ ntilde
+ o
+ ograve
+ oacute
+ ocircumflex
+ otilde
+ odieresis
+ p
+ q
+ r
+ s
+ scaron
+ t
+ u
+ ugrave
+ uacute
+ ucircumflex
+ udieresis
+ v
+ w
+ x
+ y
+ yacute
+ ydieresis
+ z
+ zcaron
+ ordfeminine
+ ordmasculine
+ germandbls
+ ae
+ eth
+ oslash
+ thorn
+ dotlessi
+ lslash
+ oe
+ zero
+ one
+ two
+ three
+ four
+ five
+ six
+ seven
+ eight
+ nine
+ onesuperior
+ twosuperior
+ threesuperior
+ onequarter
+ onehalf
+ threequarters
+ underscore
+ hyphen
+ endash
+ emdash
+ parenleft
+ parenright
+ bracketleft
+ bracketright
+ braceleft
+ braceright
+ numbersign
+ percent
+ perthousand
+ quotesingle
+ quotedbl
+ quoteleft
+ quoteright
+ quotedblleft
+ quotedblright
+ quotesinglbase
+ quotedblbase
+ guilsinglleft
+ guilsinglright
+ guillemotleft
+ guillemotright
+ asterisk
+ dagger
+ daggerdbl
+ period
+ comma
+ colon
+ semicolon
+ ellipsis
+ exclam
+ exclamdown
+ question
+ questiondown
+ slash
+ backslash
+ fraction
+ bar
+ brokenbar
+ at
+ ampersand
+ section
+ paragraph
+ periodcentered
+ bullet
+ plus
+ minus
+ plusminus
+ divide
+ multiply
+ equal
+ less
+ greater
+ logicalnot
+ mu
+ dollar
+ cent
+ sterling
+ currency
+ yen
+ Euro
+ florin
+ asciicircum
+ asciitilde
+ acute
+ grave
+ hungarumlaut
+ circumflex
+ caron
+ breve
+ tilde
+ macron
+ dieresis
+ dotaccent
+ ring
+ cedilla
+ ogonek
+ copyright
+ registered
+ trademark
+ degree
+ fi
+ fl
+ .notdef
+ a_b_c
+
+ type
+ glyphList
+
+
+ public.glyphOrder
+
+ space
+ A
+ Aacute
+ Adieresis
+ B
+ C
+ D
+ E
+ F
+ G
+ H
+ I
+ J
+ K
+ L
+ M
+ N
+ O
+ P
+ Q
+ R
+ S
+ T
+ U
+ V
+ W
+ X
+ Y
+ Z
+ quotedblleft
+ quotedblright
+ quotesinglbase
+ quotedblbase
+ period
+ comma
+ colon
+ semicolon
+ dot
+ acute
+ dieresis
+ IJ
+ arrowdown
+ arrowleft
+ arrowright
+ arrowup
+ I.narrow
+ J.narrow
+ S.closed
+ .notdef
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/metainfo.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/metainfo.plist
new file mode 100644
index 00000000..7b8b34ac
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansBoldWide.ufo/metainfo.plist
@@ -0,0 +1,10 @@
+
+
+
+
+ creator
+ com.github.fonttools.ufoLib
+ formatVersion
+ 3
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/features.fea b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/features.fea
new file mode 100644
index 00000000..81b14ace
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/features.fea
@@ -0,0 +1,4 @@
+# this is the feature from lightCondensed
+# Hi_this_is_the_feature.
+languagesystem DFLT dflt;
+languagesystem latn dflt;
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/fontinfo.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/fontinfo.plist
new file mode 100644
index 00000000..d8dfd6fd
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/fontinfo.plist
@@ -0,0 +1,67 @@
+
+
+
+
+ ascender
+ 700
+ capHeight
+ 700
+ copyright
+ License same as MutatorMath. BSD 3-clause. [test-token: C]
+ descender
+ -200
+ familyName
+ MutatorSans
+ guidelines
+
+ italicAngle
+ 0
+ openTypeNameLicense
+ License same as MutatorMath. BSD 3-clause. [test-token: C]
+ openTypeOS2VendorID
+ LTTR
+ postscriptBlueValues
+
+ -10
+ 0
+ 700
+ 710
+
+ postscriptDefaultWidthX
+ 500
+ postscriptFamilyBlues
+
+ postscriptFamilyOtherBlues
+
+ postscriptFontName
+ MutatorMathTest-LightCondensed
+ postscriptFullName
+ MutatorMathTest LightCondensed
+ postscriptOtherBlues
+
+ postscriptSlantAngle
+ 0
+ postscriptStemSnapH
+
+ postscriptStemSnapV
+
+ postscriptWindowsCharacterSet
+ 1
+ styleMapFamilyName
+
+ styleMapStyleName
+ regular
+ styleName
+ LightCondensed
+ unitsPerEm
+ 1000
+ versionMajor
+ 1
+ versionMinor
+ 2
+ xHeight
+ 500
+ year
+ 2004
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.background/S_.closed.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.background/S_.closed.glif
new file mode 100644
index 00000000..260a24c2
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.background/S_.closed.glif
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.background/S_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.background/S_.glif
new file mode 100644
index 00000000..ea1a7c6a
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.background/S_.glif
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.letterror.skateboard.navigator
+
+ location
+
+ weight
+ 820.0
+ width
+ 1000
+
+
+ com.typemytype.robofont.Image.Brightness
+ 0
+ com.typemytype.robofont.Image.Contrast
+ 1
+ com.typemytype.robofont.Image.Saturation
+ 1
+ com.typemytype.robofont.Image.Sharpness
+ 0.4
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.background/contents.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.background/contents.plist
new file mode 100644
index 00000000..701b513a
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.background/contents.plist
@@ -0,0 +1,10 @@
+
+
+
+
+ S
+ S_.glif
+ S.closed
+ S_.closed.glif
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.background/layerinfo.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.background/layerinfo.plist
new file mode 100644
index 00000000..f3d8faef
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.background/layerinfo.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ color
+ 0,1,1,0.7
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.S_.middle/S_.closed.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.S_.middle/S_.closed.glif
new file mode 100644
index 00000000..16ae9f0b
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.S_.middle/S_.closed.glif
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.letterror.skateboard.navigator
+
+ location
+
+ weight
+ 700.0
+ width
+ 569.078
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.S_.middle/contents.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.S_.middle/contents.plist
new file mode 100644
index 00000000..0ef1afc6
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.S_.middle/contents.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ S.closed
+ S_.closed.glif
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.S_.middle/layerinfo.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.S_.middle/layerinfo.plist
new file mode 100644
index 00000000..ea0e39cd
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.S_.middle/layerinfo.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ color
+ 0.5,0,1,0.7
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.S_.wide/S_.closed.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.S_.wide/S_.closed.glif
new file mode 100644
index 00000000..c33656eb
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.S_.wide/S_.closed.glif
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.letterror.skateboard.navigator
+
+ location
+
+ weight
+ 673.7998527960526
+ width
+ 1000.0
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.S_.wide/S_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.S_.wide/S_.glif
new file mode 100644
index 00000000..2abba517
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.S_.wide/S_.glif
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.letterror.skateboard.navigator
+
+ location
+
+ weight
+ 759.5997715404774
+ width
+ 1000.0
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.S_.wide/contents.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.S_.wide/contents.plist
new file mode 100644
index 00000000..701b513a
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.S_.wide/contents.plist
@@ -0,0 +1,10 @@
+
+
+
+
+ S
+ S_.glif
+ S.closed
+ S_.closed.glif
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.S_.wide/layerinfo.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.S_.wide/layerinfo.plist
new file mode 100644
index 00000000..9aafa333
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.S_.wide/layerinfo.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ color
+ 0,0.25,1,0.7
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.crossbar/B_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.crossbar/B_.glif
new file mode 100644
index 00000000..3f24fb62
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.crossbar/B_.glif
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.letterror.skateboard.navigator
+
+ location
+
+ weight
+ 715.728
+ width
+ -138.956
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.crossbar/E_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.crossbar/E_.glif
new file mode 100644
index 00000000..ba5e5896
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.crossbar/E_.glif
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.letterror.skateboard.navigator
+
+ location
+
+ weight
+ 715.728
+ width
+ -138.956
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.crossbar/F_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.crossbar/F_.glif
new file mode 100644
index 00000000..32fc9ad7
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.crossbar/F_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.crossbar/G_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.crossbar/G_.glif
new file mode 100644
index 00000000..446a8ddf
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.crossbar/G_.glif
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.letterror.skateboard.navigator
+
+ location
+
+ weight
+ 715.728
+ width
+ -138.956
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.crossbar/contents.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.crossbar/contents.plist
new file mode 100644
index 00000000..fc4dc072
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.crossbar/contents.plist
@@ -0,0 +1,14 @@
+
+
+
+
+ B
+ B_.glif
+ E
+ E_.glif
+ F
+ F_.glif
+ G
+ G_.glif
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.crossbar/layerinfo.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.crossbar/layerinfo.plist
new file mode 100644
index 00000000..321e50a8
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support.crossbar/layerinfo.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ color
+ 0,1,0.25,0.7
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support/A_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support/A_.glif
new file mode 100644
index 00000000..935df41d
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support/A_.glif
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+ com.letterror.skateboard.navigator
+
+ location
+
+ space
+ 0.0
+ weight
+ 600.0
+ width
+ 500.0
+
+
+ com.typemytype.robofont.Image.Brightness
+ 0
+ com.typemytype.robofont.Image.Contrast
+ 1
+ com.typemytype.robofont.Image.Saturation
+ 1
+ com.typemytype.robofont.Image.Sharpness
+ 0.4
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support/S_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support/S_.glif
new file mode 100644
index 00000000..4aad20f6
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support/S_.glif
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.letterror.skateboard.navigator
+
+ location
+
+ space
+ 25.0
+ weight
+ 707.6485770089287
+ width
+ 181.31051199776795
+
+
+ com.typemytype.robofont.Image.Brightness
+ 0
+ com.typemytype.robofont.Image.Contrast
+ 1
+ com.typemytype.robofont.Image.Saturation
+ 1
+ com.typemytype.robofont.Image.Sharpness
+ 0.4
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support/W_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support/W_.glif
new file mode 100644
index 00000000..06a4b755
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support/W_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support/contents.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support/contents.plist
new file mode 100644
index 00000000..eb200c68
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support/contents.plist
@@ -0,0 +1,12 @@
+
+
+
+
+ A
+ A_.glif
+ S
+ S_.glif
+ W
+ W_.glif
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support/layerinfo.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support/layerinfo.plist
new file mode 100644
index 00000000..5ab4d3bc
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs.support/layerinfo.plist
@@ -0,0 +1,13 @@
+
+
+
+
+ color
+ 0,1,0.25,0.7
+ lib
+
+ com.typemytype.robofont.segmentType
+ curve
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/A_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/A_.glif
new file mode 100644
index 00000000..a0de1496
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/A_.glif
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/A_acute.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/A_acute.glif
new file mode 100644
index 00000000..70645b1e
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/A_acute.glif
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+ public.markColor
+ 0.6567,0.6903,1,1
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/A_dieresis.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/A_dieresis.glif
new file mode 100644
index 00000000..7b0e7d97
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/A_dieresis.glif
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+ public.markColor
+ 0.6567,0.6903,1,1
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/B_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/B_.glif
new file mode 100644
index 00000000..b93e9a9f
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/B_.glif
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/C_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/C_.glif
new file mode 100644
index 00000000..46b3537b
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/C_.glif
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/D_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/D_.glif
new file mode 100644
index 00000000..6c02122d
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/D_.glif
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/E_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/E_.glif
new file mode 100644
index 00000000..a9b71bf6
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/E_.glif
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/F_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/F_.glif
new file mode 100644
index 00000000..b9579326
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/F_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/G_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/G_.glif
new file mode 100644
index 00000000..525fcc4b
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/G_.glif
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/H_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/H_.glif
new file mode 100644
index 00000000..e9431fda
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/H_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/I_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/I_.glif
new file mode 100644
index 00000000..58937754
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/I_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/I_.narrow.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/I_.narrow.glif
new file mode 100644
index 00000000..6ad5aa8a
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/I_.narrow.glif
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.typemytype.robofont.Image.Brightness
+ 0
+ com.typemytype.robofont.Image.Contrast
+ 1
+ com.typemytype.robofont.Image.Saturation
+ 1
+ com.typemytype.robofont.Image.Sharpness
+ 0.4
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/I_J_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/I_J_.glif
new file mode 100644
index 00000000..72c2c50a
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/I_J_.glif
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/J_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/J_.glif
new file mode 100644
index 00000000..5347fe26
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/J_.glif
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/J_.narrow.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/J_.narrow.glif
new file mode 100644
index 00000000..8b094c92
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/J_.narrow.glif
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/K_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/K_.glif
new file mode 100644
index 00000000..e871b0f9
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/K_.glif
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/L_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/L_.glif
new file mode 100644
index 00000000..b8d8ce46
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/L_.glif
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/M_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/M_.glif
new file mode 100644
index 00000000..e78048d7
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/M_.glif
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/N_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/N_.glif
new file mode 100644
index 00000000..28771ffb
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/N_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/O_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/O_.glif
new file mode 100644
index 00000000..04bd4642
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/O_.glif
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/P_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/P_.glif
new file mode 100644
index 00000000..5c13beba
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/P_.glif
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/Q_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/Q_.glif
new file mode 100644
index 00000000..17503c26
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/Q_.glif
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/R_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/R_.glif
new file mode 100644
index 00000000..8f0b2c9c
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/R_.glif
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/S_.closed.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/S_.closed.glif
new file mode 100644
index 00000000..880efcdb
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/S_.closed.glif
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/S_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/S_.glif
new file mode 100644
index 00000000..a642d2c7
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/S_.glif
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.typemytype.robofont.Image.Brightness
+ 0
+ com.typemytype.robofont.Image.Contrast
+ 1
+ com.typemytype.robofont.Image.Saturation
+ 1
+ com.typemytype.robofont.Image.Sharpness
+ 0.4
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/T_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/T_.glif
new file mode 100644
index 00000000..b4674b21
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/T_.glif
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/U_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/U_.glif
new file mode 100644
index 00000000..b2f892f7
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/U_.glif
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/V_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/V_.glif
new file mode 100644
index 00000000..0c97296c
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/V_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/W_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/W_.glif
new file mode 100644
index 00000000..89434d65
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/W_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/X_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/X_.glif
new file mode 100644
index 00000000..38bb129c
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/X_.glif
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/Y_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/Y_.glif
new file mode 100644
index 00000000..a42b73c5
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/Y_.glif
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.typemytype.robofont.Image.Brightness
+ 0
+ com.typemytype.robofont.Image.Contrast
+ 1
+ com.typemytype.robofont.Image.Saturation
+ 1
+ com.typemytype.robofont.Image.Sharpness
+ 0.4
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/Z_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/Z_.glif
new file mode 100644
index 00000000..e9f15351
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/Z_.glif
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/_notdef.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/_notdef.glif
new file mode 100644
index 00000000..7bd7a779
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/_notdef.glif
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ public.markColor
+ 0.6567,0.6903,1,1
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/acute.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/acute.glif
new file mode 100644
index 00000000..f6b2297f
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/acute.glif
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/arrowdown.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/arrowdown.glif
new file mode 100644
index 00000000..7cc92fd1
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/arrowdown.glif
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ public.markColor
+ 0,0.95,0.95,1
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/arrowleft.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/arrowleft.glif
new file mode 100644
index 00000000..e8395a8c
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/arrowleft.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ public.markColor
+ 0,0.95,0.95,1
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/arrowright.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/arrowright.glif
new file mode 100644
index 00000000..93e92c10
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/arrowright.glif
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ public.markColor
+ 0,0.95,0.95,1
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/arrowup.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/arrowup.glif
new file mode 100644
index 00000000..13f3a01e
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/arrowup.glif
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ public.markColor
+ 0,0.95,0.95,1
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/b.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/b.glif
new file mode 100644
index 00000000..16accedd
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/b.glif
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/c.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/c.glif
new file mode 100644
index 00000000..0b4b3ccf
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/c.glif
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/colon.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/colon.glif
new file mode 100644
index 00000000..f9c7f8a7
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/colon.glif
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/comma.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/comma.glif
new file mode 100644
index 00000000..df2b5a61
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/comma.glif
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/contents.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/contents.plist
new file mode 100644
index 00000000..45273dee
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/contents.plist
@@ -0,0 +1,104 @@
+
+
+
+
+ .notdef
+ _notdef.glif
+ A
+ A_.glif
+ Aacute
+ A_acute.glif
+ Adieresis
+ A_dieresis.glif
+ B
+ B_.glif
+ C
+ C_.glif
+ D
+ D_.glif
+ E
+ E_.glif
+ F
+ F_.glif
+ G
+ G_.glif
+ H
+ H_.glif
+ I
+ I_.glif
+ I.narrow
+ I_.narrow.glif
+ IJ
+ I_J_.glif
+ J
+ J_.glif
+ J.narrow
+ J_.narrow.glif
+ K
+ K_.glif
+ L
+ L_.glif
+ M
+ M_.glif
+ N
+ N_.glif
+ O
+ O_.glif
+ P
+ P_.glif
+ Q
+ Q_.glif
+ R
+ R_.glif
+ S
+ S_.glif
+ S.closed
+ S_.closed.glif
+ T
+ T_.glif
+ U
+ U_.glif
+ V
+ V_.glif
+ W
+ W_.glif
+ X
+ X_.glif
+ Y
+ Y_.glif
+ Z
+ Z_.glif
+ acute
+ acute.glif
+ arrowdown
+ arrowdown.glif
+ arrowleft
+ arrowleft.glif
+ arrowright
+ arrowright.glif
+ arrowup
+ arrowup.glif
+ colon
+ colon.glif
+ comma
+ comma.glif
+ dieresis
+ dieresis.glif
+ dot
+ dot.glif
+ period
+ period.glif
+ quotedblbase
+ quotedblbase.glif
+ quotedblleft
+ quotedblleft.glif
+ quotedblright
+ quotedblright.glif
+ quotesinglbase
+ quotesinglbase.glif
+ semicolon
+ semicolon.glif
+ space
+ space.glif
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/d.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/d.glif
new file mode 100644
index 00000000..8fc09610
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/d.glif
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/dieresis.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/dieresis.glif
new file mode 100644
index 00000000..765ef4c5
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/dieresis.glif
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+ public.markColor
+ 0.6567,0.6903,1,1
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/dot.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/dot.glif
new file mode 100644
index 00000000..8f14d475
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/dot.glif
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/layerinfo.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/layerinfo.plist
new file mode 100644
index 00000000..aeeb1b2a
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/layerinfo.plist
@@ -0,0 +1,13 @@
+
+
+
+
+ color
+ 1,0.75,0,0.7
+ lib
+
+ com.typemytype.robofont.segmentType
+ curve
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/period.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/period.glif
new file mode 100644
index 00000000..76242499
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/period.glif
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/quotedblbase.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/quotedblbase.glif
new file mode 100644
index 00000000..5a007a80
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/quotedblbase.glif
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/quotedblleft.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/quotedblleft.glif
new file mode 100644
index 00000000..528a8ab8
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/quotedblleft.glif
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/quotedblright.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/quotedblright.glif
new file mode 100644
index 00000000..42feceea
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/quotedblright.glif
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/quotesinglbase.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/quotesinglbase.glif
new file mode 100644
index 00000000..a451b737
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/quotesinglbase.glif
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/semicolon.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/semicolon.glif
new file mode 100644
index 00000000..2140efd3
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/semicolon.glif
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/space.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/space.glif
new file mode 100644
index 00000000..80c661f7
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/glyphs/space.glif
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/groups.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/groups.plist
new file mode 100644
index 00000000..7dc9fccd
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/groups.plist
@@ -0,0 +1,20 @@
+
+
+
+
+ public.kern1.@MMK_L_A
+
+ A
+
+ public.kern2.@MMK_R_A
+
+ A
+
+ testGroup
+
+ E
+ F
+ H
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/images/image b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/images/image
new file mode 100644
index 00000000..965d6cdc
Binary files /dev/null and b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/images/image differ
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/images/image000000000000001 b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/images/image000000000000001
new file mode 100644
index 00000000..214772c4
Binary files /dev/null and b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/images/image000000000000001 differ
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/kerning.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/kerning.plist
new file mode 100644
index 00000000..f88f6c0b
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/kerning.plist
@@ -0,0 +1,21 @@
+
+
+
+
+ T
+
+ public.kern2.@MMK_R_A
+ -75
+
+ V
+
+ public.kern2.@MMK_R_A
+ -100
+
+ public.kern1.@MMK_L_A
+
+ V
+ -15
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/layercontents.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/layercontents.plist
new file mode 100644
index 00000000..c2622431
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/layercontents.plist
@@ -0,0 +1,30 @@
+
+
+
+
+
+ foreground
+ glyphs
+
+
+ support
+ glyphs.support
+
+
+ support.crossbar
+ glyphs.support.crossbar
+
+
+ background
+ glyphs.background
+
+
+ support.S.wide
+ glyphs.support.S_.wide
+
+
+ support.S.middle
+ glyphs.support.S_.middle
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/lib.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/lib.plist
new file mode 100644
index 00000000..e8c27f6c
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/lib.plist
@@ -0,0 +1,644 @@
+
+
+
+
+ com.defcon.sortDescriptor
+
+
+ allowPseudoUnicode
+
+ type
+ cannedDesign
+
+
+ com.letterror.lightMeter.prefs
+
+ chunkSize
+ 5
+ diameter
+ 200
+ drawTail
+
+ invert
+
+ toolDiameter
+ 30
+ toolStyle
+ fluid
+
+ com.typemytype.robofont.background.layerStrokeColor
+
+ 0.0
+ 0.8
+ 0.2
+ 0.7
+
+ com.typemytype.robofont.compileSettings.autohint
+
+ com.typemytype.robofont.compileSettings.checkOutlines
+
+ com.typemytype.robofont.compileSettings.createDummyDSIG
+
+ com.typemytype.robofont.compileSettings.decompose
+
+ com.typemytype.robofont.compileSettings.generateFormat
+ 0
+ com.typemytype.robofont.compileSettings.releaseMode
+
+ com.typemytype.robofont.foreground.layerStrokeColor
+
+ 0.5
+ 0.0
+ 0.5
+ 0.7
+
+ com.typemytype.robofont.italicSlantOffset
+ 0
+ com.typemytype.robofont.segmentType
+ curve
+ com.typemytype.robofont.shouldAddPointsInSplineConversion
+ 1
+ com.typesupply.MetricsMachine4.groupColors
+
+ @MMK_L_A
+
+ 1.0
+ 0.0
+ 0.0
+ 0.25
+
+ @MMK_L_C
+
+ 1.0
+ 0.5
+ 0.0
+ 0.25
+
+ @MMK_L_E
+
+ 1.0
+ 1.0
+ 0.0
+ 0.25
+
+ @MMK_L_I
+
+ 0.0
+ 1.0
+ 0.0
+ 0.25
+
+ @MMK_L_N
+
+ 0.0
+ 1.0
+ 1.0
+ 0.25
+
+ @MMK_L_O
+
+ 0.0
+ 0.5
+ 1.0
+ 0.25
+
+ @MMK_L_S
+
+ 0.0
+ 0.0
+ 1.0
+ 0.25
+
+ @MMK_L_U
+
+ 0.5
+ 0.0
+ 1.0
+ 0.25
+
+ @MMK_L_Y
+
+ 1.0
+ 0.0
+ 1.0
+ 0.25
+
+ @MMK_L_Z
+
+ 1.0
+ 0.0
+ 0.5
+ 0.25
+
+ @MMK_L_a
+
+ 1.0
+ 0.0
+ 0.0
+ 0.25
+
+ @MMK_L_c
+
+ 1.0
+ 0.5
+ 0.0
+ 0.25
+
+ @MMK_L_e
+
+ 1.0
+ 1.0
+ 0.0
+ 0.25
+
+ @MMK_L_i
+
+ 0.0
+ 1.0
+ 0.0
+ 0.25
+
+ @MMK_L_n
+
+ 0.0
+ 1.0
+ 1.0
+ 0.25
+
+ @MMK_L_o
+
+ 0.0
+ 0.5
+ 1.0
+ 0.25
+
+ @MMK_L_s
+
+ 0.0
+ 0.0
+ 1.0
+ 0.25
+
+ @MMK_L_u
+
+ 0.5
+ 0.0
+ 1.0
+ 0.25
+
+ @MMK_L_y
+
+ 1.0
+ 0.0
+ 1.0
+ 0.25
+
+ @MMK_L_z
+
+ 1.0
+ 0.0
+ 0.5
+ 0.25
+
+ @MMK_R_A
+
+ 1.0
+ 0.0
+ 0.0
+ 0.25
+
+ @MMK_R_C
+
+ 1.0
+ 0.5
+ 0.0
+ 0.25
+
+ @MMK_R_E
+
+ 1.0
+ 1.0
+ 0.0
+ 0.25
+
+ @MMK_R_I
+
+ 0.0
+ 1.0
+ 0.0
+ 0.25
+
+ @MMK_R_N
+
+ 0.0
+ 1.0
+ 1.0
+ 0.25
+
+ @MMK_R_O
+
+ 0.0
+ 0.5
+ 1.0
+ 0.25
+
+ @MMK_R_S
+
+ 0.0
+ 0.0
+ 1.0
+ 0.25
+
+ @MMK_R_U
+
+ 0.5
+ 0.0
+ 1.0
+ 0.25
+
+ @MMK_R_Y
+
+ 1.0
+ 0.0
+ 1.0
+ 0.25
+
+ @MMK_R_Z
+
+ 1.0
+ 0.0
+ 0.5
+ 0.25
+
+ @MMK_R_a
+
+ 1.0
+ 0.0
+ 0.0
+ 0.25
+
+ @MMK_R_c
+
+ 1.0
+ 0.5
+ 0.0
+ 0.25
+
+ @MMK_R_e
+
+ 1.0
+ 1.0
+ 0.0
+ 0.25
+
+ @MMK_R_i
+
+ 0.0
+ 1.0
+ 0.0
+ 0.25
+
+ @MMK_R_n
+
+ 0.0
+ 1.0
+ 1.0
+ 0.25
+
+ @MMK_R_o
+
+ 0.0
+ 0.5
+ 1.0
+ 0.25
+
+ @MMK_R_s
+
+ 0.0
+ 0.0
+ 1.0
+ 0.25
+
+ @MMK_R_u
+
+ 0.5
+ 0.0
+ 1.0
+ 0.25
+
+ @MMK_R_y
+
+ 1.0
+ 0.0
+ 1.0
+ 0.25
+
+ @MMK_R_z
+
+ 1.0
+ 0.0
+ 0.5
+ 0.25
+
+
+ com.typesupply.defcon.sortDescriptor
+
+
+ ascending
+
+ space
+ A
+ Agrave
+ Aacute
+ Acircumflex
+ Atilde
+ Adieresis
+ Aring
+ B
+ C
+ Ccedilla
+ D
+ E
+ Egrave
+ Eacute
+ Ecircumflex
+ Edieresis
+ F
+ G
+ H
+ I
+ Igrave
+ Iacute
+ Icircumflex
+ Idieresis
+ J
+ K
+ L
+ M
+ N
+ Ntilde
+ O
+ Ograve
+ Oacute
+ Ocircumflex
+ Otilde
+ Odieresis
+ P
+ Q
+ R
+ S
+ Scaron
+ T
+ U
+ Ugrave
+ Uacute
+ Ucircumflex
+ Udieresis
+ V
+ W
+ X
+ Y
+ Yacute
+ Ydieresis
+ Z
+ Zcaron
+ AE
+ Eth
+ Oslash
+ Thorn
+ Lslash
+ OE
+ a
+ agrave
+ aacute
+ acircumflex
+ atilde
+ adieresis
+ aring
+ b
+ c
+ ccedilla
+ d
+ e
+ egrave
+ eacute
+ ecircumflex
+ edieresis
+ f
+ g
+ h
+ i
+ igrave
+ iacute
+ icircumflex
+ idieresis
+ j
+ k
+ l
+ m
+ n
+ ntilde
+ o
+ ograve
+ oacute
+ ocircumflex
+ otilde
+ odieresis
+ p
+ q
+ r
+ s
+ scaron
+ t
+ u
+ ugrave
+ uacute
+ ucircumflex
+ udieresis
+ v
+ w
+ x
+ y
+ yacute
+ ydieresis
+ z
+ zcaron
+ ordfeminine
+ ordmasculine
+ germandbls
+ ae
+ eth
+ oslash
+ thorn
+ dotlessi
+ lslash
+ oe
+ mu
+ zero
+ one
+ two
+ three
+ four
+ five
+ six
+ seven
+ eight
+ nine
+ onesuperior
+ twosuperior
+ threesuperior
+ onequarter
+ onehalf
+ threequarters
+ underscore
+ hyphen
+ endash
+ emdash
+ parenleft
+ parenright
+ bracketleft
+ bracketright
+ braceleft
+ braceright
+ numbersign
+ percent
+ perthousand
+ quotesingle
+ quotedbl
+ quoteleft
+ quoteright
+ quotedblleft
+ quotedblright
+ quotesinglbase
+ quotedblbase
+ guilsinglleft
+ guilsinglright
+ asterisk
+ dagger
+ daggerdbl
+ period
+ comma
+ colon
+ semicolon
+ ellipsis
+ exclam
+ exclamdown
+ question
+ questiondown
+ slash
+ backslash
+ fraction
+ bar
+ brokenbar
+ at
+ ampersand
+ section
+ paragraph
+ periodcentered
+ bullet
+ plus
+ minus
+ plusminus
+ divide
+ multiply
+ equal
+ less
+ greater
+ logicalnot
+ dollar
+ cent
+ sterling
+ currency
+ yen
+ Euro
+ asciicircum
+ asciitilde
+ acute
+ grave
+ hungarumlaut
+ circumflex
+ caron
+ breve
+ tilde
+ macron
+ dieresis
+ dotaccent
+ ring
+ cedilla
+ ogonek
+ copyright
+ registered
+ trademark
+ degree
+ florin
+ guillemotleft
+ guillemotright
+ fi
+ fl
+ a_b_c
+ .notdef
+
+ type
+ glyphList
+
+
+ public.glyphOrder
+
+ space
+ A
+ Aacute
+ Adieresis
+ B
+ C
+ D
+ E
+ F
+ G
+ H
+ I
+ J
+ K
+ L
+ M
+ N
+ O
+ P
+ Q
+ R
+ S
+ T
+ U
+ V
+ W
+ X
+ Y
+ Z
+ quotedblleft
+ quotedblright
+ quotesinglbase
+ quotedblbase
+ period
+ comma
+ colon
+ semicolon
+ arrowleft
+ arrowup
+ arrowright
+ arrowdown
+ dot
+ acute
+ dieresis
+ IJ
+ I.narrow
+ J.narrow
+ S.closed
+ .notdef
+
+ testLibItemKey
+
+ a
+ b
+ c
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/metainfo.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/metainfo.plist
new file mode 100644
index 00000000..7b8b34ac
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightCondensed.ufo/metainfo.plist
@@ -0,0 +1,10 @@
+
+
+
+
+ creator
+ com.github.fonttools.ufoLib
+ formatVersion
+ 3
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/features.fea b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/features.fea
new file mode 100644
index 00000000..124776d7
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/features.fea
@@ -0,0 +1,3 @@
+# this is the feature from lightWide
+languagesystem DFLT dflt;
+languagesystem latn dflt;
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/fontinfo.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/fontinfo.plist
new file mode 100644
index 00000000..222803ff
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/fontinfo.plist
@@ -0,0 +1,65 @@
+
+
+
+
+ ascender
+ 700
+ capHeight
+ 700
+ copyright
+ License same as MutatorMath. BSD 3-clause. [test-token: D]
+ descender
+ -200
+ familyName
+ MutatorSans
+ guidelines
+
+ italicAngle
+ 0
+ openTypeNameLicense
+ License same as MutatorMath. BSD 3-clause. [test-token: D]
+ openTypeOS2VendorID
+ LTTR
+ postscriptBlueValues
+
+ -10
+ 0
+ 700
+ 710
+
+ postscriptDefaultWidthX
+ 500
+ postscriptFamilyBlues
+
+ postscriptFamilyOtherBlues
+
+ postscriptFontName
+ MutatorSans-LightWide
+ postscriptOtherBlues
+
+ postscriptSlantAngle
+ 0
+ postscriptStemSnapH
+
+ postscriptStemSnapV
+
+ postscriptWindowsCharacterSet
+ 1
+ styleMapFamilyName
+
+ styleMapStyleName
+ regular
+ styleName
+ LightWide
+ unitsPerEm
+ 1000
+ versionMajor
+ 1
+ versionMinor
+ 2
+ xHeight
+ 500
+ year
+ 2004
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs.background/S_.closed.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs.background/S_.closed.glif
new file mode 100644
index 00000000..8ad86c9b
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs.background/S_.closed.glif
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs.background/contents.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs.background/contents.plist
new file mode 100644
index 00000000..0ef1afc6
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs.background/contents.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ S.closed
+ S_.closed.glif
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs.background/layerinfo.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs.background/layerinfo.plist
new file mode 100644
index 00000000..7e385c7f
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs.background/layerinfo.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ color
+ 0.5,1,0,0.7
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/A_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/A_.glif
new file mode 100644
index 00000000..9bda24f8
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/A_.glif
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.typemytype.robofont.Image.Brightness
+ 0
+ com.typemytype.robofont.Image.Contrast
+ 1
+ com.typemytype.robofont.Image.Saturation
+ 1
+ com.typemytype.robofont.Image.Sharpness
+ 0.4
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/A_acute.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/A_acute.glif
new file mode 100644
index 00000000..cf8ade0e
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/A_acute.glif
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+ public.markColor
+ 0.6567,0.6903,1,1
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/A_dieresis.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/A_dieresis.glif
new file mode 100644
index 00000000..64d419b2
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/A_dieresis.glif
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+ public.markColor
+ 0.6567,0.6903,1,1
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/B_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/B_.glif
new file mode 100644
index 00000000..2df4bd12
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/B_.glif
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/C_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/C_.glif
new file mode 100644
index 00000000..dfe52541
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/C_.glif
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/D_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/D_.glif
new file mode 100644
index 00000000..7aab614c
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/D_.glif
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/E_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/E_.glif
new file mode 100644
index 00000000..201d0def
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/E_.glif
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.typemytype.robofont.Image.Brightness
+ 0
+ com.typemytype.robofont.Image.Contrast
+ 1
+ com.typemytype.robofont.Image.Saturation
+ 1
+ com.typemytype.robofont.Image.Sharpness
+ 0.4
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/F_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/F_.glif
new file mode 100644
index 00000000..f1e68b8f
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/F_.glif
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.typemytype.robofont.guideline.magnetic.tCW6w1QdWl
+ 5
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/G_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/G_.glif
new file mode 100644
index 00000000..f15c562d
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/G_.glif
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/H_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/H_.glif
new file mode 100644
index 00000000..e6f12e3c
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/H_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/I_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/I_.glif
new file mode 100644
index 00000000..fa3c5a78
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/I_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/I_.narrow.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/I_.narrow.glif
new file mode 100644
index 00000000..fdf59866
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/I_.narrow.glif
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.typemytype.robofont.Image.Brightness
+ 0
+ com.typemytype.robofont.Image.Contrast
+ 1
+ com.typemytype.robofont.Image.Saturation
+ 1
+ com.typemytype.robofont.Image.Sharpness
+ 0.4
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/I_J_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/I_J_.glif
new file mode 100644
index 00000000..1b8dc0d9
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/I_J_.glif
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/J_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/J_.glif
new file mode 100644
index 00000000..5c9c0927
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/J_.glif
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/J_.narrow.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/J_.narrow.glif
new file mode 100644
index 00000000..9d39b581
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/J_.narrow.glif
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/K_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/K_.glif
new file mode 100644
index 00000000..6f8061f5
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/K_.glif
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/L_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/L_.glif
new file mode 100644
index 00000000..b1bb16e5
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/L_.glif
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/M_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/M_.glif
new file mode 100644
index 00000000..3403d3f0
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/M_.glif
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/N_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/N_.glif
new file mode 100644
index 00000000..6a7073de
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/N_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/O_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/O_.glif
new file mode 100644
index 00000000..b5723838
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/O_.glif
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/P_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/P_.glif
new file mode 100644
index 00000000..0625d631
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/P_.glif
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/Q_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/Q_.glif
new file mode 100644
index 00000000..2a1da2a0
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/Q_.glif
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/R_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/R_.glif
new file mode 100644
index 00000000..9b34050a
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/R_.glif
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/S_.closed.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/S_.closed.glif
new file mode 100644
index 00000000..08a88124
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/S_.closed.glif
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/S_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/S_.glif
new file mode 100644
index 00000000..dda8c480
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/S_.glif
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.typemytype.robofont.Image.Brightness
+ 0
+ com.typemytype.robofont.Image.Contrast
+ 1
+ com.typemytype.robofont.Image.Saturation
+ 1
+ com.typemytype.robofont.Image.Sharpness
+ 0.4
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/T_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/T_.glif
new file mode 100644
index 00000000..de53b29d
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/T_.glif
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/U_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/U_.glif
new file mode 100644
index 00000000..f17919fa
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/U_.glif
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/V_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/V_.glif
new file mode 100644
index 00000000..aa96bcef
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/V_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/W_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/W_.glif
new file mode 100644
index 00000000..c0e4302e
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/W_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/X_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/X_.glif
new file mode 100644
index 00000000..af8c9762
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/X_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/Y_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/Y_.glif
new file mode 100644
index 00000000..c3aa70c1
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/Y_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/Z_.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/Z_.glif
new file mode 100644
index 00000000..9c01301f
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/Z_.glif
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/_notdef.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/_notdef.glif
new file mode 100644
index 00000000..7bd7a779
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/_notdef.glif
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ public.markColor
+ 0.6567,0.6903,1,1
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/acute.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/acute.glif
new file mode 100644
index 00000000..f1b10c6d
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/acute.glif
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/arrowdown.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/arrowdown.glif
new file mode 100644
index 00000000..8392d98c
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/arrowdown.glif
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/arrowleft.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/arrowleft.glif
new file mode 100644
index 00000000..cdbd1cac
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/arrowleft.glif
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/arrowright.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/arrowright.glif
new file mode 100644
index 00000000..55b68fb9
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/arrowright.glif
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/arrowup.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/arrowup.glif
new file mode 100644
index 00000000..1545bd7e
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/arrowup.glif
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/b.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/b.glif
new file mode 100644
index 00000000..16accedd
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/b.glif
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/c.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/c.glif
new file mode 100644
index 00000000..0b4b3ccf
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/c.glif
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/colon.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/colon.glif
new file mode 100644
index 00000000..e442b708
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/colon.glif
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/comma.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/comma.glif
new file mode 100644
index 00000000..b214a2eb
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/comma.glif
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/contents.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/contents.plist
new file mode 100644
index 00000000..45273dee
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/contents.plist
@@ -0,0 +1,104 @@
+
+
+
+
+ .notdef
+ _notdef.glif
+ A
+ A_.glif
+ Aacute
+ A_acute.glif
+ Adieresis
+ A_dieresis.glif
+ B
+ B_.glif
+ C
+ C_.glif
+ D
+ D_.glif
+ E
+ E_.glif
+ F
+ F_.glif
+ G
+ G_.glif
+ H
+ H_.glif
+ I
+ I_.glif
+ I.narrow
+ I_.narrow.glif
+ IJ
+ I_J_.glif
+ J
+ J_.glif
+ J.narrow
+ J_.narrow.glif
+ K
+ K_.glif
+ L
+ L_.glif
+ M
+ M_.glif
+ N
+ N_.glif
+ O
+ O_.glif
+ P
+ P_.glif
+ Q
+ Q_.glif
+ R
+ R_.glif
+ S
+ S_.glif
+ S.closed
+ S_.closed.glif
+ T
+ T_.glif
+ U
+ U_.glif
+ V
+ V_.glif
+ W
+ W_.glif
+ X
+ X_.glif
+ Y
+ Y_.glif
+ Z
+ Z_.glif
+ acute
+ acute.glif
+ arrowdown
+ arrowdown.glif
+ arrowleft
+ arrowleft.glif
+ arrowright
+ arrowright.glif
+ arrowup
+ arrowup.glif
+ colon
+ colon.glif
+ comma
+ comma.glif
+ dieresis
+ dieresis.glif
+ dot
+ dot.glif
+ period
+ period.glif
+ quotedblbase
+ quotedblbase.glif
+ quotedblleft
+ quotedblleft.glif
+ quotedblright
+ quotedblright.glif
+ quotesinglbase
+ quotesinglbase.glif
+ semicolon
+ semicolon.glif
+ space
+ space.glif
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/d.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/d.glif
new file mode 100644
index 00000000..8fc09610
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/d.glif
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/dieresis.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/dieresis.glif
new file mode 100644
index 00000000..1eef4666
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/dieresis.glif
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+ public.markColor
+ 0.6567,0.6903,1,1
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/dot.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/dot.glif
new file mode 100644
index 00000000..8f14d475
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/dot.glif
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/layerinfo.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/layerinfo.plist
new file mode 100644
index 00000000..aeeb1b2a
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/layerinfo.plist
@@ -0,0 +1,13 @@
+
+
+
+
+ color
+ 1,0.75,0,0.7
+ lib
+
+ com.typemytype.robofont.segmentType
+ curve
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/period.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/period.glif
new file mode 100644
index 00000000..3a362b47
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/period.glif
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/quotedblbase.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/quotedblbase.glif
new file mode 100644
index 00000000..fbfc8536
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/quotedblbase.glif
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/quotedblleft.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/quotedblleft.glif
new file mode 100644
index 00000000..f34371e4
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/quotedblleft.glif
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/quotedblright.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/quotedblright.glif
new file mode 100644
index 00000000..53e5d26a
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/quotedblright.glif
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/quotesinglbase.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/quotesinglbase.glif
new file mode 100644
index 00000000..4ff1ffbb
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/quotesinglbase.glif
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/semicolon.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/semicolon.glif
new file mode 100644
index 00000000..995849fd
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/semicolon.glif
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/space.glif b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/space.glif
new file mode 100644
index 00000000..80c661f7
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/glyphs/space.glif
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/groups.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/groups.plist
new file mode 100644
index 00000000..7dc9fccd
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/groups.plist
@@ -0,0 +1,20 @@
+
+
+
+
+ public.kern1.@MMK_L_A
+
+ A
+
+ public.kern2.@MMK_R_A
+
+ A
+
+ testGroup
+
+ E
+ F
+ H
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/kerning.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/kerning.plist
new file mode 100644
index 00000000..5fda98fd
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/kerning.plist
@@ -0,0 +1,210 @@
+
+
+
+
+ B
+
+ H
+ -40
+ J
+ -85
+ O
+ -10
+ S
+ -25
+ T
+ -85
+ U
+ -35
+ V
+ -85
+ public.kern2.@MMK_R_A
+ -25
+
+ C
+
+ J
+ -75
+ T
+ -40
+ V
+ -65
+ public.kern2.@MMK_R_A
+ -40
+
+ E
+
+ J
+ -80
+ T
+ -10
+
+ F
+
+ H
+ -35
+ J
+ -245
+ O
+ -50
+ S
+ -70
+ U
+ -10
+ public.kern2.@MMK_R_A
+ -125
+
+ G
+
+ J
+ -60
+ O
+ -15
+ S
+ -15
+ T
+ -105
+ U
+ -20
+ V
+ -70
+ public.kern2.@MMK_R_A
+ -40
+
+ H
+
+ J
+ -40
+ S
+ -30
+
+ J
+
+ J
+ -135
+ O
+ -10
+ public.kern2.@MMK_R_A
+ -70
+
+ L
+
+ J
+ -25
+ O
+ -95
+ T
+ -305
+ U
+ -85
+ V
+ -185
+
+ O
+
+ J
+ -120
+ S
+ -45
+ T
+ -90
+ V
+ -80
+ public.kern2.@MMK_R_A
+ -30
+
+ P
+
+ J
+ -200
+ T
+ -20
+ public.kern2.@MMK_R_A
+ -95
+
+ R
+
+ H
+ -45
+ J
+ -155
+ O
+ -55
+ S
+ -70
+ T
+ -80
+ U
+ -60
+ V
+ -95
+ public.kern2.@MMK_R_A
+ -80
+
+ S
+
+ H
+ -5
+ J
+ -115
+ O
+ -40
+ S
+ -45
+ T
+ -50
+ U
+ -10
+ V
+ -20
+ public.kern2.@MMK_R_A
+ -45
+
+ T
+
+ H
+ -15
+ J
+ -315
+ O
+ -90
+ S
+ -25
+ public.kern2.@MMK_R_A
+ -215
+
+ U
+
+ J
+ -140
+ public.kern2.@MMK_R_A
+ -75
+
+ V
+
+ H
+ -20
+ J
+ -265
+ O
+ -55
+ S
+ -50
+ public.kern2.@MMK_R_A
+ -210
+
+ public.kern1.@MMK_L_A
+
+ J
+ -35
+ O
+ -55
+ T
+ -190
+ U
+ -95
+ V
+ -180
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/layercontents.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/layercontents.plist
new file mode 100644
index 00000000..e9a336b2
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/layercontents.plist
@@ -0,0 +1,14 @@
+
+
+
+
+
+ foreground
+ glyphs
+
+
+ background
+ glyphs.background
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/lib.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/lib.plist
new file mode 100644
index 00000000..73160063
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/lib.plist
@@ -0,0 +1,623 @@
+
+
+
+
+ com.defcon.sortDescriptor
+
+
+ allowPseudoUnicode
+
+ type
+ cannedDesign
+
+
+ com.typemytype.robofont.background.layerStrokeColor
+
+ 1.0
+ 0.75
+ 0.0
+ 0.7
+
+ com.typemytype.robofont.compileSettings.autohint
+
+ com.typemytype.robofont.compileSettings.checkOutlines
+
+ com.typemytype.robofont.compileSettings.createDummyDSIG
+
+ com.typemytype.robofont.compileSettings.decompose
+
+ com.typemytype.robofont.compileSettings.generateFormat
+ 0
+ com.typemytype.robofont.compileSettings.releaseMode
+
+ com.typemytype.robofont.foreground.layerStrokeColor
+
+ 0.5
+ 0.0
+ 0.5
+ 0.7
+
+ com.typemytype.robofont.italicSlantOffset
+ 0
+ com.typemytype.robofont.segmentType
+ curve
+ com.typemytype.robofont.shouldAddPointsInSplineConversion
+ 1
+ com.typesupply.MetricsMachine4.groupColors
+
+ @MMK_L_A
+
+ 1.0
+ 0.0
+ 0.0
+ 0.25
+
+ @MMK_L_C
+
+ 1.0
+ 0.5
+ 0.0
+ 0.25
+
+ @MMK_L_E
+
+ 1.0
+ 1.0
+ 0.0
+ 0.25
+
+ @MMK_L_I
+
+ 0.0
+ 1.0
+ 0.0
+ 0.25
+
+ @MMK_L_N
+
+ 0.0
+ 1.0
+ 1.0
+ 0.25
+
+ @MMK_L_O
+
+ 0.0
+ 0.5
+ 1.0
+ 0.25
+
+ @MMK_L_S
+
+ 0.0
+ 0.0
+ 1.0
+ 0.25
+
+ @MMK_L_U
+
+ 0.5
+ 0.0
+ 1.0
+ 0.25
+
+ @MMK_L_Y
+
+ 1.0
+ 0.0
+ 1.0
+ 0.25
+
+ @MMK_L_Z
+
+ 1.0
+ 0.0
+ 0.5
+ 0.25
+
+ @MMK_L_a
+
+ 1.0
+ 0.0
+ 0.0
+ 0.25
+
+ @MMK_L_c
+
+ 1.0
+ 0.5
+ 0.0
+ 0.25
+
+ @MMK_L_e
+
+ 1.0
+ 1.0
+ 0.0
+ 0.25
+
+ @MMK_L_i
+
+ 0.0
+ 1.0
+ 0.0
+ 0.25
+
+ @MMK_L_n
+
+ 0.0
+ 1.0
+ 1.0
+ 0.25
+
+ @MMK_L_o
+
+ 0.0
+ 0.5
+ 1.0
+ 0.25
+
+ @MMK_L_s
+
+ 0.0
+ 0.0
+ 1.0
+ 0.25
+
+ @MMK_L_u
+
+ 0.5
+ 0.0
+ 1.0
+ 0.25
+
+ @MMK_L_y
+
+ 1.0
+ 0.0
+ 1.0
+ 0.25
+
+ @MMK_L_z
+
+ 1.0
+ 0.0
+ 0.5
+ 0.25
+
+ @MMK_R_A
+
+ 1.0
+ 0.0
+ 0.0
+ 0.25
+
+ @MMK_R_C
+
+ 1.0
+ 0.5
+ 0.0
+ 0.25
+
+ @MMK_R_E
+
+ 1.0
+ 1.0
+ 0.0
+ 0.25
+
+ @MMK_R_I
+
+ 0.0
+ 1.0
+ 0.0
+ 0.25
+
+ @MMK_R_N
+
+ 0.0
+ 1.0
+ 1.0
+ 0.25
+
+ @MMK_R_O
+
+ 0.0
+ 0.5
+ 1.0
+ 0.25
+
+ @MMK_R_S
+
+ 0.0
+ 0.0
+ 1.0
+ 0.25
+
+ @MMK_R_U
+
+ 0.5
+ 0.0
+ 1.0
+ 0.25
+
+ @MMK_R_Y
+
+ 1.0
+ 0.0
+ 1.0
+ 0.25
+
+ @MMK_R_Z
+
+ 1.0
+ 0.0
+ 0.5
+ 0.25
+
+ @MMK_R_a
+
+ 1.0
+ 0.0
+ 0.0
+ 0.25
+
+ @MMK_R_c
+
+ 1.0
+ 0.5
+ 0.0
+ 0.25
+
+ @MMK_R_e
+
+ 1.0
+ 1.0
+ 0.0
+ 0.25
+
+ @MMK_R_i
+
+ 0.0
+ 1.0
+ 0.0
+ 0.25
+
+ @MMK_R_n
+
+ 0.0
+ 1.0
+ 1.0
+ 0.25
+
+ @MMK_R_o
+
+ 0.0
+ 0.5
+ 1.0
+ 0.25
+
+ @MMK_R_s
+
+ 0.0
+ 0.0
+ 1.0
+ 0.25
+
+ @MMK_R_u
+
+ 0.5
+ 0.0
+ 1.0
+ 0.25
+
+ @MMK_R_y
+
+ 1.0
+ 0.0
+ 1.0
+ 0.25
+
+ @MMK_R_z
+
+ 1.0
+ 0.0
+ 0.5
+ 0.25
+
+
+ com.typesupply.defcon.sortDescriptor
+
+
+ ascending
+
+ space
+ A
+ Agrave
+ Aacute
+ Acircumflex
+ Atilde
+ Adieresis
+ Aring
+ B
+ C
+ Ccedilla
+ D
+ E
+ Egrave
+ Eacute
+ Ecircumflex
+ Edieresis
+ F
+ G
+ H
+ I
+ Igrave
+ Iacute
+ Icircumflex
+ Idieresis
+ J
+ K
+ L
+ M
+ N
+ Ntilde
+ O
+ Ograve
+ Oacute
+ Ocircumflex
+ Otilde
+ Odieresis
+ P
+ Q
+ R
+ S
+ Scaron
+ T
+ U
+ Ugrave
+ Uacute
+ Ucircumflex
+ Udieresis
+ V
+ W
+ X
+ Y
+ Yacute
+ Ydieresis
+ Z
+ Zcaron
+ AE
+ Eth
+ Oslash
+ Thorn
+ Lslash
+ OE
+ a
+ agrave
+ aacute
+ acircumflex
+ atilde
+ adieresis
+ aring
+ b
+ c
+ ccedilla
+ d
+ e
+ egrave
+ eacute
+ ecircumflex
+ edieresis
+ f
+ g
+ h
+ i
+ igrave
+ iacute
+ icircumflex
+ idieresis
+ j
+ k
+ l
+ m
+ n
+ ntilde
+ o
+ ograve
+ oacute
+ ocircumflex
+ otilde
+ odieresis
+ p
+ q
+ r
+ s
+ scaron
+ t
+ u
+ ugrave
+ uacute
+ ucircumflex
+ udieresis
+ v
+ w
+ x
+ y
+ yacute
+ ydieresis
+ z
+ zcaron
+ ordfeminine
+ ordmasculine
+ germandbls
+ ae
+ eth
+ oslash
+ thorn
+ dotlessi
+ lslash
+ oe
+ zero
+ one
+ two
+ three
+ four
+ five
+ six
+ seven
+ eight
+ nine
+ onesuperior
+ twosuperior
+ threesuperior
+ onequarter
+ onehalf
+ threequarters
+ underscore
+ hyphen
+ endash
+ emdash
+ parenleft
+ parenright
+ bracketleft
+ bracketright
+ braceleft
+ braceright
+ numbersign
+ percent
+ perthousand
+ quotesingle
+ quotedbl
+ quoteleft
+ quoteright
+ quotedblleft
+ quotedblright
+ quotesinglbase
+ quotedblbase
+ guilsinglleft
+ guilsinglright
+ guillemotleft
+ guillemotright
+ asterisk
+ dagger
+ daggerdbl
+ period
+ comma
+ colon
+ semicolon
+ ellipsis
+ exclam
+ exclamdown
+ question
+ questiondown
+ slash
+ backslash
+ fraction
+ bar
+ brokenbar
+ at
+ ampersand
+ section
+ paragraph
+ periodcentered
+ bullet
+ plus
+ minus
+ plusminus
+ divide
+ multiply
+ equal
+ less
+ greater
+ logicalnot
+ mu
+ dollar
+ cent
+ sterling
+ currency
+ yen
+ Euro
+ florin
+ asciicircum
+ asciitilde
+ acute
+ grave
+ hungarumlaut
+ circumflex
+ caron
+ breve
+ tilde
+ macron
+ dieresis
+ dotaccent
+ ring
+ cedilla
+ ogonek
+ copyright
+ registered
+ trademark
+ degree
+ fi
+ fl
+ .notdef
+ a_b_c
+
+ type
+ glyphList
+
+
+ public.glyphOrder
+
+ space
+ A
+ Aacute
+ Adieresis
+ B
+ C
+ D
+ E
+ F
+ G
+ H
+ I
+ J
+ K
+ L
+ M
+ N
+ O
+ P
+ Q
+ R
+ S
+ T
+ U
+ V
+ W
+ X
+ Y
+ Z
+ quotedblleft
+ quotedblright
+ quotesinglbase
+ quotedblbase
+ period
+ comma
+ colon
+ semicolon
+ arrowleft
+ arrowup
+ arrowright
+ arrowdown
+ dot
+ acute
+ dieresis
+ IJ
+ I.narrow
+ J.narrow
+ S.closed
+ .notdef
+
+
+
diff --git a/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/metainfo.plist b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/metainfo.plist
new file mode 100644
index 00000000..7b8b34ac
--- /dev/null
+++ b/fixtures/fonts/mutatorsans-variable/MutatorSansLightWide.ufo/metainfo.plist
@@ -0,0 +1,10 @@
+
+
+
+
+ creator
+ com.github.fonttools.ufoLib
+ formatVersion
+ 3
+
+
diff --git a/packages/types/__fixtures__/variation_parity.json b/packages/types/__fixtures__/variation_parity.json
new file mode 100644
index 00000000..a7761f3f
--- /dev/null
+++ b/packages/types/__fixtures__/variation_parity.json
@@ -0,0 +1,438 @@
+{
+ "glyphName": "A",
+ "designspaceTarget": {
+ "wdth": 500.0,
+ "wght": 500.0
+ },
+ "normalisedLocation": {
+ "wdth": 0.5,
+ "wght": 0.5
+ },
+ "data": {
+ "regions": [
+ [
+ {
+ "axisTag": "wdth",
+ "lower": 0.0,
+ "peak": 0.0,
+ "upper": 0.0
+ },
+ {
+ "axisTag": "wght",
+ "lower": 0.0,
+ "peak": 0.0,
+ "upper": 0.0
+ }
+ ],
+ [
+ {
+ "axisTag": "wdth",
+ "lower": 0.0,
+ "peak": 1.0,
+ "upper": 1.0
+ },
+ {
+ "axisTag": "wght",
+ "lower": 0.0,
+ "peak": 0.0,
+ "upper": 0.0
+ }
+ ],
+ [
+ {
+ "axisTag": "wdth",
+ "lower": 0.0,
+ "peak": 0.0,
+ "upper": 0.0
+ },
+ {
+ "axisTag": "wght",
+ "lower": 0.0,
+ "peak": 1.0,
+ "upper": 1.0
+ }
+ ],
+ [
+ {
+ "axisTag": "wdth",
+ "lower": 0.0,
+ "peak": 1.0,
+ "upper": 1.0
+ },
+ {
+ "axisTag": "wght",
+ "lower": 0.0,
+ "peak": 1.0,
+ "upper": 1.0
+ }
+ ]
+ ],
+ "deltas": [
+ [
+ 396.0,
+ 20.0,
+ 0.0,
+ 60.0,
+ 0.0,
+ 200.0,
+ 700.0,
+ 165.0,
+ 700.0,
+ 75.0,
+ 164.0,
+ 325.0,
+ 164.0,
+ 325.0,
+ 200.0,
+ 75.0,
+ 200.0,
+ 332.0,
+ 0.0,
+ 376.0,
+ 0.0,
+ 231.0,
+ 700.0,
+ 192.0,
+ 700.0,
+ 175.0,
+ 661.0,
+ 222.0,
+ 661.0,
+ 222.0,
+ 700.0,
+ 175.0,
+ 700.0
+ ],
+ [
+ 794.0,
+ 30.0,
+ 0.0,
+ 37.0,
+ 0.0,
+ 412.0,
+ 0.0,
+ 405.0,
+ 0.0,
+ 170.0,
+ 60.0,
+ 620.0,
+ 60.0,
+ 620.0,
+ 54.0,
+ 170.0,
+ 54.0,
+ 755.0,
+ 0.0,
+ 764.0,
+ 0.0,
+ 389.0,
+ 0.0,
+ 380.0,
+ 0.0,
+ 395.0,
+ 3.0,
+ 398.0,
+ 3.0,
+ 398.0,
+ 0.0,
+ 395.0,
+ 0.0
+ ],
+ [
+ 344.0,
+ -30.0,
+ 0.0,
+ 190.0,
+ 0.0,
+ 134.0,
+ 100.0,
+ -61.0,
+ 100.0,
+ 35.0,
+ -44.0,
+ 255.0,
+ -44.0,
+ 255.0,
+ 130.0,
+ 35.0,
+ 130.0,
+ 58.0,
+ 0.0,
+ 354.0,
+ 0.0,
+ 383.0,
+ 100.0,
+ 102.0,
+ 100.0,
+ 29.0,
+ -121.0,
+ 252.0,
+ -121.0,
+ 252.0,
+ 100.0,
+ 29.0,
+ 100.0
+ ],
+ [
+ -244.0,
+ 0.0,
+ 0.0,
+ 63.0,
+ 0.0,
+ -106.0,
+ 0.0,
+ -149.0,
+ 0.0,
+ -70.0,
+ -60.0,
+ -260.0,
+ -60.0,
+ -260.0,
+ -44.0,
+ -70.0,
+ -44.0,
+ -345.0,
+ 0.0,
+ -224.0,
+ 0.0,
+ -73.0,
+ 0.0,
+ -194.0,
+ 0.0,
+ -189.0,
+ -3.0,
+ -42.0,
+ -3.0,
+ -42.0,
+ 0.0,
+ -189.0,
+ 0.0
+ ]
+ ]
+ },
+ "expected": [
+ 904.0,
+ 20.0,
+ 0.0,
+ 189.25,
+ 0.0,
+ 446.5,
+ 750.0,
+ 299.75,
+ 750.0,
+ 160.0,
+ 157.0,
+ 697.5,
+ 157.0,
+ 697.5,
+ 281.0,
+ 160.0,
+ 281.0,
+ 652.25,
+ 0.0,
+ 879.0,
+ 0.0,
+ 598.75,
+ 750.0,
+ 384.5,
+ 750.0,
+ 339.75,
+ 601.25,
+ 536.5,
+ 601.25,
+ 536.5,
+ 750.0,
+ 339.75,
+ 750.0
+ ],
+ "masters": [
+ {
+ "sourceName": "LightCondensed",
+ "isDefaultSource": true,
+ "designspaceLocation": {
+ "wdth": 0.0,
+ "wght": 0.0
+ },
+ "normalisedLocation": {
+ "wdth": 0.0,
+ "wght": 0.0
+ },
+ "expected": [
+ 396.0,
+ 20.0,
+ 0.0,
+ 60.0,
+ 0.0,
+ 200.0,
+ 700.0,
+ 165.0,
+ 700.0,
+ 75.0,
+ 164.0,
+ 325.0,
+ 164.0,
+ 325.0,
+ 200.0,
+ 75.0,
+ 200.0,
+ 332.0,
+ 0.0,
+ 376.0,
+ 0.0,
+ 231.0,
+ 700.0,
+ 192.0,
+ 700.0,
+ 175.0,
+ 661.0,
+ 222.0,
+ 661.0,
+ 222.0,
+ 700.0,
+ 175.0,
+ 700.0
+ ]
+ },
+ {
+ "sourceName": "BoldCondensed",
+ "isDefaultSource": false,
+ "designspaceLocation": {
+ "wdth": 0.0,
+ "wght": 1000.0
+ },
+ "normalisedLocation": {
+ "wdth": 0.0,
+ "wght": 1.0
+ },
+ "expected": [
+ 740.0,
+ -10.0,
+ 0.0,
+ 250.0,
+ 0.0,
+ 334.0,
+ 800.0,
+ 104.0,
+ 800.0,
+ 110.0,
+ 120.0,
+ 580.0,
+ 120.0,
+ 580.0,
+ 330.0,
+ 110.0,
+ 330.0,
+ 390.0,
+ 0.0,
+ 730.0,
+ 0.0,
+ 614.0,
+ 800.0,
+ 294.0,
+ 800.0,
+ 204.0,
+ 540.0,
+ 474.0,
+ 540.0,
+ 474.0,
+ 800.0,
+ 204.0,
+ 800.0
+ ]
+ },
+ {
+ "sourceName": "LightWide",
+ "isDefaultSource": false,
+ "designspaceLocation": {
+ "wdth": 1000.0,
+ "wght": 0.0
+ },
+ "normalisedLocation": {
+ "wdth": 1.0,
+ "wght": 0.0
+ },
+ "expected": [
+ 1190.0,
+ 50.0,
+ 0.0,
+ 97.0,
+ 0.0,
+ 612.0,
+ 700.0,
+ 570.0,
+ 700.0,
+ 245.0,
+ 224.0,
+ 945.0,
+ 224.0,
+ 945.0,
+ 254.0,
+ 245.0,
+ 254.0,
+ 1087.0,
+ 0.0,
+ 1140.0,
+ 0.0,
+ 620.0,
+ 700.0,
+ 572.0,
+ 700.0,
+ 570.0,
+ 664.0,
+ 620.0,
+ 664.0,
+ 620.0,
+ 700.0,
+ 570.0,
+ 700.0
+ ]
+ },
+ {
+ "sourceName": "BoldWide",
+ "isDefaultSource": false,
+ "designspaceLocation": {
+ "wdth": 1000.0,
+ "wght": 1000.0
+ },
+ "normalisedLocation": {
+ "wdth": 1.0,
+ "wght": 1.0
+ },
+ "expected": [
+ 1290.0,
+ 20.0,
+ 0.0,
+ 350.0,
+ 0.0,
+ 640.0,
+ 800.0,
+ 360.0,
+ 800.0,
+ 210.0,
+ 120.0,
+ 940.0,
+ 120.0,
+ 940.0,
+ 340.0,
+ 210.0,
+ 340.0,
+ 800.0,
+ 0.0,
+ 1270.0,
+ 0.0,
+ 930.0,
+ 800.0,
+ 480.0,
+ 800.0,
+ 410.0,
+ 540.0,
+ 830.0,
+ 540.0,
+ 830.0,
+ 800.0,
+ 410.0,
+ 800.0
+ ]
+ }
+ ]
+}
diff --git a/packages/types/src/font.ts b/packages/types/src/font.ts
index 29735ec6..b826d0ed 100644
--- a/packages/types/src/font.ts
+++ b/packages/types/src/font.ts
@@ -11,11 +11,20 @@ export type {
RenderPointSnapshot,
RenderContourSnapshot,
GlyphSnapshot,
+ GlyphGeometry,
+ MasterSnapshot,
+ InterpolationResult,
+ SourceError,
+ AxisTent,
+ GlyphVariationData,
CommandResult,
RuleId,
MatchedRule,
FontMetrics,
FontMetadata,
+ Axis,
+ Location,
+ Source,
} from "./generated";
// Domain types (for Editor API)
diff --git a/packages/types/src/generated/Axis.ts b/packages/types/src/generated/Axis.ts
new file mode 100644
index 00000000..185f21dc
--- /dev/null
+++ b/packages/types/src/generated/Axis.ts
@@ -0,0 +1,3 @@
+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
+export type Axis = { tag: string, name: string, minimum: number, default: number, maximum: number, hidden: boolean, };
diff --git a/packages/types/src/generated/AxisTent.ts b/packages/types/src/generated/AxisTent.ts
new file mode 100644
index 00000000..ca476146
--- /dev/null
+++ b/packages/types/src/generated/AxisTent.ts
@@ -0,0 +1,3 @@
+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
+export type AxisTent = { axisTag: string, lower: number, peak: number, upper: number, };
diff --git a/packages/types/src/generated/GlyphGeometry.ts b/packages/types/src/generated/GlyphGeometry.ts
new file mode 100644
index 00000000..2a3f13dd
--- /dev/null
+++ b/packages/types/src/generated/GlyphGeometry.ts
@@ -0,0 +1,5 @@
+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+import type { AnchorSnapshot } from "./AnchorSnapshot";
+import type { ContourSnapshot } from "./ContourSnapshot";
+
+export type GlyphGeometry = { xAdvance: number, contours: Array, anchors: Array, };
diff --git a/packages/types/src/generated/GlyphVariationData.ts b/packages/types/src/generated/GlyphVariationData.ts
new file mode 100644
index 00000000..da446126
--- /dev/null
+++ b/packages/types/src/generated/GlyphVariationData.ts
@@ -0,0 +1,13 @@
+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+import type { AxisTent } from "./AxisTent";
+
+export type GlyphVariationData = {
+/**
+ * One entry per region. Inner = tents on the axes the region depends on.
+ */
+regions: Array>,
+/**
+ * Same length as `regions`. Each entry = flat values matching `flatten()` order:
+ * [xAdvance, p0.x, p0.y, ..., a0.x, a0.y, ...].
+ */
+deltas: Array>, };
diff --git a/packages/types/src/generated/InterpolationResult.ts b/packages/types/src/generated/InterpolationResult.ts
new file mode 100644
index 00000000..1e3e8edc
--- /dev/null
+++ b/packages/types/src/generated/InterpolationResult.ts
@@ -0,0 +1,5 @@
+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+import type { GlyphGeometry } from "./GlyphGeometry";
+import type { SourceError } from "./SourceError";
+
+export type InterpolationResult = { geometry: GlyphGeometry, errors: Array, };
diff --git a/packages/types/src/generated/Location.ts b/packages/types/src/generated/Location.ts
new file mode 100644
index 00000000..dbbf8be7
--- /dev/null
+++ b/packages/types/src/generated/Location.ts
@@ -0,0 +1,3 @@
+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
+export type Location = { values: { [key in string]?: number }, };
diff --git a/packages/types/src/generated/MasterSnapshot.ts b/packages/types/src/generated/MasterSnapshot.ts
new file mode 100644
index 00000000..c80e0bf8
--- /dev/null
+++ b/packages/types/src/generated/MasterSnapshot.ts
@@ -0,0 +1,5 @@
+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+import type { GlyphGeometry } from "./GlyphGeometry";
+import type { Location } from "./Location";
+
+export type MasterSnapshot = { sourceId: string, sourceName: string, isDefaultSource: boolean, location: Location, geometry: GlyphGeometry, };
diff --git a/packages/types/src/generated/Source.ts b/packages/types/src/generated/Source.ts
new file mode 100644
index 00000000..33e14357
--- /dev/null
+++ b/packages/types/src/generated/Source.ts
@@ -0,0 +1,4 @@
+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+import type { Location } from "./Location";
+
+export type Source = { id: string, name: string, location: Location, layerId: string, filename: string | null, };
diff --git a/packages/types/src/generated/SourceError.ts b/packages/types/src/generated/SourceError.ts
new file mode 100644
index 00000000..0fa35723
--- /dev/null
+++ b/packages/types/src/generated/SourceError.ts
@@ -0,0 +1,3 @@
+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
+export type SourceError = { sourceIndex: number, sourceName: string, message: string, };
diff --git a/packages/types/src/generated/index.ts b/packages/types/src/generated/index.ts
index 80334416..286f609f 100644
--- a/packages/types/src/generated/index.ts
+++ b/packages/types/src/generated/index.ts
@@ -9,9 +9,18 @@ export type { AnchorSnapshot } from "./AnchorSnapshot";
export type { RenderPointSnapshot } from "./RenderPointSnapshot";
export type { RenderContourSnapshot } from "./RenderContourSnapshot";
export type { GlyphSnapshot } from "./GlyphSnapshot";
+export type { GlyphGeometry } from "./GlyphGeometry";
+export type { MasterSnapshot } from "./MasterSnapshot";
+export type { InterpolationResult } from "./InterpolationResult";
+export type { SourceError } from "./SourceError";
+export type { AxisTent } from "./AxisTent";
+export type { GlyphVariationData } from "./GlyphVariationData";
export type { CommandResult } from "./CommandResult";
export type { RuleId } from "./RuleId";
export type { MatchedRule } from "./MatchedRule";
export type { FontMetrics } from "./FontMetrics";
export type { FontMetadata } from "./FontMetadata";
export type { DecomposedTransform } from "./DecomposedTransform";
+export type { Axis } from "./Axis";
+export type { Location } from "./Location";
+export type { Source } from "./Source";
diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts
index 4ec58d92..a09fa65a 100644
--- a/packages/types/src/index.ts
+++ b/packages/types/src/index.ts
@@ -31,12 +31,21 @@ export type {
RenderPointSnapshot,
RenderContourSnapshot,
GlyphSnapshot,
+ GlyphGeometry,
+ MasterSnapshot,
+ InterpolationResult,
+ SourceError,
+ AxisTent,
+ GlyphVariationData,
CommandResult,
RuleId,
MatchedRule,
FontMetadata,
FontMetrics,
DecomposedTransform,
+ Axis,
+ Location,
+ Source,
} from "./font";
// Domain types (for Editor API)
diff --git a/scripts/oxlint/shift-plugin.mjs b/scripts/oxlint/shift-plugin.mjs
index e2b2019f..d132a4ec 100644
--- a/scripts/oxlint/shift-plugin.mjs
+++ b/scripts/oxlint/shift-plugin.mjs
@@ -23,6 +23,7 @@ const CONTOURS_ALLOWED = [
"Editor.ts", // coordinator-level structural traversal
"clipboard/", // ClipboardContent is not a Glyph, different type
"ContourDoubleClick.ts", // finds contour by segment match
+ "interpolation/", // interpolation produces snapshots from raw contour data
];
function checkParam(context, node) {
@@ -60,6 +61,7 @@ const SNAPSHOT_ALLOWED = [
"behaviors/", // tool behaviors capture snapshots for undo via drafts
"types/engine.ts", // engine response types
"lib/model/", // reactive model uses snapshots for sync
+ "interpolation/", // interpolation produces snapshots by blending masters
];
function isAllowedFile(filename, allowList) {
diff --git a/scripts/patch-generated-types.ts b/scripts/patch-generated-types.ts
index a2635efc..2ef4ae00 100644
--- a/scripts/patch-generated-types.ts
+++ b/scripts/patch-generated-types.ts
@@ -25,6 +25,19 @@ const FILE_IMPORTS: Record = {
"CommandResult.ts": ["PointId"],
};
+/** Match what pre-commit's trailing-whitespace + end-of-file-fixer do, so the
+ * hooks don't re-modify these files on every commit attempt. ts-rs emits
+ * trailing whitespace and no terminating newline. */
+function normalizeWhitespace(content: string): string {
+ return (
+ content
+ .split("\n")
+ .map((line) => line.replace(/[ \t]+$/, ""))
+ .join("\n")
+ .replace(/\n*$/, "") + "\n"
+ );
+}
+
function patchFile(filename: string, imports: string[]): boolean {
const filePath = path.join(GENERATED_DIR, filename);
@@ -36,23 +49,30 @@ function patchFile(filename: string, imports: string[]): boolean {
let content = fs.readFileSync(filePath, "utf8");
const importStatement = `import type { ${imports.join(", ")} } from "../ids";\n`;
+ const tsrsComment =
+ "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.";
- if (content.includes('from "../ids"')) {
- console.log(`✓ ${filename} already patched`);
- return true;
+ const alreadyHasImport = content.includes('from "../ids"');
+
+ if (!alreadyHasImport) {
+ if (content.startsWith(tsrsComment)) {
+ content = tsrsComment + "\n" + importStatement + content.slice(tsrsComment.length);
+ } else {
+ content = importStatement + content;
+ }
}
- const tsrsComment =
- "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.";
+ content = normalizeWhitespace(content);
- if (content.startsWith(tsrsComment)) {
- content = tsrsComment + "\n" + importStatement + content.slice(tsrsComment.length);
- } else {
- content = importStatement + content;
+ // Avoid touching disk if nothing actually changed — keeps git unaware.
+ const existing = fs.readFileSync(filePath, "utf8");
+ if (existing === content) {
+ console.info(`✓ ${filename} already patched`);
+ return true;
}
fs.writeFileSync(filePath, content);
- console.log(`✅ Patched ${filename} with imports: ${imports.join(", ")}`);
+ console.info(`✅ Patched ${filename} with imports: ${imports.join(", ")}`);
return true;
}
@@ -68,7 +88,7 @@ function patchNapiDts(): boolean {
const importLine = 'import type { ContourId, PointId, AnchorId } from "@shift/types";';
if (content.includes(importLine)) {
- console.log("✓ shift-node/index.d.ts already patched");
+ console.info("✓ shift-node/index.d.ts already patched");
return true;
}
@@ -80,12 +100,31 @@ function patchNapiDts(): boolean {
}
fs.writeFileSync(napiDtsPath, content);
- console.log("✅ Patched shift-node/index.d.ts with branded ID imports");
+ console.info("✅ Patched shift-node/index.d.ts with branded ID imports");
return true;
}
+/** Normalize whitespace on every generated file (whether it needs an ID
+ * import or not). ts-rs emits trailing whitespace + no terminating newline
+ * — left untouched, pre-commit's trailing-whitespace + end-of-file-fixer
+ * hooks would re-flag every commit. */
+function normalizeAllGenerated(): void {
+ if (!fs.existsSync(GENERATED_DIR)) return;
+ for (const filename of fs.readdirSync(GENERATED_DIR)) {
+ if (!filename.endsWith(".ts")) continue;
+ if (filename in FILE_IMPORTS) continue; // patchFile already normalised
+ const filePath = path.join(GENERATED_DIR, filename);
+ const original = fs.readFileSync(filePath, "utf8");
+ const normalized = normalizeWhitespace(original);
+ if (original !== normalized) {
+ fs.writeFileSync(filePath, normalized);
+ console.info(`✅ Normalised whitespace in ${filename}`);
+ }
+ }
+}
+
function main(): void {
- console.log("🔧 Patching generated types with branded ID imports...\n");
+ console.info("🔧 Patching generated types with branded ID imports...\n");
let success = true;
@@ -95,14 +134,16 @@ function main(): void {
}
}
+ normalizeAllGenerated();
+
if (!patchNapiDts()) {
success = false;
}
- console.log("");
+ console.info("");
if (success) {
- console.log("✅ All generated types patched successfully!");
+ console.info("✅ All generated types patched successfully!");
} else {
console.error("❌ Some files could not be patched");
process.exit(1);