mirror of
https://github.com/penpot/penpot-exporter-figma-plugin.git
synced 2024-12-22 05:33:02 -05:00
Use Fill Styles to generate fills more efficiently (#180)
* first commit * change esbuild * changeset
This commit is contained in:
parent
4e5d01adb3
commit
672567614b
23 changed files with 137 additions and 29 deletions
5
.changeset/pretty-cats-clap.md
Normal file
5
.changeset/pretty-cats-clap.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"penpot-exporter": minor
|
||||
---
|
||||
|
||||
Use Fill Styles to optimize fills transformations
|
27
plugin-src/StyleLibrary.ts
Normal file
27
plugin-src/StyleLibrary.ts
Normal 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();
|
|
@ -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
|
||||
);
|
||||
};
|
||||
|
|
|
@ -15,7 +15,8 @@ export const transformText = (node: TextNode): TextAttributes & Pick<TextShape,
|
|||
'textDecoration',
|
||||
'indentation',
|
||||
'listOptions',
|
||||
'fills'
|
||||
'fills',
|
||||
'fillStyleId'
|
||||
]);
|
||||
|
||||
return {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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()
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -17,6 +17,7 @@ export type StyleTextSegment = Pick<
|
|||
| 'indentation'
|
||||
| 'listOptions'
|
||||
| 'fills'
|
||||
| 'fillStyleId'
|
||||
>;
|
||||
|
||||
type PartialTranslation = {
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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[]>;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue