From c2a0de5f0cdcced29a52296e95b3bb1b1e153808 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Tue, 26 May 2026 15:14:23 +0300 Subject: [PATCH] fix(kiwi): support Figma OpenType variant fields --- packages/core/src/kiwi/fig/codec/index.ts | 8 + .../src/kiwi/fig/node-change/font/features.ts | 69 +++++- packages/docs/development/roadmap.md | 209 +++++++++--------- .../io/fig/export/font-variations.test.ts | 16 +- .../io/fig/import/font-variations.test.ts | 18 +- .../io/fig/import/schema-coverage.test.ts | 7 +- .../canvas/text-font-variations.test.ts | 6 +- 7 files changed, 214 insertions(+), 119 deletions(-) diff --git a/packages/core/src/kiwi/fig/codec/index.ts b/packages/core/src/kiwi/fig/codec/index.ts index 9d9634ce0..d72dcbf24 100644 --- a/packages/core/src/kiwi/fig/codec/index.ts +++ b/packages/core/src/kiwi/fig/codec/index.ts @@ -395,6 +395,14 @@ export interface NodeChange { fontVariations?: Array<{ axisTag?: number; axisName?: string; value?: number }> fontVariantCommonLigatures?: boolean fontVariantContextualLigatures?: boolean + fontVariantDiscretionaryLigatures?: boolean + fontVariantHistoricalLigatures?: boolean + fontVariantOrdinal?: boolean + fontVariantSlashedZero?: boolean + fontVariantNumericFigure?: string + fontVariantNumericSpacing?: string + fontVariantNumericFraction?: string + fontVariantCaps?: string fontVersion?: string emojiImageSet?: string lineHeight?: { value: number; units: string } diff --git a/packages/core/src/kiwi/fig/node-change/font/features.ts b/packages/core/src/kiwi/fig/node-change/font/features.ts index ef214bbcd..0ea2c672a 100644 --- a/packages/core/src/kiwi/fig/node-change/font/features.ts +++ b/packages/core/src/kiwi/fig/node-change/font/features.ts @@ -1,11 +1,41 @@ import type { NodeChange } from '#core/kiwi/fig/codec' import type { FontFeature } from '#core/scene-graph' -const LIGATURE_FEATURES = [ +const BOOLEAN_FEATURES = [ ['fontVariantCommonLigatures', 'LIGA'], - ['fontVariantContextualLigatures', 'CALT'] + ['fontVariantContextualLigatures', 'CALT'], + ['fontVariantDiscretionaryLigatures', 'DLIG'], + ['fontVariantHistoricalLigatures', 'HLIG'], + ['fontVariantOrdinal', 'ORDN'], + ['fontVariantSlashedZero', 'ZERO'] ] as const +const ENUM_FEATURES = [ + ['fontVariantNumericFigure', { LINING: 'LNUM', OLDSTYLE: 'ONUM' }], + ['fontVariantNumericSpacing', { PROPORTIONAL: 'PNUM', TABULAR: 'TNUM' }], + ['fontVariantNumericFraction', { DIAGONAL: 'FRAC', STACKED: 'AFRC' }], + ['fontVariantCaps', { SMALL: 'SMCP', PETITE: 'PCAP', UNICASE: 'UNIC', TITLING: 'TITL' }] +] as const + +const BOOLEAN_FEATURE_EXPORT = Object.fromEntries( + BOOLEAN_FEATURES.map(([field, tag]) => [tag, field]) +) as Partial> + +const ENUM_FEATURE_EXPORT: Partial< + Record +> = { + LNUM: { field: 'fontVariantNumericFigure', value: 'LINING' }, + ONUM: { field: 'fontVariantNumericFigure', value: 'OLDSTYLE' }, + PNUM: { field: 'fontVariantNumericSpacing', value: 'PROPORTIONAL' }, + TNUM: { field: 'fontVariantNumericSpacing', value: 'TABULAR' }, + FRAC: { field: 'fontVariantNumericFraction', value: 'DIAGONAL' }, + AFRC: { field: 'fontVariantNumericFraction', value: 'STACKED' }, + SMCP: { field: 'fontVariantCaps', value: 'SMALL' }, + PCAP: { field: 'fontVariantCaps', value: 'PETITE' }, + UNIC: { field: 'fontVariantCaps', value: 'UNICASE' }, + TITL: { field: 'fontVariantCaps', value: 'TITLING' } +} + function addFeature(features: FontFeature[], tag: string, enabled: boolean): void { const normalizedTag = tag.toUpperCase() if (features.some((feature) => feature.tag === normalizedTag)) return @@ -14,25 +44,48 @@ function addFeature(features: FontFeature[], tag: string, enabled: boolean): voi export function convertFontFeatures(nc: NodeChange): FontFeature[] { const features: FontFeature[] = [] - for (const [field, tag] of LIGATURE_FEATURES) { + for (const [field, tag] of BOOLEAN_FEATURES) { const enabled = nc[field] if (enabled !== undefined) addFeature(features, tag, enabled) } + for (const [field, values] of ENUM_FEATURES) { + const tag = (values as Partial>)[String(nc[field])] + if (tag) addFeature(features, tag, true) + } for (const tag of nc.toggledOnOTFeatures ?? []) addFeature(features, tag, true) for (const tag of nc.toggledOffOTFeatures ?? []) addFeature(features, tag, false) return features } +function applyFontFeatureToKiwi( + nc: NodeChange, + tag: string, + enabled: boolean, + toggledOn: string[], + toggledOff: string[] +): void { + const booleanField = BOOLEAN_FEATURE_EXPORT[tag] + if (booleanField) { + nc[booleanField] = enabled + return + } + + const enumField = ENUM_FEATURE_EXPORT[tag] + if (enabled && enumField) { + nc[enumField.field] = enumField.value + return + } + + if (enabled) toggledOn.push(tag) + else toggledOff.push(tag) +} + export function applyFontFeaturesToKiwi(nc: NodeChange, features: FontFeature[]): void { const toggledOn: string[] = [] const toggledOff: string[] = [] for (const feature of features) { - const tag = feature.tag.toUpperCase() - if (tag === 'LIGA') nc.fontVariantCommonLigatures = feature.enabled - else if (tag === 'CALT') nc.fontVariantContextualLigatures = feature.enabled - else if (feature.enabled) toggledOn.push(tag) - else toggledOff.push(tag) + applyFontFeatureToKiwi(nc, feature.tag.toUpperCase(), feature.enabled, toggledOn, toggledOff) } if (toggledOn.length > 0) nc.toggledOnOTFeatures = toggledOn diff --git a/packages/docs/development/roadmap.md b/packages/docs/development/roadmap.md index 76340a1ed..a9c874c54 100644 --- a/packages/docs/development/roadmap.md +++ b/packages/docs/development/roadmap.md @@ -105,99 +105,99 @@ Figma's design documentation groups features into these areas: ## Figma compatibility matrix -| Area | Import | Render | UI edit | Export round-trip | CLI/MCP | Notes | -|---|---:|---:|---:|---:|---:|---| -| Pages / canvases | ✅ | ✅ | ✅ | ✅ | ✅ | Multi-page documents and per-page viewport are supported. | -| Frames | ✅ | ✅ | ✅ | ✅ | ✅ | Includes clipping and auto-layout container behavior. | -| Groups | ✅ | ✅ | ✅ | ✅ | ✅ | Grouping preserves visual positions. | -| Sections | ✅ | ✅ | ✅ | ✅ | ✅ | Section rendering and title pills are OpenPencil-specific approximations. | -| Rectangles / rounded rectangles | ✅ | ✅ | ✅ | ✅ | ✅ | Per-corner radii and smoothed corners render for fills, strokes, clips, masks, and effects. | -| Ellipses / arcs | ✅ | ✅ | ◐ | ✅ | ✅ | `arcData` renders/exports; no full inspector controls. | -| Lines | ✅ | ✅ | ✅ | ✅ | ✅ | Stroke caps/joins render but are not fully exposed in UI. | -| Polygons / stars | ✅ | ✅ | ◐ | ✅ | ✅ | `pointCount` and `starInnerRadius` modeled. | -| Text | ✅ | ✅ | ◐ | ✅ | ✅ | Derived Figma glyphs improve fidelity; advanced typography is partial. | -| Vectors / vector networks | ✅ | ✅ | ◐ | ✅ | ✅ | Vector edit support exists; Figma Draw tools are not fully replicated. | -| Boolean operations | ✅ | ✅ | ◐ | ✅ | ✅ | Figma `BOOLEAN_OPERATION` nodes import/export as boolean operations; inspector editing remains limited. | -| Components | ✅ | ✅ | ◐ | ✅ | ✅ | Component metadata, descriptions, links, and publish fields mostly round-trip. | -| Component sets / variants | ✅ | ✅ | ◐ | ✅ | ✅ | Variant values are usable; full component property authoring is incomplete. | -| Instances / overrides | ✅ | ✅ | ◐ | ✅ | ✅ | Raw symbol overrides and derived symbol data are preserved for fidelity. | -| Slots | ↩ | ◐ | — | ↩ | — | Some component property payloads may survive round-trip, but Figma slots are not a first-class workflow. | -| Connectors | ◐ | ◐ | — | ◐ | ◐ | Type exists, but Figma connector semantics are weak. | -| Shape-with-text / FigJam shapes | ◐ | ◐ | — | ◐ | ◐ | Type exists, but not a full FigJam feature implementation. | -| Slices | ◐ | — | ◐ | ◐ | ✅ | Slice-like export regions exist via tooling, not as true Figma slice nodes. | -| FigJam / Slides / Code / CMS / Buzz node families | ↩ | — | — | ↩ | — | Current Kiwi schema recognizes many newer Figma node families (`TABLE`, `SLIDE`, `CODE_COMPONENT`, `CMS_RICH_TEXT`, `REPEATER`, `WEBPAGE`, etc.), but OpenPencil only preserves/round-trips them where safe; they are not first-class scene nodes. | -| Solid fills | ✅ | ✅ | ✅ | ✅ | ✅ | Color variables supported for common fill cases. | -| Gradients | ✅ | ✅ | ✅ | ✅ | ✅ | Linear/radial/angular/diamond support; Figma edge cases may differ. | -| Image fills | ✅ | ✅ | ◐ | ✅ | ✅ | Fill/fit/crop/tile support exists; imported crop/tile affine transforms are applied, but exact Figma parity is still partial. | -| Pattern / noise / custom fills | ✅ | ◐ | — | ✅ | — | Schema metadata imports/exports and paints render with a solid fallback color; first-class pattern/noise/custom rendering is still missing. Transformed image tile fills cover only a subset of pattern-like imported fills. | -| Video/GIF/media fills | ↩ | — | — | ↩ | — | Kiwi schema includes media paint/export enums, but OpenPencil has no video/GIF playback or media layer support. | -| Layer/fill/effect blend modes | ✅ | ◐ | — | ✅ | ✅ | Canvas applies node, fill, and common shadow effect blend modes; Figma isolation edge cases remain partial. | -| Opacity | ✅ | ✅ | ✅ | ✅ | ✅ | Node opacity uses save layers in the renderer. | -| Strokes | ✅ | ✅ | ✅ | ✅ | ✅ | Weight, alignment, dashes, and side weights are supported. | -| Stroke caps / joins / miter limit | ✅ | ✅ | ◐ | ✅ | ✅ | Renderer/export support exists; inspector controls are limited. | -| Effects: shadows and blurs | ✅ | ✅ | ✅ | ✅ | ✅ | `showShadowBehindNode` is rendered but not exposed in UI. | -| Effect styles | ↩ | — | — | ↩ | — | Style IDs round-trip; no style manager. | -| Corner radius | ✅ | ✅ | ✅ | ✅ | ✅ | Uniform and independent radii supported. | -| Corner smoothing | ✅ | ✅ | — | ✅ | ✅ | Figma-style smoothed corners render for common uniform and independent-radius rectangles; exact parity still needs broader fixture tuning. | -| Masks | ✅ | ◐ | — | ✅ | ✅ | Figma schema `mask` / `maskType` fields import and export; common sibling alpha/vector/luminance mask stacks render, including consecutive mask layers. UI controls and deeper Figma edge cases remain incomplete. | -| Auto layout: vertical/horizontal | ✅ | ✅ | ✅ | ✅ | ✅ | Yoga-backed layout. | -| Auto layout: wrap | ✅ | ✅ | ✅ | ✅ | ✅ | UI toggle exists. | -| Auto layout: grid | ✅ | ◐ | ◐ | ✅ | ✅ | CSS-grid-like support is partial; newer schema fields for grid child alignment and auto tracks are not fully exposed. | -| Padding / gaps / alignment | ✅ | ✅ | ✅ | ✅ | ✅ | Common flex controls are exposed. | -| Hug / fill / fixed sizing | ✅ | ✅ | ✅ | ✅ | ✅ | Min/max support is partial in UI. | -| Ignore auto layout / absolute positioning | ✅ | ✅ | ◐ | ✅ | ✅ | Mode is modeled; UI coverage is partial. | -| Strokes included in layout | ✅ | ◐ | — | ✅ | ✅ | Stored/exported and used in layout paths, but no obvious panel control. | -| Reverse z-index / align-content | ✅ | ◐ | — | ✅ | ✅ | Modeled and exported; UI is limited. | -| Constraints | ✅ | ◐ | — | ✅ | ✅ | Tools/API expose constraints; main UI is limited. | -| Layout grids / guides | ↩ | — | — | ↩ | — | `styleIdForGrid` and `guides` are preserved only. | -| Text styles | ↩ | ◐ | — | ↩ | — | Style IDs round-trip; no style management UI. Rich schema metadata such as derived text data, leading trim, decoration style/thickness/fill, and semantic font style/weight is preserved for round-trip. | -| Rich style runs | ✅ | ✅ | ◐ | ✅ | ✅ | Import/render/export support; editing mixed runs is partial. | -| Text auto resize | ✅ | ✅ | ◐ | ✅ | ✅ | Used by renderer/layout; UI does not expose every mode. | -| Text truncation / max lines | ✅ | ✅ | — | ✅ | ✅ | Renderer supports ending truncation; no inspector control. | -| Text case | ✅ | ◐ | — | ✅ | ✅ | Model/export/JSX support; UI missing. | -| Vertical text alignment | ✅ | ◐ | — | ✅ | ✅ | Modeled; UI/render parity needs more coverage. | -| Justified text | ✅ | ◐ | — | ✅ | ✅ | Modeled; UI does not expose it. | -| Font variations / OpenType features | ✅ | ✅ | — | ✅ | — | Imported `fontVariations`, common ligature toggles, and raw `toggledOnOTFeatures` / `toggledOffOTFeatures` are applied to CanvasKit text styles and exported; UI controls are not exposed. | -| Variables: collections/modes/aliases | ✅ | ◐ | ◐ | ✅ | ✅ | Color/number/string/boolean model exists; inspector coverage is still incomplete. | -| Variables bound to fills/strokes | ✅ | ✅ | ✅ | ✅ | ✅ | Common color bindings render and edit. | -| Variables bound to text/layout/visibility/effects | ◐ | ◐ | ◐ | ◐ | ✅ | Some bindings exist; not full Figma property coverage. | -| Variables in prototypes / expressions / conditionals | — | — | — | — | — | Depends on prototype system, which is not implemented. | -| Libraries / publish / update review | ↩ | — | ◐ | ↩ | — | Metadata can survive round-trip; no full library workflow. | -| Prototype flows / starting points | — | — | — | — | — | Not modeled. | -| Prototype hotspots / triggers / actions | — | — | — | — | — | Not modeled. | -| Prototype overlays / scroll-to | — | — | — | — | — | Not modeled. | -| Smart animate / easing / spring / duration | — | — | — | — | — | Not modeled. | -| Interactive components | — | — | — | — | — | Component-level prototype connections are not supported. | -| Dev Mode inspect / measurements / annotations | — | — | — | — | ◐ | OpenPencil has CLI/MCP inspection, but not Figma Dev Mode UI. | -| Code Connect / dev resources / ready-for-dev | — | — | — | — | — | Not modeled. | -| Comments | — | — | — | — | — | Not modeled. | -| Version history / branches | — | — | — | — | — | Not modeled. | -| Real-time collaboration | — | ✅ | ✅ | — | — | OpenPencil has its own P2P collaboration, not Figma-compatible metadata. | +| Area | Import | Render | UI edit | Export round-trip | CLI/MCP | Notes | +| ---------------------------------------------------- | -----: | -----: | ------: | ----------------: | ------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Pages / canvases | ✅ | ✅ | ✅ | ✅ | ✅ | Multi-page documents and per-page viewport are supported. | +| Frames | ✅ | ✅ | ✅ | ✅ | ✅ | Includes clipping and auto-layout container behavior. | +| Groups | ✅ | ✅ | ✅ | ✅ | ✅ | Grouping preserves visual positions. | +| Sections | ✅ | ✅ | ✅ | ✅ | ✅ | Section rendering and title pills are OpenPencil-specific approximations. | +| Rectangles / rounded rectangles | ✅ | ✅ | ✅ | ✅ | ✅ | Per-corner radii and smoothed corners render for fills, strokes, clips, masks, and effects. | +| Ellipses / arcs | ✅ | ✅ | ◐ | ✅ | ✅ | `arcData` renders/exports; no full inspector controls. | +| Lines | ✅ | ✅ | ✅ | ✅ | ✅ | Stroke caps/joins render but are not fully exposed in UI. | +| Polygons / stars | ✅ | ✅ | ◐ | ✅ | ✅ | `pointCount` and `starInnerRadius` modeled. | +| Text | ✅ | ✅ | ◐ | ✅ | ✅ | Derived Figma glyphs improve fidelity; advanced typography is partial. | +| Vectors / vector networks | ✅ | ✅ | ◐ | ✅ | ✅ | Vector edit support exists; Figma Draw tools are not fully replicated. | +| Boolean operations | ✅ | ✅ | ◐ | ✅ | ✅ | Figma `BOOLEAN_OPERATION` nodes import/export as boolean operations; inspector editing remains limited. | +| Components | ✅ | ✅ | ◐ | ✅ | ✅ | Component metadata, descriptions, links, and publish fields mostly round-trip. | +| Component sets / variants | ✅ | ✅ | ◐ | ✅ | ✅ | Variant values are usable; full component property authoring is incomplete. | +| Instances / overrides | ✅ | ✅ | ◐ | ✅ | ✅ | Raw symbol overrides and derived symbol data are preserved for fidelity. | +| Slots | ↩ | ◐ | — | ↩ | — | Some component property payloads may survive round-trip, but Figma slots are not a first-class workflow. | +| Connectors | ◐ | ◐ | — | ◐ | ◐ | Type exists, but Figma connector semantics are weak. | +| Shape-with-text / FigJam shapes | ◐ | ◐ | — | ◐ | ◐ | Type exists, but not a full FigJam feature implementation. | +| Slices | ◐ | — | ◐ | ◐ | ✅ | Slice-like export regions exist via tooling, not as true Figma slice nodes. | +| FigJam / Slides / Code / CMS / Buzz node families | ↩ | — | — | ↩ | — | Current Kiwi schema recognizes many newer Figma node families (`TABLE`, `SLIDE`, `CODE_COMPONENT`, `CMS_RICH_TEXT`, `REPEATER`, `WEBPAGE`, etc.), but OpenPencil only preserves/round-trips them where safe; they are not first-class scene nodes. | +| Solid fills | ✅ | ✅ | ✅ | ✅ | ✅ | Color variables supported for common fill cases. | +| Gradients | ✅ | ✅ | ✅ | ✅ | ✅ | Linear/radial/angular/diamond support; Figma edge cases may differ. | +| Image fills | ✅ | ✅ | ◐ | ✅ | ✅ | Fill/fit/crop/tile support exists; imported crop/tile affine transforms are applied, but exact Figma parity is still partial. | +| Pattern / noise / custom fills | ✅ | ◐ | — | ✅ | — | Schema metadata imports/exports and paints render with a solid fallback color; first-class pattern/noise/custom rendering is still missing. Transformed image tile fills cover only a subset of pattern-like imported fills. | +| Video/GIF/media fills | ↩ | — | — | ↩ | — | Kiwi schema includes media paint/export enums, but OpenPencil has no video/GIF playback or media layer support. | +| Layer/fill/effect blend modes | ✅ | ◐ | — | ✅ | ✅ | Canvas applies node, fill, and common shadow effect blend modes; Figma isolation edge cases remain partial. | +| Opacity | ✅ | ✅ | ✅ | ✅ | ✅ | Node opacity uses save layers in the renderer. | +| Strokes | ✅ | ✅ | ✅ | ✅ | ✅ | Weight, alignment, dashes, and side weights are supported. | +| Stroke caps / joins / miter limit | ✅ | ✅ | ◐ | ✅ | ✅ | Renderer/export support exists; inspector controls are limited. | +| Effects: shadows and blurs | ✅ | ✅ | ✅ | ✅ | ✅ | `showShadowBehindNode` is rendered but not exposed in UI. | +| Effect styles | ↩ | — | — | ↩ | — | Style IDs round-trip; no style manager. | +| Corner radius | ✅ | ✅ | ✅ | ✅ | ✅ | Uniform and independent radii supported. | +| Corner smoothing | ✅ | ✅ | — | ✅ | ✅ | Figma-style smoothed corners render for common uniform and independent-radius rectangles; exact parity still needs broader fixture tuning. | +| Masks | ✅ | ◐ | — | ✅ | ✅ | Figma schema `mask` / `maskType` fields import and export; common sibling alpha/vector/luminance mask stacks render, including consecutive mask layers. UI controls and deeper Figma edge cases remain incomplete. | +| Auto layout: vertical/horizontal | ✅ | ✅ | ✅ | ✅ | ✅ | Yoga-backed layout. | +| Auto layout: wrap | ✅ | ✅ | ✅ | ✅ | ✅ | UI toggle exists. | +| Auto layout: grid | ✅ | ◐ | ◐ | ✅ | ✅ | CSS-grid-like support is partial; newer schema fields for grid child alignment and auto tracks are not fully exposed. | +| Padding / gaps / alignment | ✅ | ✅ | ✅ | ✅ | ✅ | Common flex controls are exposed. | +| Hug / fill / fixed sizing | ✅ | ✅ | ✅ | ✅ | ✅ | Min/max support is partial in UI. | +| Ignore auto layout / absolute positioning | ✅ | ✅ | ◐ | ✅ | ✅ | Mode is modeled; UI coverage is partial. | +| Strokes included in layout | ✅ | ◐ | — | ✅ | ✅ | Stored/exported and used in layout paths, but no obvious panel control. | +| Reverse z-index / align-content | ✅ | ◐ | — | ✅ | ✅ | Modeled and exported; UI is limited. | +| Constraints | ✅ | ◐ | — | ✅ | ✅ | Tools/API expose constraints; main UI is limited. | +| Layout grids / guides | ↩ | — | — | ↩ | — | `styleIdForGrid` and `guides` are preserved only. | +| Text styles | ↩ | ◐ | — | ↩ | — | Style IDs round-trip; no style management UI. Rich schema metadata such as derived text data, leading trim, decoration style/thickness/fill, and semantic font style/weight is preserved for round-trip. | +| Rich style runs | ✅ | ✅ | ◐ | ✅ | ✅ | Import/render/export support; editing mixed runs is partial. | +| Text auto resize | ✅ | ✅ | ◐ | ✅ | ✅ | Used by renderer/layout; UI does not expose every mode. | +| Text truncation / max lines | ✅ | ✅ | — | ✅ | ✅ | Renderer supports ending truncation; no inspector control. | +| Text case | ✅ | ◐ | — | ✅ | ✅ | Model/export/JSX support; UI missing. | +| Vertical text alignment | ✅ | ◐ | — | ✅ | ✅ | Modeled; UI/render parity needs more coverage. | +| Justified text | ✅ | ◐ | — | ✅ | ✅ | Modeled; UI does not expose it. | +| Font variations / OpenType features | ✅ | ✅ | — | ✅ | — | Imported `fontVariations`, common ligature/caps/numeric OpenType fields, and raw `toggledOnOTFeatures` / `toggledOffOTFeatures` are applied to CanvasKit text styles and exported; UI controls are not exposed. | +| Variables: collections/modes/aliases | ✅ | ◐ | ◐ | ✅ | ✅ | Color/number/string/boolean model exists; inspector coverage is still incomplete. | +| Variables bound to fills/strokes | ✅ | ✅ | ✅ | ✅ | ✅ | Common color bindings render and edit. | +| Variables bound to text/layout/visibility/effects | ◐ | ◐ | ◐ | ◐ | ✅ | Some bindings exist; not full Figma property coverage. | +| Variables in prototypes / expressions / conditionals | — | — | — | — | — | Depends on prototype system, which is not implemented. | +| Libraries / publish / update review | ↩ | — | ◐ | ↩ | — | Metadata can survive round-trip; no full library workflow. | +| Prototype flows / starting points | — | — | — | — | — | Not modeled. | +| Prototype hotspots / triggers / actions | — | — | — | — | — | Not modeled. | +| Prototype overlays / scroll-to | — | — | — | — | — | Not modeled. | +| Smart animate / easing / spring / duration | — | — | — | — | — | Not modeled. | +| Interactive components | — | — | — | — | — | Component-level prototype connections are not supported. | +| Dev Mode inspect / measurements / annotations | — | — | — | — | ◐ | OpenPencil has CLI/MCP inspection, but not Figma Dev Mode UI. | +| Code Connect / dev resources / ready-for-dev | — | — | — | — | — | Not modeled. | +| Comments | — | — | — | — | — | Not modeled. | +| Version history / branches | — | — | — | — | — | Not modeled. | +| Real-time collaboration | — | ✅ | ✅ | — | — | OpenPencil has its own P2P collaboration, not Figma-compatible metadata. | ## Raw Kiwi metadata coverage OpenPencil deliberately preserves many Figma/Kiwi fields even when they are not rendered or editable. These live under `SceneNode.source.fig` and are applied late during `.fig` export. A schema coverage test compares the current `fig.kiwi` `NodeChange` fields against modeled codec fields, raw-preserved fields, and intentionally schema-only metadata buckets so drift stays visible. -| Field group | Import/export | Render | UI | Fidelity impact | -|---|---:|---:|---:|---| -| `source.fig.rawSize` | ✅ | Indirect | — | Preserves original Figma size for round-trip. Cleared when size is edited. | -| `source.fig.rawTransform` | ✅ | Indirect | — | Preserves exact Figma transform. Cleared when transform is edited. | -| `source.fig.rawNodeFields` | ✅ | Mixed | — | Late-applied to exported NodeChange for round-trip fidelity; raw-field and schema coverage tests guard preservation drift. | -| `source.fig.layout` | ✅ | ✅ | ◐ | Preserves original Figma stack metadata while using normalized layout fields. | -| `source.fig.symbolOverrides` | ✅ | Indirect | — | Important for instance override fidelity. | -| `source.fig.componentPropAssignments` | ✅ | Indirect | ◐ | Used for component property fidelity; not raw-editable. | -| `source.fig.derivedSymbolData` | ✅ | Indirect | — | Critical for instance-derived geometry/layout/text. | -| `source.fig.derivedSymbolDataLayoutVersion` | ✅ | — | — | Figma bookkeeping. | -| `source.fig.uniformScaleFactor` | ✅ | Indirect | — | Important for scaled instances. | -| Style IDs: fill/stroke/text/effect/grid | ↩ | — | — | Preserves style linkage for Figma, but OpenPencil has no style manager yet. | -| Component property refs/defs/specs | ✅ | Indirect | ◐ | Full Figma component-property authoring is incomplete. | -| State-group metadata | ↩ | — | — | Preserved only. | -| Version/sort/publish/library metadata | ↩ | — | ◐ | Assets UI shows a subset; publish/update workflow is missing. | -| Variable and parameter consumption maps | ✅ | ◐ | ◐ | Filtered/preserved for safe round-trip; normalized bindings cover common cases. | -| Page fields: background, page type, guides | ↩ | ◐ | — | Background/page type/guides mostly round-trip. Guides are not rendered/editable. | -| Text internals: `textData`, layout versions, font version, derived data | ✅ | ✅ | — | Important for text fidelity; most internals are not editable. Imported derived text data, leading trim, decoration style, underline decoration paint/offset/thickness, semantic font metadata, and raw OpenType feature toggles are preserved for round-trip when safe; decoration style/thickness/color now render through CanvasKit. -| `fontVariations` | ✅ | ✅ | — | Variable font axes are imported, rendered, and exported for text nodes and style runs. | -| Raw paint/effect/vector/geometry payloads | ✅ | ✅ | ◐ | Converted fields render; raw payloads preserve Figma import/export details, including mask, background paint, layout grid, export setting, and prototype interaction metadata where safe. | +| Field group | Import/export | Render | UI | Fidelity impact | +| ----------------------------------------------------------------------- | ------------: | -------: | --: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `source.fig.rawSize` | ✅ | Indirect | — | Preserves original Figma size for round-trip. Cleared when size is edited. | +| `source.fig.rawTransform` | ✅ | Indirect | — | Preserves exact Figma transform. Cleared when transform is edited. | +| `source.fig.rawNodeFields` | ✅ | Mixed | — | Late-applied to exported NodeChange for round-trip fidelity; raw-field and schema coverage tests guard preservation drift. | +| `source.fig.layout` | ✅ | ✅ | ◐ | Preserves original Figma stack metadata while using normalized layout fields. | +| `source.fig.symbolOverrides` | ✅ | Indirect | — | Important for instance override fidelity. | +| `source.fig.componentPropAssignments` | ✅ | Indirect | ◐ | Used for component property fidelity; not raw-editable. | +| `source.fig.derivedSymbolData` | ✅ | Indirect | — | Critical for instance-derived geometry/layout/text. | +| `source.fig.derivedSymbolDataLayoutVersion` | ✅ | — | — | Figma bookkeeping. | +| `source.fig.uniformScaleFactor` | ✅ | Indirect | — | Important for scaled instances. | +| Style IDs: fill/stroke/text/effect/grid | ↩ | — | — | Preserves style linkage for Figma, but OpenPencil has no style manager yet. | +| Component property refs/defs/specs | ✅ | Indirect | ◐ | Full Figma component-property authoring is incomplete. | +| State-group metadata | ↩ | — | — | Preserved only. | +| Version/sort/publish/library metadata | ↩ | — | ◐ | Assets UI shows a subset; publish/update workflow is missing. | +| Variable and parameter consumption maps | ✅ | ◐ | ◐ | Filtered/preserved for safe round-trip; normalized bindings cover common cases. | +| Page fields: background, page type, guides | ↩ | ◐ | — | Background/page type/guides mostly round-trip. Guides are not rendered/editable. | +| Text internals: `textData`, layout versions, font version, derived data | ✅ | ✅ | — | Important for text fidelity; most internals are not editable. Imported derived text data, leading trim, decoration style, underline decoration paint/offset/thickness, semantic font metadata, and raw OpenType feature toggles are preserved for round-trip when safe; decoration style/thickness/color now render through CanvasKit. | +| `fontVariations` | ✅ | ✅ | — | Variable font axes are imported, rendered, and exported for text nodes and style runs. | +| Raw paint/effect/vector/geometry payloads | ✅ | ✅ | ◐ | Converted fields render; raw payloads preserve Figma import/export details, including mask, background paint, layout grid, export setting, and prototype interaction metadata where safe. | ## Highest-priority visual gaps @@ -214,20 +214,19 @@ These are parsed or visible in Figma docs and most likely to cause visible diffe ## Code map -| Concern | Files | -|---|---| -| Scene graph fields | `packages/core/src/scene-graph/types.ts` | -| Source metadata invalidation | `packages/core/src/scene-graph/source-metadata.ts` | -| Kiwi import mapping | `packages/core/src/kiwi/fig/node-change/convert.ts` | -| Kiwi export mapping | `packages/core/src/kiwi/fig/node-change/export-node.ts`, `packages/core/src/kiwi/fig/node-change/serialize.ts` | -| Kiwi schema | `packages/core/src/kiwi/fig/codec/schema/fig.kiwi`, `tests/engine/io/fig/import/schema-coverage.test.ts` | -| Renderer dispatch | `packages/core/src/canvas/scene.ts` | -| Fills / images / gradients | `packages/core/src/canvas/fills.ts` | -| Strokes | `packages/core/src/canvas/strokes.ts` | -| Effects / shadows | `packages/core/src/canvas/shadows.ts` | -| Text rendering | `packages/core/src/canvas/text.ts`, `packages/core/src/canvas/text-derived.ts` | -| Layout engine | `packages/core/src/layout.ts`, `packages/core/src/layout/**` | -| Property panels | `src/components/properties/**`, `packages/vue/src/controls/**` | -| CLI | `packages/cli/src/index.ts`, `packages/cli/src/commands/**` | -| MCP/tools | `packages/core/src/tools/**`, `packages/mcp/src/tool/registration.ts` | - +| Concern | Files | +| ---------------------------- | -------------------------------------------------------------------------------------------------------------- | +| Scene graph fields | `packages/core/src/scene-graph/types.ts` | +| Source metadata invalidation | `packages/core/src/scene-graph/source-metadata.ts` | +| Kiwi import mapping | `packages/core/src/kiwi/fig/node-change/convert.ts` | +| Kiwi export mapping | `packages/core/src/kiwi/fig/node-change/export-node.ts`, `packages/core/src/kiwi/fig/node-change/serialize.ts` | +| Kiwi schema | `packages/core/src/kiwi/fig/codec/schema/fig.kiwi`, `tests/engine/io/fig/import/schema-coverage.test.ts` | +| Renderer dispatch | `packages/core/src/canvas/scene.ts` | +| Fills / images / gradients | `packages/core/src/canvas/fills.ts` | +| Strokes | `packages/core/src/canvas/strokes.ts` | +| Effects / shadows | `packages/core/src/canvas/shadows.ts` | +| Text rendering | `packages/core/src/canvas/text.ts`, `packages/core/src/canvas/text-derived.ts` | +| Layout engine | `packages/core/src/layout.ts`, `packages/core/src/layout/**` | +| Property panels | `src/components/properties/**`, `packages/vue/src/controls/**` | +| CLI | `packages/cli/src/index.ts`, `packages/cli/src/commands/**` | +| MCP/tools | `packages/core/src/tools/**`, `packages/mcp/src/tool/registration.ts` | diff --git a/tests/engine/io/fig/export/font-variations.test.ts b/tests/engine/io/fig/export/font-variations.test.ts index 530e770bb..febe5a7a7 100644 --- a/tests/engine/io/fig/export/font-variations.test.ts +++ b/tests/engine/io/fig/export/font-variations.test.ts @@ -25,6 +25,13 @@ describe('Figma font variation export', () => { fontFeatures: [ { tag: 'LIGA', enabled: false }, { tag: 'DLIG', enabled: true }, + { tag: 'HLIG', enabled: false }, + { tag: 'ORDN', enabled: true }, + { tag: 'ZERO', enabled: true }, + { tag: 'ONUM', enabled: true }, + { tag: 'TNUM', enabled: true }, + { tag: 'FRAC', enabled: true }, + { tag: 'SMCP', enabled: true }, { tag: 'KERN', enabled: false } ], styleRuns: [ @@ -56,7 +63,14 @@ describe('Figma font variation export', () => { expect(nodeChange.textDecorationFillPaints?.[0]?.type).toBe('SOLID') expect(nodeChange.fontVariantCommonLigatures).toBe(false) expect(nodeChange.fontVariantContextualLigatures).toBe(true) - expect(nodeChange.toggledOnOTFeatures).toEqual(['DLIG']) + expect(nodeChange.fontVariantDiscretionaryLigatures).toBe(true) + expect(nodeChange.fontVariantHistoricalLigatures).toBe(false) + expect(nodeChange.fontVariantOrdinal).toBe(true) + expect(nodeChange.fontVariantSlashedZero).toBe(true) + expect(nodeChange.fontVariantNumericFigure).toBe('OLDSTYLE') + expect(nodeChange.fontVariantNumericSpacing).toBe('TABULAR') + expect(nodeChange.fontVariantNumericFraction).toBe('DIAGONAL') + expect(nodeChange.fontVariantCaps).toBe('SMALL') expect(nodeChange.toggledOffOTFeatures).toEqual(['KERN']) expect(nodeChange.textData?.styleOverrideTable?.[0]?.fontVariations).toEqual([ { axisTag: 0x77647468, axisName: 'wdth', value: 88 } diff --git a/tests/engine/io/fig/import/font-variations.test.ts b/tests/engine/io/fig/import/font-variations.test.ts index 3198ddd1f..6169588ac 100644 --- a/tests/engine/io/fig/import/font-variations.test.ts +++ b/tests/engine/io/fig/import/font-variations.test.ts @@ -30,7 +30,15 @@ describe('Figma font variation import', () => { textData: { characters: 'Ligatures' }, fontVariantCommonLigatures: false, fontVariantContextualLigatures: true, - toggledOnOTFeatures: ['DLIG'], + fontVariantDiscretionaryLigatures: true, + fontVariantHistoricalLigatures: false, + fontVariantOrdinal: true, + fontVariantSlashedZero: true, + fontVariantNumericFigure: 'OLDSTYLE', + fontVariantNumericSpacing: 'TABULAR', + fontVariantNumericFraction: 'DIAGONAL', + fontVariantCaps: 'SMALL', + toggledOnOTFeatures: ['SS01'], toggledOffOTFeatures: ['KERN'] } as NodeChange, [] @@ -40,6 +48,14 @@ describe('Figma font variation import', () => { { tag: 'LIGA', enabled: false }, { tag: 'CALT', enabled: true }, { tag: 'DLIG', enabled: true }, + { tag: 'HLIG', enabled: false }, + { tag: 'ORDN', enabled: true }, + { tag: 'ZERO', enabled: true }, + { tag: 'ONUM', enabled: true }, + { tag: 'TNUM', enabled: true }, + { tag: 'FRAC', enabled: true }, + { tag: 'SMCP', enabled: true }, + { tag: 'SS01', enabled: true }, { tag: 'KERN', enabled: false } ]) }) diff --git a/tests/engine/io/fig/import/schema-coverage.test.ts b/tests/engine/io/fig/import/schema-coverage.test.ts index ed97c259e..9b8a4a3ff 100644 --- a/tests/engine/io/fig/import/schema-coverage.test.ts +++ b/tests/engine/io/fig/import/schema-coverage.test.ts @@ -373,12 +373,12 @@ describe('Figma Kiwi schema coverage', () => { expect( Object.fromEntries([...buckets].map(([bucket, items]) => [bucket, items.length])) ).toEqual({ - modeled: 104, + modeled: 112, schemaTag: 60, internalBookkeeping: 17, rawPreserved: 52, styleLibraryMetadata: 39, - componentInstanceMetadata: 42, + componentInstanceMetadata: 34, textMetadata: 23, slideFigjamMetadata: 39, visualGeometryMetadata: 38, @@ -405,6 +405,9 @@ describe('Figma Kiwi schema coverage', () => { expect(covered('textDecorationStyle')).toBe(true) expect(covered('semanticWeight')).toBe(true) expect(covered('semanticItalic')).toBe(true) + expect(covered('fontVariantDiscretionaryLigatures')).toBe(true) + expect(covered('fontVariantNumericFigure')).toBe(true) + expect(covered('fontVariantCaps')).toBe(true) expect(covered('toggledOnOTFeatures')).toBe(true) expect(covered('toggledOffOTFeatures')).toBe(true) expect(covered('textDecorationFillPaints')).toBe(true) diff --git a/tests/engine/render/canvas/text-font-variations.test.ts b/tests/engine/render/canvas/text-font-variations.test.ts index 38d361900..b629cf836 100644 --- a/tests/engine/render/canvas/text-font-variations.test.ts +++ b/tests/engine/render/canvas/text-font-variations.test.ts @@ -39,11 +39,13 @@ describe('canvas text font variations', () => { expect( textFontFeatures([ { tag: 'LIGA', enabled: false }, - { tag: 'CALT', enabled: true } + { tag: 'CALT', enabled: true }, + { tag: 'SS01', enabled: true } ]) ).toEqual([ { name: 'liga', value: 0 }, - { name: 'calt', value: 1 } + { name: 'calt', value: 1 }, + { name: 'ss01', value: 1 } ]) })