0
Fork 0
mirror of https://github.com/penpot/penpot-exporter-figma-plugin.git synced 2024-12-21 21:23:06 -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 { 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
);
};

View file

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

View file

@ -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';
@ -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),

View file

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

View file

@ -1,3 +1,4 @@
import { styleLibrary } from '@plugin/StyleLibrary';
import { translateImageFill, translateSolidFill } from '@plugin/translators/fills';
import {
translateGradientLinearFill,
@ -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':

View file

@ -17,6 +17,7 @@ export type StyleTextSegment = Pick<
| 'indentation'
| 'listOptions'
| 'fills'
| 'fillStyleId'
>;
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 { 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)
};
};

View file

@ -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",

View file

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

View file

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

View file

@ -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 = {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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[]>;
};