Skip to content

Commit

Permalink
Use Fill Styles to generate fills more efficiently (#180)
Browse files Browse the repository at this point in the history
* first commit

* change esbuild

* changeset
  • Loading branch information
Cenadros authored Jun 19, 2024
1 parent 4e5d01a commit 6725676
Show file tree
Hide file tree
Showing 23 changed files with 137 additions and 29 deletions.
5 changes: 5 additions & 0 deletions .changeset/pretty-cats-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"penpot-exporter": minor
---

Use Fill Styles to optimize fills transformations
27 changes: 27 additions & 0 deletions plugin-src/StyleLibrary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Fill } from '@ui/lib/types/utils/fill';

class StyleLibrary {
private styles: Map<string, Fill[]> = 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<string, Fill[]> {
return Object.fromEntries(this.styles.entries());
}

public init(styles: Record<string, Fill[]>): void {
this.styles = new Map(Object.entries(styles));
}
}

export const styleLibrary = new StyleLibrary();
48 changes: 45 additions & 3 deletions plugin-src/transformers/partials/transformFills.ts
Original file line number Diff line number Diff line change
@@ -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<ShapeAttributes, 'fills'> => {
node:
| (MinimalFillsMixin & DimensionAndPositionMixin)
| VectorRegion
| VectorNode
| StyleTextSegment
): Pick<ShapeAttributes, 'fills' | 'fillStyleId'> | Pick<TextStyle, 'fills' | 'fillStyleId'> => {
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<ShapeAttributes, 'fills' | 'fillStyleId'> => {
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
);
};
3 changes: 2 additions & 1 deletion plugin-src/transformers/partials/transformText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export const transformText = (node: TextNode): TextAttributes & Pick<TextShape,
'textDecoration',
'indentation',
'listOptions',
'fills'
'fills',
'fillStyleId'
]);

return {
Expand Down
7 changes: 3 additions & 4 deletions plugin-src/transformers/partials/transformVectorPaths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import {
transformLayoutAttributes,
transformProportion,
transformSceneNode,
transformStrokesFromVector
transformStrokesFromVector,
transformVectorFills
} from '@plugin/transformers/partials';
import { translateFills } from '@plugin/translators/fills';
import { translateCommands, translateWindingRule } from '@plugin/translators/vectors';

import { PathShape } from '@ui/lib/types/shapes/pathShape';
Expand Down Expand Up @@ -66,13 +66,12 @@ const transformVectorPath = (
type: 'path',
name: 'svg-path',
content: translateCommands(node, normalizedPaths),
fills:
vectorPath.windingRule === 'NONE' ? [] : translateFills(vectorRegion?.fills ?? node.fills),
svgAttrs: {
fillRule: translateWindingRule(vectorPath.windingRule)
},
constraintsH: 'scale',
constraintsV: 'scale',
...transformVectorFills(node, vectorPath, vectorRegion),
...transformStrokesFromVector(node, normalizedPaths, vectorRegion),
...transformEffects(node),
...transformSceneNode(node),
Expand Down
4 changes: 3 additions & 1 deletion plugin-src/transformers/transformDocumentNode.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { componentsLibrary } from '@plugin/ComponentLibrary';
import { imagesLibrary } from '@plugin/ImageLibrary';
import { remoteComponentLibrary } from '@plugin/RemoteComponentLibrary';
import { styleLibrary } from '@plugin/StyleLibrary';
import { translateRemoteChildren } from '@plugin/translators';
import { sleep } from '@plugin/utils';

Expand Down Expand Up @@ -86,6 +87,7 @@ export const transformDocumentNode = async (node: DocumentNode): Promise<PenpotD
name: node.name,
children,
components: componentsLibrary.all(),
images: await downloadImages()
images: await downloadImages(),
styles: styleLibrary.all()
};
};
14 changes: 14 additions & 0 deletions plugin-src/translators/fills/translateFills.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { styleLibrary } from '@plugin/StyleLibrary';
import { translateImageFill, translateSolidFill } from '@plugin/translators/fills';
import {
translateGradientLinearFill,
Expand Down Expand Up @@ -41,6 +42,19 @@ export const translateFills = (
return penpotFills;
};

export const translateFillStyle = (
fillStyleId: string | typeof figma.mixed | undefined,
fills: readonly Paint[] | typeof figma.mixed | undefined
): string | undefined => {
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':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type StyleTextSegment = Pick<
| 'indentation'
| 'listOptions'
| 'fills'
| 'fillStyleId'
>;

type PartialTranslation = {
Expand Down
6 changes: 3 additions & 3 deletions plugin-src/translators/text/translateStyleTextSegments.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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)
};
};
4 changes: 2 additions & 2 deletions plugin-src/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
7 changes: 4 additions & 3 deletions ui-src/lib/types/penpotFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions ui-src/lib/types/shapes/shape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions ui-src/lib/types/shapes/textShape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
2 changes: 1 addition & 1 deletion ui-src/parser/creators/createArtboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion ui-src/parser/creators/createBool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
2 changes: 1 addition & 1 deletion ui-src/parser/creators/createCircle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion ui-src/parser/creators/createComponentsLibrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion ui-src/parser/creators/createPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
2 changes: 1 addition & 1 deletion ui-src/parser/creators/createRectangle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions ui-src/parser/creators/createText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});

Expand Down
10 changes: 7 additions & 3 deletions ui-src/parser/creators/symbols/symbolFills.ts
Original file line number Diff line number Diff line change
@@ -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;

if (!nodeFills) return;

return fills.map(fill => {
return nodeFills.map(fill => {
if (fill.fillImage) {
fill.fillImage = symbolFillImage(fill.fillImage);
}
Expand Down
10 changes: 9 additions & 1 deletion ui-src/parser/parse.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -40,8 +41,15 @@ const optimizeImages = async (images: Record<string, Uint8Array>) => {
}
};

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);

Expand Down
2 changes: 2 additions & 0 deletions ui-src/types/penpotDocument.ts
Original file line number Diff line number Diff line change
@@ -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<string, ComponentShape>;
images: Record<string, Uint8Array>;
styles: Record<string, Fill[]>;
};

0 comments on commit 6725676

Please sign in to comment.