0
Fork 0
mirror of https://github.com/penpot/penpot-exporter-figma-plugin.git synced 2024-12-22 13:43:03 -05:00

Use Fill Styles to generate fills more efficiently (#180)

* first commit

* change esbuild

* changeset
This commit is contained in:
Alex Sánchez 2024-06-19 15:58:13 +02:00 committed by GitHub
parent 4e5d01adb3
commit 672567614b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 137 additions and 29 deletions

View file

@ -0,0 +1,5 @@
---
"penpot-exporter": minor
---
Use Fill Styles to optimize fills transformations

View file

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

View file

@ -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 { ShapeAttributes } from '@ui/lib/types/shapes/shape';
import { TextStyle } from '@ui/lib/types/shapes/textShape';
export const transformFills = ( export const transformFills = (
node: MinimalFillsMixin & DimensionAndPositionMixin node:
): Pick<ShapeAttributes, 'fills'> => { | (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 { return {
fills: translateFills(node.fills) 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
);
};

View file

@ -15,7 +15,8 @@ export const transformText = (node: TextNode): TextAttributes & Pick<TextShape,
'textDecoration', 'textDecoration',
'indentation', 'indentation',
'listOptions', 'listOptions',
'fills' 'fills',
'fillStyleId'
]); ]);
return { return {

View file

@ -6,9 +6,9 @@ import {
transformLayoutAttributes, transformLayoutAttributes,
transformProportion, transformProportion,
transformSceneNode, transformSceneNode,
transformStrokesFromVector transformStrokesFromVector,
transformVectorFills
} from '@plugin/transformers/partials'; } from '@plugin/transformers/partials';
import { translateFills } from '@plugin/translators/fills';
import { translateCommands, translateWindingRule } from '@plugin/translators/vectors'; import { translateCommands, translateWindingRule } from '@plugin/translators/vectors';
import { PathShape } from '@ui/lib/types/shapes/pathShape'; import { PathShape } from '@ui/lib/types/shapes/pathShape';
@ -66,13 +66,12 @@ const transformVectorPath = (
type: 'path', type: 'path',
name: 'svg-path', name: 'svg-path',
content: translateCommands(node, normalizedPaths), content: translateCommands(node, normalizedPaths),
fills:
vectorPath.windingRule === 'NONE' ? [] : translateFills(vectorRegion?.fills ?? node.fills),
svgAttrs: { svgAttrs: {
fillRule: translateWindingRule(vectorPath.windingRule) fillRule: translateWindingRule(vectorPath.windingRule)
}, },
constraintsH: 'scale', constraintsH: 'scale',
constraintsV: 'scale', constraintsV: 'scale',
...transformVectorFills(node, vectorPath, vectorRegion),
...transformStrokesFromVector(node, normalizedPaths, vectorRegion), ...transformStrokesFromVector(node, normalizedPaths, vectorRegion),
...transformEffects(node), ...transformEffects(node),
...transformSceneNode(node), ...transformSceneNode(node),

View file

@ -1,6 +1,7 @@
import { componentsLibrary } from '@plugin/ComponentLibrary'; import { componentsLibrary } from '@plugin/ComponentLibrary';
import { imagesLibrary } from '@plugin/ImageLibrary'; import { imagesLibrary } from '@plugin/ImageLibrary';
import { remoteComponentLibrary } from '@plugin/RemoteComponentLibrary'; import { remoteComponentLibrary } from '@plugin/RemoteComponentLibrary';
import { styleLibrary } from '@plugin/StyleLibrary';
import { translateRemoteChildren } from '@plugin/translators'; import { translateRemoteChildren } from '@plugin/translators';
import { sleep } from '@plugin/utils'; import { sleep } from '@plugin/utils';
@ -86,6 +87,7 @@ export const transformDocumentNode = async (node: DocumentNode): Promise<PenpotD
name: node.name, name: node.name,
children, children,
components: componentsLibrary.all(), components: componentsLibrary.all(),
images: await downloadImages() images: await downloadImages(),
styles: styleLibrary.all()
}; };
}; };

