diff --git a/.changeset/pretty-cats-clap.md b/.changeset/pretty-cats-clap.md new file mode 100644 index 0000000..67474f8 --- /dev/null +++ b/.changeset/pretty-cats-clap.md @@ -0,0 +1,5 @@ +--- +"penpot-exporter": minor +--- + +Use Fill Styles to optimize fills transformations diff --git a/plugin-src/StyleLibrary.ts b/plugin-src/StyleLibrary.ts new file mode 100644 index 0000000..56ef624 --- /dev/null +++ b/plugin-src/StyleLibrary.ts @@ -0,0 +1,27 @@ +import { Fill } from '@ui/lib/types/utils/fill'; + +class StyleLibrary { + private styles: Map = new Map(); + + public register(id: string, styles: Fill[]) { + this.styles.set(id, styles); + } + + public get(id: string): Fill[] | undefined { + return this.styles.get(id); + } + + public has(id: string): boolean { + return this.styles.has(id); + } + + public all(): Record { + return Object.fromEntries(this.styles.entries()); + } + + public init(styles: Record): void { + this.styles = new Map(Object.entries(styles)); + } +} + +export const styleLibrary = new StyleLibrary(); diff --git a/plugin-src/transformers/partials/transformFills.ts b/plugin-src/transformers/partials/transformFills.ts index 6a8df81..b71882f 100644 --- a/plugin-src/transformers/partials/transformFills.ts +++ b/plugin-src/transformers/partials/transformFills.ts @@ -1,11 +1,53 @@ -import { translateFills } from '@plugin/translators/fills'; +import { translateFillStyle, translateFills } from '@plugin/translators/fills'; +import { StyleTextSegment } from '@plugin/translators/text/paragraph'; import { ShapeAttributes } from '@ui/lib/types/shapes/shape'; +import { TextStyle } from '@ui/lib/types/shapes/textShape'; export const transformFills = ( - node: MinimalFillsMixin & DimensionAndPositionMixin -): Pick => { + node: + | (MinimalFillsMixin & DimensionAndPositionMixin) + | VectorRegion + | VectorNode + | StyleTextSegment +): Pick | Pick => { + if (hasFillStyle(node)) { + return { + fills: [], + fillStyleId: translateFillStyle(node.fillStyleId, node.fills) + }; + } + return { fills: translateFills(node.fills) }; }; + +export const transformVectorFills = ( + node: VectorNode, + vectorPath: VectorPath, + vectorRegion: VectorRegion | undefined +): Pick => { + if (vectorPath.windingRule === 'NONE') { + return { + fills: [] + }; + } + + const fillsNode = vectorRegion?.fills ? vectorRegion : node; + return transformFills(fillsNode); +}; + +const hasFillStyle = ( + node: + | (MinimalFillsMixin & DimensionAndPositionMixin) + | VectorRegion + | VectorNode + | StyleTextSegment +): boolean => { + return ( + node.fillStyleId !== figma.mixed && + node.fillStyleId !== undefined && + node.fillStyleId.length > 0 + ); +}; diff --git a/plugin-src/transformers/partials/transformText.ts b/plugin-src/transformers/partials/transformText.ts index 3f86908..207945b 100644 --- a/plugin-src/transformers/partials/transformText.ts +++ b/plugin-src/transformers/partials/transformText.ts @@ -15,7 +15,8 @@ export const transformText = (node: TextNode): TextAttributes & Pick { + if (fillStyleId === figma.mixed || fillStyleId === undefined) return; + + if (!styleLibrary.has(fillStyleId)) { + styleLibrary.register(fillStyleId, translateFills(fills)); + } + + return fillStyleId; +}; + export const translatePageFill = (fill: Paint): string | undefined => { switch (fill.type) { case 'SOLID': diff --git a/plugin-src/translators/text/paragraph/translateParagraphProperties.ts b/plugin-src/translators/text/paragraph/translateParagraphProperties.ts index 6b7f0b7..35153a7 100644 --- a/plugin-src/translators/text/paragraph/translateParagraphProperties.ts +++ b/plugin-src/translators/text/paragraph/translateParagraphProperties.ts @@ -17,6 +17,7 @@ export type StyleTextSegment = Pick< | 'indentation' | 'listOptions' | 'fills' + | 'fillStyleId' >; type PartialTranslation = { diff --git a/plugin-src/translators/text/translateStyleTextSegments.ts b/plugin-src/translators/text/translateStyleTextSegments.ts index cee2132..2112764 100644 --- a/plugin-src/translators/text/translateStyleTextSegments.ts +++ b/plugin-src/translators/text/translateStyleTextSegments.ts @@ -1,4 +1,4 @@ -import { translateFills } from '@plugin/translators/fills'; +import { transformFills } from '@plugin/transformers/partials'; import { translateFontId } from '@plugin/translators/text/font'; import { StyleTextSegment, translateParagraphProperties } from '@plugin/translators/text/paragraph'; import { @@ -41,8 +41,8 @@ export const transformTextStyle = (node: TextNode, segment: StyleTextSegment): T const translateStyleTextSegment = (node: TextNode, segment: StyleTextSegment): PenpotTextNode => { return { - fills: translateFills(segment.fills), text: segment.characters, - ...transformTextStyle(node, segment) + ...transformTextStyle(node, segment), + ...transformFills(segment) }; }; diff --git a/plugin-src/tsconfig.json b/plugin-src/tsconfig.json index 53ad9ac..b4845ab 100644 --- a/plugin-src/tsconfig.json +++ b/plugin-src/tsconfig.json @@ -1,8 +1,8 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { - "target": "ES2017", - "lib": ["ES2017"], + "target": "ES2019", + "lib": ["ES2019"], "strict": true, "typeRoots": ["../node_modules/@figma"], "moduleResolution": "Node", diff --git a/ui-src/lib/types/penpotFile.ts b/ui-src/lib/types/penpotFile.ts index 1526172..20952b4 100644 --- a/ui-src/lib/types/penpotFile.ts +++ b/ui-src/lib/types/penpotFile.ts @@ -7,6 +7,7 @@ import { GroupShape } from '@ui/lib/types/shapes/groupShape'; import { PathShape } from '@ui/lib/types/shapes/pathShape'; import { RectShape } from '@ui/lib/types/shapes/rectShape'; import { TextShape } from '@ui/lib/types/shapes/textShape'; +import { Color } from '@ui/lib/types/utils/color'; import { Uuid } from '@ui/lib/types/utils/uuid'; export interface PenpotFile { @@ -22,9 +23,9 @@ export interface PenpotFile { createCircle(circle: CircleShape): Uuid; createPath(path: PathShape): Uuid; createText(options: TextShape): Uuid; - // addLibraryColor(color: any): void; - // updateLibraryColor(color: any): void; - // deleteLibraryColor(color: any): void; + addLibraryColor(color: Color): void; + updateLibraryColor(color: Color): void; + deleteLibraryColor(color: Color): void; // addLibraryTypography(typography: any): void; // deleteLibraryTypography(typography: any): void; startComponent(component: ComponentShape): Uuid; diff --git a/ui-src/lib/types/shapes/shape.ts b/ui-src/lib/types/shapes/shape.ts index 30d84ca..ed4ddcc 100644 --- a/ui-src/lib/types/shapes/shape.ts +++ b/ui-src/lib/types/shapes/shape.ts @@ -51,6 +51,7 @@ export type ShapeAttributes = { hidden?: boolean; maskedGroup?: boolean; fills?: Fill[]; + fillStyleId?: string; // @TODO: move to any other place hideFillOnExport?: boolean; proportion?: number; proportionLock?: boolean; diff --git a/ui-src/lib/types/shapes/textShape.ts b/ui-src/lib/types/shapes/textShape.ts index 6f08a7a..c6f02b2 100644 --- a/ui-src/lib/types/shapes/textShape.ts +++ b/ui-src/lib/types/shapes/textShape.ts @@ -60,6 +60,7 @@ export type TextStyle = FontId & { textAlign?: TextHorizontalAlign; textDirection?: 'ltr' | 'rtl' | 'auto'; fills?: Fill[]; + fillStyleId?: string; // @TODO: move to any other place }; export type FontId = { diff --git a/ui-src/parser/creators/createArtboard.ts b/ui-src/parser/creators/createArtboard.ts index 90379d2..ff714b9 100644 --- a/ui-src/parser/creators/createArtboard.ts +++ b/ui-src/parser/creators/createArtboard.ts @@ -14,7 +14,7 @@ export const createArtboard = ( shape.id = id; shape.shapeRef ??= parseFigmaId(file, figmaRelatedId, true); - shape.fills = symbolFills(shape.fills); + shape.fills = symbolFills(shape.fillStyleId, shape.fills); shape.strokes = symbolStrokes(shape.strokes); file.addArtboard(shape); diff --git a/ui-src/parser/creators/createBool.ts b/ui-src/parser/creators/createBool.ts index bd79ed7..2ff6199 100644 --- a/ui-src/parser/creators/createBool.ts +++ b/ui-src/parser/creators/createBool.ts @@ -11,7 +11,7 @@ export const createBool = ( ) => { shape.id = parseFigmaId(file, figmaId); shape.shapeRef = parseFigmaId(file, figmaRelatedId, true); - shape.fills = symbolFills(shape.fills); + shape.fills = symbolFills(shape.fillStyleId, shape.fills); shape.strokes = symbolStrokes(shape.strokes); shape.boolType = symbolBoolType(shape.boolType); diff --git a/ui-src/parser/creators/createCircle.ts b/ui-src/parser/creators/createCircle.ts index b1e1a8e..2fea901 100644 --- a/ui-src/parser/creators/createCircle.ts +++ b/ui-src/parser/creators/createCircle.ts @@ -9,7 +9,7 @@ export const createCircle = ( ) => { shape.id = parseFigmaId(file, figmaId); shape.shapeRef = parseFigmaId(file, figmaRelatedId, true); - shape.fills = symbolFills(shape.fills); + shape.fills = symbolFills(shape.fillStyleId, shape.fills); shape.strokes = symbolStrokes(shape.strokes); file.createCircle(shape); diff --git a/ui-src/parser/creators/createComponentsLibrary.ts b/ui-src/parser/creators/createComponentsLibrary.ts index 935e4db..a00837f 100644 --- a/ui-src/parser/creators/createComponentsLibrary.ts +++ b/ui-src/parser/creators/createComponentsLibrary.ts @@ -43,7 +43,7 @@ const createComponentLibrary = async (file: PenpotFile, uiComponent: UiComponent const { children = [], ...shape } = component; - shape.fills = symbolFills(shape.fills); + shape.fills = symbolFills(shape.fillStyleId, shape.fills); shape.strokes = symbolStrokes(shape.strokes); shape.id = uiComponent.componentId; shape.componentId = uiComponent.componentId; diff --git a/ui-src/parser/creators/createPath.ts b/ui-src/parser/creators/createPath.ts index d04bf58..f5848f0 100644 --- a/ui-src/parser/creators/createPath.ts +++ b/ui-src/parser/creators/createPath.ts @@ -9,7 +9,7 @@ export const createPath = ( ) => { shape.id = parseFigmaId(file, figmaId); shape.shapeRef = parseFigmaId(file, figmaRelatedId, true); - shape.fills = symbolFills(shape.fills); + shape.fills = symbolFills(shape.fillStyleId, shape.fills); shape.strokes = symbolStrokes(shape.strokes); shape.content = symbolPathContent(shape.content); diff --git a/ui-src/parser/creators/createRectangle.ts b/ui-src/parser/creators/createRectangle.ts index e17ef11..f4bbb86 100644 --- a/ui-src/parser/creators/createRectangle.ts +++ b/ui-src/parser/creators/createRectangle.ts @@ -9,7 +9,7 @@ export const createRectangle = ( ) => { shape.id = parseFigmaId(file, figmaId); shape.shapeRef = parseFigmaId(file, figmaRelatedId, true); - shape.fills = symbolFills(shape.fills); + shape.fills = symbolFills(shape.fillStyleId, shape.fills); shape.strokes = symbolStrokes(shape.strokes); file.createRect(shape); diff --git a/ui-src/parser/creators/createText.ts b/ui-src/parser/creators/createText.ts index 1310318..cba4501 100644 --- a/ui-src/parser/creators/createText.ts +++ b/ui-src/parser/creators/createText.ts @@ -21,10 +21,10 @@ const parseContent = (content: TextContent | undefined): TextContent | undefined content.children?.forEach(paragraphSet => { paragraphSet.children.forEach(paragraph => { paragraph.children.forEach(textNode => { - textNode.fills = symbolFills(textNode.fills); + textNode.fills = symbolFills(textNode.fillStyleId, textNode.fills); }); - paragraph.fills = symbolFills(paragraph.fills); + paragraph.fills = symbolFills(paragraph.fillStyleId, paragraph.fills); }); }); diff --git a/ui-src/parser/creators/symbols/symbolFills.ts b/ui-src/parser/creators/symbols/symbolFills.ts index 8516ee8..03c6e84 100644 --- a/ui-src/parser/creators/symbols/symbolFills.ts +++ b/ui-src/parser/creators/symbols/symbolFills.ts @@ -1,11 +1,15 @@ +import { styleLibrary } from '@plugin/StyleLibrary'; + import { Fill } from '@ui/lib/types/utils/fill'; import { ImageColor, PartialImageColor } from '@ui/lib/types/utils/imageColor'; import { uiImages } from '@ui/parser/libraries'; -export const symbolFills = (fills?: Fill[]): Fill[] | undefined => { - if (!fills) return; +export const symbolFills = (fillStyleId?: string, fills?: Fill[]): Fill[] | undefined => { + const nodeFills = fillStyleId ? styleLibrary.get(fillStyleId) : fills; - return fills.map(fill => { + if (!nodeFills) return; + + return nodeFills.map(fill => { if (fill.fillImage) { fill.fillImage = symbolFillImage(fill.fillImage); } diff --git a/ui-src/parser/parse.ts b/ui-src/parser/parse.ts index 0376ffc..4413738 100644 --- a/ui-src/parser/parse.ts +++ b/ui-src/parser/parse.ts @@ -1,4 +1,5 @@ import { componentsLibrary } from '@plugin/ComponentLibrary'; +import { styleLibrary } from '@plugin/StyleLibrary'; // @TODO: Direct import on purpose, to avoid problems with the tsc linting import { sleep } from '@plugin/utils/sleep'; @@ -40,8 +41,15 @@ const optimizeImages = async (images: Record) => { } }; -export const parse = async ({ name, children = [], components, images }: PenpotDocument) => { +export const parse = async ({ + name, + children = [], + components, + images, + styles +}: PenpotDocument) => { componentsLibrary.init(components); + styleLibrary.init(styles); await optimizeImages(images); diff --git a/ui-src/types/penpotDocument.ts b/ui-src/types/penpotDocument.ts index c809ed7..b0cbadb 100644 --- a/ui-src/types/penpotDocument.ts +++ b/ui-src/types/penpotDocument.ts @@ -1,9 +1,11 @@ import { PenpotPage } from '@ui/lib/types/penpotPage'; import { ComponentShape } from '@ui/lib/types/shapes/componentShape'; +import { Fill } from '@ui/lib/types/utils/fill'; export type PenpotDocument = { name: string; children?: PenpotPage[]; components: Record; images: Record; + styles: Record; };