mirror of
https://github.com/penpot/penpot-exporter-figma-plugin.git
synced 2024-12-22 05:33:02 -05:00
Color libraries (#183)
* wip * color library * fixes * fixes * fixes * fixes * changeset * fixes * fixes * fixes * fixes
This commit is contained in:
parent
b652af158e
commit
a58f9e913d
19 changed files with 248 additions and 35 deletions
5
.changeset/strange-gorillas-knock.md
Normal file
5
.changeset/strange-gorillas-knock.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"penpot-exporter": minor
|
||||
---
|
||||
|
||||
Support for color libraries
|
|
@ -1,13 +1,11 @@
|
|||
import { Fill } from '@ui/lib/types/utils/fill';
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public get(id: string): Fill[] | undefined {
|
||||
public get(id: string): PaintStyle | undefined {
|
||||
return this.styles.get(id);
|
||||
}
|
||||
|
||||
|
@ -15,13 +13,9 @@ class StyleLibrary {
|
|||
return this.styles.has(id);
|
||||
}
|
||||
|
||||
public all(): Record<string, Fill[]> {
|
||||
public all(): Record<string, PaintStyle | undefined> {
|
||||
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,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 { ShapeAttributes } from '@ui/lib/types/shapes/shape';
|
||||
|
@ -14,7 +14,7 @@ export const transformFills = (
|
|||
if (hasFillStyle(node)) {
|
||||
return {
|
||||
fills: [],
|
||||
fillStyleId: translateFillStyle(node.fillStyleId, node.fills)
|
||||
fillStyleId: translateFillStyleId(node.fillStyleId)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -3,9 +3,11 @@ import { imagesLibrary } from '@plugin/ImageLibrary';
|
|||
import { remoteComponentLibrary } from '@plugin/RemoteComponentLibrary';
|
||||
import { styleLibrary } from '@plugin/StyleLibrary';
|
||||
import { translateRemoteChildren } from '@plugin/translators';
|
||||
import { translatePaintStyles } from '@plugin/translators/fills';
|
||||
import { sleep } from '@plugin/utils';
|
||||
|
||||
import { PenpotPage } from '@ui/lib/types/penpotPage';
|
||||
import { FillStyle } from '@ui/lib/types/utils/fill';
|
||||
import { PenpotDocument } from '@ui/types';
|
||||
|
||||
import { transformPageNode } from '.';
|
||||
|
@ -48,6 +50,47 @@ const downloadImages = async (): Promise<Record<string, Uint8Array>> => {
|
|||
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 children = [];
|
||||
let currentPage = 1;
|
||||
|
@ -74,6 +117,11 @@ const processPages = async (node: DocumentNode): Promise<PenpotPage[]> => {
|
|||
};
|
||||
|
||||
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);
|
||||
|
||||
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 {
|
||||
name: node.name,
|
||||
children,
|
||||
components: componentsLibrary.all(),
|
||||
images: await downloadImages(),
|
||||
styles: styleLibrary.all()
|
||||
images,
|
||||
styles
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export * from './translateFills';
|
||||
export * from './translateImageFill';
|
||||
export * from './translatePaintStyles';
|
||||
export * from './translateSolidFill';
|
||||
|
|
|
@ -42,14 +42,13 @@ export const translateFills = (
|
|||
return penpotFills;
|
||||
};
|
||||
|
||||
export const translateFillStyle = (
|
||||
fillStyleId: string | typeof figma.mixed | undefined,
|
||||
fills: readonly Paint[] | typeof figma.mixed | undefined
|
||||
export const translateFillStyleId = (
|
||||
fillStyleId: string | typeof figma.mixed | undefined
|
||||
): string | undefined => {
|
||||
if (fillStyleId === figma.mixed || fillStyleId === undefined) return;
|
||||
|
||||
if (!styleLibrary.has(fillStyleId)) {
|
||||
styleLibrary.register(fillStyleId, translateFills(fills));
|
||||
styleLibrary.register(fillStyleId);
|
||||
}
|
||||
|
||||
return fillStyleId;
|
||||
|
|
35
plugin-src/translators/fills/translatePaintStyles.ts
Normal file
35
plugin-src/translators/fills/translatePaintStyles.ts
Normal 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;
|
||||
};
|
|
@ -29,6 +29,15 @@ const stepMessages: Record<Steps, Messages> = {
|
|||
total: 'pages built 🏗️',
|
||||
current: 'Currently processing layer'
|
||||
},
|
||||
fills: {
|
||||
total: 'color libraries fetched 🎨'
|
||||
},
|
||||
format: {
|
||||
total: 'formatting color libraries 🎨'
|
||||
},
|
||||
libraries: {
|
||||
total: 'color libraries built 🎨'
|
||||
},
|
||||
components: {
|
||||
total: 'components built 🏗️',
|
||||
current: 'Currently processing layer'
|
||||
|
@ -60,7 +69,10 @@ const StepProgress = (): JSX.Element | null => {
|
|||
case 'images':
|
||||
case 'optimization':
|
||||
case 'building':
|
||||
case 'fills':
|
||||
case 'components':
|
||||
case 'format':
|
||||
case 'libraries':
|
||||
return (
|
||||
<>
|
||||
{processedItems} of {totalItems} {stepMessages[step].total}
|
||||
|
|
|
@ -26,7 +26,10 @@ export type Steps =
|
|||
| 'optimization'
|
||||
| 'building'
|
||||
| 'components'
|
||||
| 'exporting';
|
||||
| 'exporting'
|
||||
| 'fills'
|
||||
| 'format'
|
||||
| 'libraries';
|
||||
|
||||
export const useFigma = (): UseFigmaHook => {
|
||||
const [missingFonts, setMissingFonts] = useState<string[]>();
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Gradient } from './gradient';
|
||||
import { ImageColor } from './imageColor';
|
||||
import { ImageColor, PartialImageColor } from './imageColor';
|
||||
import { Uuid } from './uuid';
|
||||
|
||||
export type Color = {
|
||||
|
@ -13,5 +13,5 @@ export type Color = {
|
|||
refId?: Uuid;
|
||||
refFile?: Uuid;
|
||||
gradient?: Gradient;
|
||||
image?: ImageColor;
|
||||
image?: ImageColor | PartialImageColor; // @TODO: move to any other place
|
||||
};
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { Color } from '@ui/lib/types/utils/color';
|
||||
|
||||
import { Gradient } from './gradient';
|
||||
import { ImageColor, PartialImageColor } from './imageColor';
|
||||
import { Uuid } from './uuid';
|
||||
|
@ -10,3 +12,9 @@ export type Fill = {
|
|||
fillColorRefId?: Uuid;
|
||||
fillImage?: ImageColor | PartialImageColor; // @TODO: move to any other place
|
||||
};
|
||||
|
||||
export type FillStyle = {
|
||||
name: string;
|
||||
fills: Fill[];
|
||||
colors: Color[];
|
||||
};
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import { sleep } from '@plugin/utils/sleep';
|
||||
|
||||
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 { 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';
|
||||
|
||||
export const createFile = async (name: string, children: PenpotPage[]) => {
|
||||
const file = createPenpotFile(name);
|
||||
export const buildFile = async (file: PenpotFile, children: PenpotPage[]) => {
|
||||
let pagesBuilt = 1;
|
||||
|
||||
uiComponents.init();
|
||||
|
@ -35,6 +34,8 @@ export const createFile = async (name: string, children: PenpotPage[]) => {
|
|||
await sleep(0);
|
||||
}
|
||||
|
||||
await createColorsLibrary(file);
|
||||
|
||||
await createComponentsLibrary(file);
|
||||
|
||||
return file;
|
41
ui-src/parser/creators/createColorsLibrary.ts
Normal file
41
ui-src/parser/creators/createColorsLibrary.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,10 +1,11 @@
|
|||
export * from './createArtboard';
|
||||
export * from './createBool';
|
||||
export * from './createCircle';
|
||||
export * from './createColorsLibrary';
|
||||
export * from './createComponent';
|
||||
export * from './createComponentInstance';
|
||||
export * from './createComponentsLibrary';
|
||||
export * from './createFile';
|
||||
export * from './buildFile';
|
||||
export * from './createGroup';
|
||||
export * from './createItems';
|
||||
export * from './createPage';
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
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';
|
||||
import { uiColorLibraries } from '@ui/parser/libraries/UiColorLibraries';
|
||||
|
||||
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;
|
||||
|
||||
|
|
19
ui-src/parser/libraries/UiColorLibraries.ts
Normal file
19
ui-src/parser/libraries/UiColorLibraries.ts
Normal 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();
|
|
@ -1,2 +1,3 @@
|
|||
export * from './UiComponents';
|
||||
export * from './UiImages';
|
||||
export * from './UiColorLibraries';
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
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';
|
||||
|
||||
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 { uiColorLibraries } from '@ui/parser/libraries';
|
||||
import { PenpotDocument } from '@ui/types';
|
||||
|
||||
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 ({
|
||||
name,
|
||||
children = [],
|
||||
|
@ -49,9 +89,11 @@ export const parse = async ({
|
|||
styles
|
||||
}: PenpotDocument) => {
|
||||
componentsLibrary.init(components);
|
||||
styleLibrary.init(styles);
|
||||
|
||||
const file = createFile(name);
|
||||
|
||||
await optimizeImages(images);
|
||||
await prepareColorLibraries(file, styles);
|
||||
|
||||
return createFile(name, children);
|
||||
return buildFile(file, children);
|
||||
};
|
||||
|
|
|
@ -1,11 +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';
|
||||
import { FillStyle } 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[]>;
|
||||
styles: Record<string, FillStyle>;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue