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

Color libraries (#183)

* wip

* color library

* fixes

* fixes

* fixes

* fixes

* changeset

* fixes

* fixes

* fixes

* fixes
This commit is contained in:
Alex Sánchez 2024-06-25 14:12:37 +02:00 committed by GitHub
parent b652af158e
commit a58f9e913d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 248 additions and 35 deletions

View file

@ -0,0 +1,5 @@
---
"penpot-exporter": minor
---
Support for color libraries

View file

@ -1,13 +1,11 @@
import { Fill } from '@ui/lib/types/utils/fill';
class StyleLibrary { class StyleLibrary {
private styles: Map<string, Fill[]> = new Map(); private styles: Map<string, PaintStyle | undefined> = new Map();
public register(id: string, styles: Fill[]) { public register(id: string, styles?: PaintStyle | undefined) {
this.styles.set(id, styles); this.styles.set(id, styles);
} }
public get(id: string): Fill[] | undefined { public get(id: string): PaintStyle | undefined {
return this.styles.get(id); return this.styles.get(id);
} }
@ -15,13 +13,9 @@ class StyleLibrary {
return this.styles.has(id); return this.styles.has(id);
} }
public all(): Record<string, Fill[]> { public all(): Record<string, PaintStyle | undefined> {
return Object.fromEntries(this.styles.entries()); 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(); export const styleLibrary = new StyleLibrary();

View file

@ -1,4 +1,4 @@
import { translateFillStyle, translateFills } from '@plugin/translators/fills'; import { translateFillStyleId, translateFills } from '@plugin/translators/fills';
import { StyleTextSegment } from '@plugin/translators/text/paragraph'; import { StyleTextSegment } from '@plugin/translators/text/paragraph';
import { ShapeAttributes } from '@ui/lib/types/shapes/shape'; import { ShapeAttributes } from '@ui/lib/types/shapes/shape';
@ -14,7 +14,7 @@ export const transformFills = (
if (hasFillStyle(node)) { if (hasFillStyle(node)) {
return { return {
fills: [], fills: [],
fillStyleId: translateFillStyle(node.fillStyleId, node.fills) fillStyleId: translateFillStyleId(node.fillStyleId)
}; };
} }

View file

@ -3,9 +3,11 @@ import { imagesLibrary } from '@plugin/ImageLibrary';
import { remoteComponentLibrary } from '@plugin/RemoteComponentLibrary'; import { remoteComponentLibrary } from '@plugin/RemoteComponentLibrary';
import { styleLibrary } from '@plugin/StyleLibrary'; import { styleLibrary } from '@plugin/StyleLibrary';
import { translateRemoteChildren } from '@plugin/translators'; import { translateRemoteChildren } from '@plugin/translators';
import { translatePaintStyles } from '@plugin/translators/fills';
import { sleep } from '@plugin/utils'; import { sleep } from '@plugin/utils';
import { PenpotPage } from '@ui/lib/types/penpotPage'; import { PenpotPage } from '@ui/lib/types/penpotPage';
import { FillStyle } from '@ui/lib/types/utils/fill';
import { PenpotDocument } from '@ui/types'; import { PenpotDocument } from '@ui/types';
import { transformPageNode } from '.'; import { transformPageNode } from '.';
@ -48,6 +50,47 @@ const downloadImages = async (): Promise<Record<string, Uint8Array>> => {
return images; return images;
}; };
const getFillStyles = async (): Promise<Record<string, FillStyle>> => {
const stylesToFetch = Object.entries(styleLibrary.all());
const styles: Record<string, FillStyle> = {};
if (stylesToFetch.length === 0) return styles;
let currentStyle = 1;
figma.ui.postMessage({
type: 'PROGRESS_TOTAL_ITEMS',
data: stylesToFetch.length
});
figma.ui.postMessage({
type: 'PROGRESS_STEP',
data: 'fills'
});
for (const [styleId, paintStyle] of stylesToFetch) {
const figmaStyle = paintStyle ?? (await figma.getStyleByIdAsync(styleId));
if (figmaStyle && isPaintStyle(figmaStyle)) {
styles[styleId] = translatePaintStyles(figmaStyle);
}
figma.ui.postMessage({
type: 'PROGRESS_PROCESSED_ITEMS',
data: currentStyle++
});
await sleep(0);
}
await sleep(20);
return styles;
};
const isPaintStyle = (style: BaseStyle): style is PaintStyle => {
return style.type === 'PAINT';
};
const processPages = async (node: DocumentNode): Promise<PenpotPage[]> => { const processPages = async (node: DocumentNode): Promise<PenpotPage[]> => {
const children = []; const children = [];
let currentPage = 1; let currentPage = 1;
@ -74,6 +117,11 @@ const processPages = async (node: DocumentNode): Promise<PenpotPage[]> => {
}; };
export const transformDocumentNode = async (node: DocumentNode): Promise<PenpotDocument> => { export const transformDocumentNode = async (node: DocumentNode): Promise<PenpotDocument> => {
const localPaintStyles = await figma.getLocalPaintStylesAsync();
localPaintStyles.forEach(style => {
styleLibrary.register(style.id, style);
});
const children = await processPages(node); const children = await processPages(node);
if (remoteComponentLibrary.remaining() > 0) { if (remoteComponentLibrary.remaining() > 0) {
@ -83,11 +131,15 @@ export const transformDocumentNode = async (node: DocumentNode): Promise<PenpotD
}); });
} }
const styles = await getFillStyles();
const images = await downloadImages();
return { return {
name: node.name, name: node.name,
children, children,
components: componentsLibrary.all(), components: componentsLibrary.all(),
images: await downloadImages(), images,
styles: styleLibrary.all() styles
}; };
}; };

View file

@ -1,3 +1,4 @@
export * from './translateFills'; export * from './translateFills';
export * from './translateImageFill'; export * from './translateImageFill';
export * from './translatePaintStyles';
export * from './translateSolidFill'; export * from './translateSolidFill';

View file

@ -42,14 +42,13 @@ export const translateFills = (
return penpotFills; return penpotFills;
}; };
export const translateFillStyle = ( export const translateFillStyleId = (
fillStyleId: string | typeof figma.mixed | undefined, fillStyleId: string | typeof figma.mixed | undefined
fills: readonly Paint[] | typeof figma.mixed | undefined
): string | undefined => { ): string | undefined => {
if (fillStyleId === figma.mixed || fillStyleId === undefined) return; if (fillStyleId === figma.mixed || fillStyleId === undefined) return;
if (!styleLibrary.has(fillStyleId)) { if (!styleLibrary.has(fillStyleId)) {
styleLibrary.register(fillStyleId, translateFills(fills)); styleLibrary.register(fillStyleId);
} }
return fillStyleId; return fillStyleId;

View file

@ -0,0 +1,35 @@
import { translateFill } from '@plugin/translators/fills/translateFills';
import { FillStyle } from '@ui/lib/types/utils/fill';
export const translatePaintStyles = (figmaStyle: PaintStyle): FillStyle => {
const fillStyle: FillStyle = {
name: figmaStyle.name,
fills: [],
colors: []
};
const colorName = (figmaStyle: PaintStyle, index: number): string => {
// @TODO: Think something better
return figmaStyle.paints.length > 1 ? `Color ${index + 1}` : figmaStyle.name;
};
let index = 0;
const path =
(figmaStyle.remote ? 'Remote / ' : '') + (figmaStyle.paints.length > 1 ? figmaStyle.name : '');
for (const fill of figmaStyle.paints) {
const penpotFill = translateFill(fill);
if (penpotFill) {
fillStyle.fills.unshift(penpotFill);
fillStyle.colors.unshift({
path,
name: colorName(figmaStyle, index)
});
}
index++;
}
return fillStyle;
};

View file

@ -29,6 +29,15 @@ const stepMessages: Record<Steps, Messages> = {
total: 'pages built 🏗️', total: 'pages built 🏗️',
current: 'Currently processing layer' current: 'Currently processing layer'
}, },
fills: {
total: 'color libraries fetched 🎨'
},
format: {
total: 'formatting color libraries 🎨'
},
libraries: {
total: 'color libraries built 🎨'
},
components: { components: {
total: 'components built 🏗️', total: 'components built 🏗️',
current: 'Currently processing layer' current: 'Currently processing layer'
@ -60,7 +69,10 @@ const StepProgress = (): JSX.Element | null => {
case 'images': case 'images':
case 'optimization': case 'optimization':
case 'building': case 'building':
case 'fills':
case 'components': case 'components':
case 'format':
case 'libraries':
return ( return (
<> <>
{processedItems} of {totalItems} {stepMessages[step].total} {processedItems} of {totalItems} {stepMessages[step].total}

View file

@ -26,7 +26,10 @@ export type Steps =
| 'optimization' | 'optimization'
| 'building' | 'building'
| 'components' | 'components'
| 'exporting'; | 'exporting'
| 'fills'
| 'format'
| 'libraries';
export const useFigma = (): UseFigmaHook => { export const useFigma = (): UseFigmaHook => {
const [missingFonts, setMissingFonts] = useState<string[]>(); const [missingFonts, setMissingFonts] = useState<string[]>();

View file

@ -1,5 +1,5 @@
import { Gradient } from './gradient'; import { Gradient } from './gradient';
import { ImageColor } from './imageColor'; import { ImageColor, PartialImageColor } from './imageColor';
import { Uuid } from './uuid'; import { Uuid } from './uuid';
export type Color = { export type Color = {
@ -13,5 +13,5 @@ export type Color = {
refId?: Uuid; refId?: Uuid;
refFile?: Uuid; refFile?: Uuid;
gradient?: Gradient; gradient?: Gradient;
image?: ImageColor; image?: ImageColor | PartialImageColor; // @TODO: move to any other place
}; };

View file

@ -1,3 +1,5 @@
import { Color } from '@ui/lib/types/utils/color';
import { Gradient } from './gradient'; import { Gradient } from './gradient';
import { ImageColor, PartialImageColor } from './imageColor'; import { ImageColor, PartialImageColor } from './imageColor';
import { Uuid } from './uuid'; import { Uuid } from './uuid';
@ -10,3 +12,9 @@ export type Fill = {
fillColorRefId?: Uuid; fillColorRefId?: Uuid;
fillImage?: ImageColor | PartialImageColor; // @TODO: move to any other place fillImage?: ImageColor | PartialImageColor; // @TODO: move to any other place
}; };
export type FillStyle = {
name: string;
fills: Fill[];
colors: Color[];
};

View file

@ -1,14 +1,13 @@
import { sleep } from '@plugin/utils/sleep'; import { sleep } from '@plugin/utils/sleep';
import { sendMessage } from '@ui/context'; import { sendMessage } from '@ui/context';
import { createFile as createPenpotFile } from '@ui/lib/penpot'; import { PenpotFile } from '@ui/lib/types/penpotFile';
import { PenpotPage } from '@ui/lib/types/penpotPage'; import { PenpotPage } from '@ui/lib/types/penpotPage';
import { idLibrary } from '@ui/parser'; import { idLibrary } from '@ui/parser';
import { createComponentsLibrary, createPage } from '@ui/parser/creators'; import { createColorsLibrary, createComponentsLibrary, createPage } from '@ui/parser/creators';
import { uiComponents } from '@ui/parser/libraries'; import { uiComponents } from '@ui/parser/libraries';
export const createFile = async (name: string, children: PenpotPage[]) => { export const buildFile = async (file: PenpotFile, children: PenpotPage[]) => {
const file = createPenpotFile(name);
let pagesBuilt = 1; let pagesBuilt = 1;
uiComponents.init(); uiComponents.init();
@ -35,6 +34,8 @@ export const createFile = async (name: string, children: PenpotPage[]) => {
await sleep(0); await sleep(0);
} }
await createColorsLibrary(file);
await createComponentsLibrary(file); await createComponentsLibrary(file);
return file; return file;

View file

@ -0,0 +1,41 @@
import { sleep } from '@plugin/utils/sleep';
import { sendMessage } from '@ui/context';
import { PenpotFile } from '@ui/lib/types/penpotFile';
import { uiColorLibraries } from '@ui/parser/libraries/UiColorLibraries';
export const createColorsLibrary = async (file: PenpotFile) => {
let librariesBuilt = 1;
const libraries = uiColorLibraries.all();
sendMessage({
type: 'PROGRESS_TOTAL_ITEMS',
data: libraries.length
});
sendMessage({
type: 'PROGRESS_STEP',
data: 'libraries'
});
for (const library of libraries) {
for (let index = 0; index < library.fills.length; index++) {
file.addLibraryColor({
...library.colors[index],
id: library.fills[index].fillColorRefId,
refFile: library.fills[index].fillColorRefFile,
color: library.fills[index].fillColor,
opacity: library.fills[index].fillOpacity,
image: library.fills[index].fillImage,
gradient: library.fills[index].fillColorGradient
});
sendMessage({
type: 'PROGRESS_PROCESSED_ITEMS',
data: librariesBuilt++
});
await sleep(0);
}
}
};

View file

@ -1,10 +1,11 @@
export * from './createArtboard'; export * from './createArtboard';
export * from './createBool'; export * from './createBool';
export * from './createCircle'; export * from './createCircle';
export * from './createColorsLibrary';
export * from './createComponent'; export * from './createComponent';
export * from './createComponentInstance'; export * from './createComponentInstance';
export * from './createComponentsLibrary'; export * from './createComponentsLibrary';
export * from './createFile'; export * from './buildFile';
export * from './createGroup'; export * from './createGroup';
export * from './createItems'; export * from './createItems';
export * from './createPage'; export * from './createPage';

View file

@ -1,11 +1,10 @@
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';
import { uiColorLibraries } from '@ui/parser/libraries/UiColorLibraries';
export const symbolFills = (fillStyleId?: string, fills?: Fill[]): Fill[] | undefined => { export const symbolFills = (fillStyleId?: string, fills?: Fill[]): Fill[] | undefined => {
const nodeFills = fillStyleId ? styleLibrary.get(fillStyleId) : fills; const nodeFills = fillStyleId ? uiColorLibraries.get(fillStyleId)?.fills : fills;
if (!nodeFills) return; if (!nodeFills) return;

View file

@ -0,0 +1,19 @@
import { FillStyle } from '@ui/lib/types/utils/fill';
class UiColorLibraries {
private libraries: Map<string, FillStyle> = new Map();
public register(id: string, fillStyle: FillStyle) {
this.libraries.set(id, fillStyle);
}
public get(id: string): FillStyle | undefined {
return this.libraries.get(id);
}
public all(): FillStyle[] {
return Array.from(this.libraries.values());
}
}
export const uiColorLibraries = new UiColorLibraries();

View file

@ -1,2 +1,3 @@
export * from './UiComponents'; export * from './UiComponents';
export * from './UiImages'; export * from './UiImages';
export * from './UiColorLibraries';

View file

@ -1,11 +1,14 @@
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';
import { sendMessage } from '@ui/context'; import { sendMessage } from '@ui/context';
import { createFile } from '@ui/parser/creators'; import { createFile } from '@ui/lib/penpot';
import { PenpotFile } from '@ui/lib/types/penpotFile';
import { FillStyle } from '@ui/lib/types/utils/fill';
import { buildFile } from '@ui/parser/creators';
import { uiImages } from '@ui/parser/libraries'; import { uiImages } from '@ui/parser/libraries';
import { uiColorLibraries } from '@ui/parser/libraries';
import { PenpotDocument } from '@ui/types'; import { PenpotDocument } from '@ui/types';
import { parseImage } from '.'; import { parseImage } from '.';
@ -41,6 +44,43 @@ const optimizeImages = async (images: Record<string, Uint8Array>) => {
} }
}; };
const prepareColorLibraries = async (file: PenpotFile, styles: Record<string, FillStyle>) => {
const stylesToRegister = Object.entries(styles);
if (stylesToRegister.length === 0) return;
let stylesRegistered = 1;
sendMessage({
type: 'PROGRESS_TOTAL_ITEMS',
data: stylesToRegister.length
});
sendMessage({
type: 'PROGRESS_STEP',
data: 'format'
});
for (const [key, fillStyle] of stylesToRegister) {
for (let index = 0; index < fillStyle.fills.length; index++) {
const colorId = file.newId();
fillStyle.fills[index].fillColorRefId = colorId;
fillStyle.fills[index].fillColorRefFile = file.getId();
fillStyle.colors[index].id = colorId;
fillStyle.colors[index].refFile = file.getId();
}
uiColorLibraries.register(key, fillStyle);
sendMessage({
type: 'PROGRESS_PROCESSED_ITEMS',
data: stylesRegistered++
});
await sleep(0);
}
};
export const parse = async ({ export const parse = async ({
name, name,
children = [], children = [],
@ -49,9 +89,11 @@ export const parse = async ({
styles styles
}: PenpotDocument) => { }: PenpotDocument) => {
componentsLibrary.init(components); componentsLibrary.init(components);
styleLibrary.init(styles);
const file = createFile(name);
await optimizeImages(images); await optimizeImages(images);
await prepareColorLibraries(file, styles);
return createFile(name, children); return buildFile(file, children);
}; };

View file

@ -1,11 +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'; import { FillStyle } 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[]>; styles: Record<string, FillStyle>;
}; };