View file

@ -1,3 +1,4 @@
import { styleLibrary } from '@plugin/StyleLibrary';
import { translateImageFill, translateSolidFill } from '@plugin/translators/fills'; import { translateImageFill, translateSolidFill } from '@plugin/translators/fills';
import { import {
translateGradientLinearFill, translateGradientLinearFill,
@ -41,6 +42,19 @@ export const translateFills = (
return penpotFills; 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 => { export const translatePageFill = (fill: Paint): string | undefined => {
switch (fill.type) { switch (fill.type) {
case 'SOLID': case 'SOLID':

View file

@ -17,6 +17,7 @@ export type StyleTextSegment = Pick<
| 'indentation' | 'indentation'
| 'listOptions' | 'listOptions'
| 'fills' | 'fills'
| 'fillStyleId'
>; >;
type PartialTranslation = { type PartialTranslation = {

View file

@ -1,4 +1,4 @@
import { translateFills } from '@plugin/translators/fills'; import { transformFills } from '@plugin/transformers/partials';
import { translateFontId } from '@plugin/translators/text/font'; import { translateFontId } from '@plugin/translators/text/font';
import { StyleTextSegment, translateParagraphProperties } from '@plugin/translators/text/paragraph'; import { StyleTextSegment, translateParagraphProperties } from '@plugin/translators/text/paragraph';
import { import {
@ -41,8 +41,8 @@ export const transformTextStyle = (node: TextNode, segment: StyleTextSegment): T
const translateStyleTextSegment = (node: TextNode, segment: StyleTextSegment): PenpotTextNode => { const translateStyleTextSegment = (node: TextNode, segment: StyleTextSegment): PenpotTextNode => {
return { return {
fills: translateFills(segment.fills),
text: segment.characters, text: segment.characters,
...transformTextStyle(node, segment) ...transformTextStyle(node, segment),
...transformFills(segment)
}; };
}; };

View file

@ -1,8 +1,8 @@
{ {
"extends": "../tsconfig.base.json", "extends": "../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"target": "ES2017", "target": "ES2019",
"lib": ["ES2017"], "lib": ["ES2019"],
"strict": true, "strict": true,
"typeRoots": ["../node_modules/@figma"], "typeRoots": ["../node_modules/@figma"],
"moduleResolution": "Node", "moduleResolution": "Node",

View file

@ -7,6 +7,7 @@ import { GroupShape } from '@ui/lib/types/shapes/groupShape';
import { PathShape } from '@ui/lib/types/shapes/pathShape'; import { PathShape } from '@ui/lib/types/shapes/pathShape';
import { RectShape } from '@ui/lib/types/shapes/rectShape'; import { RectShape } from '@ui/lib/types/shapes/rectShape';
import { TextShape } from '@ui/lib/types/shapes/textShape'; import { TextShape } from '@ui/lib/types/shapes/textShape';
import { Color } from '@ui/lib/types/utils/color';
import { Uuid } from '@ui/lib/types/utils/uuid'; import { Uuid } from '@ui/lib/types/utils/uuid';
export interface PenpotFile { export interface PenpotFile {
@ -22,9 +23,9 @@ export interface PenpotFile {
createCircle(circle: CircleShape): Uuid; createCircle(circle: CircleShape): Uuid;
createPath(path: PathShape): Uuid; createPath(path: PathShape): Uuid;
createText(options: TextShape): Uuid; createText(options: TextShape): Uuid;
// addLibraryColor(color: any): void; addLibraryColor(color: Color): void;
// updateLibraryColor(color: any): void; updateLibraryColor(color: Color): void;
// deleteLibraryColor(color: any): void; deleteLibraryColor(color: Color): void;
// addLibraryTypography(typography: any): void; // addLibraryTypography(typography: any): void;
// deleteLibraryTypography(typography: any): void; // deleteLibraryTypography(typography: any): void;
startComponent(component: ComponentShape): Uuid; startComponent(component: ComponentShape): Uuid;

View file

@ -51,6 +51,7 @@ export type ShapeAttributes = {
hidden?: boolean; hidden?: boolean;
maskedGroup?: boolean; maskedGroup?: boolean;
fills?: Fill[]; fills?: Fill[];
fillStyleId?: string; // @TODO: move to any other place
hideFillOnExport?: boolean; hideFillOnExport?: boolean;
proportion?: number; proportion?: number;
proportionLock?: boolean; proportionLock?: boolean;

View file

@ -60,6 +60,7 @@ export type TextStyle = FontId & {
textAlign?: TextHorizontalAlign; textAlign?: TextHorizontalAlign;
textDirection?: 'ltr' | 'rtl' | 'auto'; textDirection?: 'ltr' | 'rtl' | 'auto';
fills?: Fill[]; fills?: Fill[];
fillStyleId?: string; // @TODO: move to any other place
}; };
export type FontId = { export type FontId = {

View file

@ -14,7 +14,7 @@ export const createArtboard = (
shape.id = id; shape.id = id;
shape.shapeRef ??= parseFigmaId(file, figmaRelatedId, true); shape.shapeRef ??= parseFigmaId(file, figmaRelatedId, true);
shape.fills = symbolFills(shape.fills); shape.fills = symbolFills(shape.fillStyleId, shape.fills);
shape.strokes = symbolStrokes(shape.strokes); shape.strokes = symbolStrokes(shape.strokes);
file.addArtboard(shape); file.addArtboard(shape);

View file

@ -11,7 +11,7 @@ export const createBool = (
) => { ) => {
shape.id = parseFigmaId(file, figmaId); shape.id = parseFigmaId(file, figmaId);
shape.shapeRef = parseFigmaId(file, figmaRelatedId, true); shape.shapeRef = parseFigmaId(file, figmaRelatedId, true);
shape.fills = symbolFills(shape.fills); shape.fills = symbolFills(shape.fillStyleId, shape.fills);
shape.strokes = symbolStrokes(shape.strokes); shape.strokes = symbolStrokes(shape.strokes);
shape.boolType = symbolBoolType(shape.boolType); shape.boolType = symbolBoolType(shape.boolType);

View file

@ -9,7 +9,7 @@ export const createCircle = (
) => { ) => {
shape.id = parseFigmaId(file, figmaId); shape.id = parseFigmaId(file, figmaId);
shape.shapeRef = parseFigmaId(file, figmaRelatedId, true); shape.shapeRef = parseFigmaId(file, figmaRelatedId, true);
shape.fills = symbolFills(shape.fills); shape.fills = symbolFills(shape.fillStyleId, shape.fills);
shape.strokes = symbolStrokes(shape.strokes); shape.strokes = symbolStrokes(shape.strokes);
file.createCircle(shape); file.createCircle(shape);

View file

@ -43,7 +43,7 @@ const createComponentLibrary = async (file: PenpotFile, uiComponent: UiComponent
const { children = [], ...shape } = component; const { children = [], ...shape } = component;
shape.fills = symbolFills(shape.fills); shape.fills = symbolFills(shape.fillStyleId, shape.fills);
shape.strokes = symbolStrokes(shape.strokes); shape.strokes = symbolStrokes(shape.strokes);
shape.id = uiComponent.componentId; shape.id = uiComponent.componentId;
shape.componentId = uiComponent.componentId; shape.componentId = uiComponent.componentId;

View file

@ -9,7 +9,7 @@ export const createPath = (
) => { ) => {
shape.id = parseFigmaId(file, figmaId); shape.id = parseFigmaId(file, figmaId);
shape.shapeRef = parseFigmaId(file, figmaRelatedId, true); shape.shapeRef = parseFigmaId(file, figmaRelatedId, true);
shape.fills = symbolFills(shape.fills); shape.fills = symbolFills(shape.fillStyleId, shape.fills);
shape.strokes = symbolStrokes(shape.strokes); shape.strokes = symbolStrokes(shape.strokes);
shape.content = symbolPathContent(shape.content); shape.content = symbolPathContent(shape.content);

View file

@ -9,7 +9,7 @@ export const createRectangle = (
) => { ) => {
shape.id = parseFigmaId(file, figmaId); shape.id = parseFigmaId(file, figmaId);
shape.shapeRef = parseFigmaId(file, figmaRelatedId, true); shape.shapeRef = parseFigmaId(file, figmaRelatedId, true);
shape.fills = symbolFills(shape.fills); shape.fills = symbolFills(shape.fillStyleId, shape.fills);
shape.strokes = symbolStrokes(shape.strokes); shape.strokes = symbolStrokes(shape.strokes);
file.createRect(shape); file.createRect(shape);

View file

@ -21,10 +21,10 @@ const parseContent = (content: TextContent | undefined): TextContent | undefined
content.children?.forEach(paragraphSet => { content.children?.forEach(paragraphSet => {
paragraphSet.children.forEach(paragraph => { paragraphSet.children.forEach(paragraph => {
paragraph.children.forEach(textNode => { 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);
}); });
}); });

View file

@ -1,11 +1,15 @@
import { styleLibrary } from '@plugin/StyleLibrary';
import { Fill } from '@ui/lib/types/utils/fill'; import { Fill } from '@ui/lib/types/utils/fill';
import { ImageColor, PartialImageColor } from '@ui/lib/types/utils/imageColor'; import { ImageColor, PartialImageColor } from '@ui/lib/types/utils/imageColor';
import { uiImages } from '@ui/parser/libraries'; import { uiImages } from '@ui/parser/libraries';
export const symbolFills = (fills?: Fill[]): Fill[] | undefined => { export const symbolFills = (fillStyleId?: string, fills?: Fill[]): Fill[] | undefined => {
if (!fills) return; const nodeFills = fillStyleId ? styleLibrary.get(fillStyleId) : fills;
return fills.map(fill => { if (!nodeFills) return;
return nodeFills.map(fill => {
if (fill.fillImage) { if (fill.fillImage) {
fill.fillImage = symbolFillImage(fill.fillImage); fill.fillImage = symbolFillImage(fill.fillImage);
} }

View file

@ -1,4 +1,5 @@
import { componentsLibrary } from '@plugin/ComponentLibrary'; import { componentsLibrary } from '@plugin/ComponentLibrary';
import { styleLibrary } from '@plugin/StyleLibrary';
// @TODO: Direct import on purpose, to avoid problems with the tsc linting // @TODO: Direct import on purpose, to avoid problems with the tsc linting
import { sleep } from '@plugin/utils/sleep'; import { sleep } from '@plugin/utils/sleep';
@ -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); componentsLibrary.init(components);
styleLibrary.init(styles);
await optimizeImages(images); await optimizeImages(images);

View file

@ -1,9 +1,11 @@
import { PenpotPage } from '@ui/lib/types/penpotPage'; import { PenpotPage } from '@ui/lib/types/penpotPage';
import { ComponentShape } from '@ui/lib/types/shapes/componentShape'; import { ComponentShape } from '@ui/lib/types/shapes/componentShape';
import { Fill } from '@ui/lib/types/utils/fill';
export type PenpotDocument = { export type PenpotDocument = {
name: string; name: string;
children?: PenpotPage[]; children?: PenpotPage[];
components: Record<string, ComponentShape>; components: Record<string, ComponentShape>;
images: Record<string, Uint8Array>; images: Record<string, Uint8Array>;
styles: Record<string, Fill[]>;
}; };