fix(kiwi): support Figma OpenType variant fields
This commit is contained in:
parent
a53ab45e86
commit
c2a0de5f0c
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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<Record<string, (typeof BOOLEAN_FEATURES)[number][0]>>
|
||||
|
||||
const ENUM_FEATURE_EXPORT: Partial<
|
||||
Record<string, { field: (typeof ENUM_FEATURES)[number][0]; value: string }>
|
||||
> = {
|
||||
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<Record<string, string>>)[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
|
||||
|
|
|
|||
|
|
@ -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` |
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
])
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
])
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue