diff --git a/.changeset/sharp-years-hear.md b/.changeset/sharp-years-hear.md new file mode 100644 index 0000000..8c59434 --- /dev/null +++ b/.changeset/sharp-years-hear.md @@ -0,0 +1,5 @@ +--- +"penpot-exporter": minor +--- + +Add progress bar during the export diff --git a/.changeset/tame-buttons-develop.md b/.changeset/tame-buttons-develop.md new file mode 100644 index 0000000..5d1397a --- /dev/null +++ b/.changeset/tame-buttons-develop.md @@ -0,0 +1,5 @@ +--- +"penpot-exporter": minor +--- + +Improve performance so the interface of figma feels more responsive during the export process diff --git a/plugin-src/findAllTextnodes.ts b/plugin-src/findAllTextnodes.ts index e98386e..827d5a1 100644 --- a/plugin-src/findAllTextnodes.ts +++ b/plugin-src/findAllTextnodes.ts @@ -1,27 +1,23 @@ +import { isGoogleFont } from '@plugin/translators/text/font/gfonts'; +import { isLocalFont } from '@plugin/translators/text/font/local'; +import { sleep } from '@plugin/utils'; + import { registerChange } from './registerChange'; -import { isGoogleFont } from './translators/text/font/gfonts'; -import { isLocalFont } from './translators/text/font/local'; export const findAllTextNodes = async () => { - await figma.loadAllPagesAsync(); - - const nodes = figma.root.findAllWithCriteria({ - types: ['TEXT'] - }); - const fonts = new Set(); - nodes.forEach(node => { - const styledTextSegments = node.getStyledTextSegments(['fontName']); + for (const page of figma.root.children) { + await page.loadAsync(); - styledTextSegments.forEach(segment => { - if (isGoogleFont(segment.fontName) || isLocalFont(segment.fontName)) { - return; + for (const node of page.children) { + if (node.type === 'TEXT') { + extractMissingFonts(node, fonts); } - fonts.add(segment.fontName.family); - }); - }); + sleep(0); + } + } figma.ui.postMessage({ type: 'CUSTOM_FONTS', @@ -30,3 +26,15 @@ export const findAllTextNodes = async () => { figma.currentPage.once('nodechange', registerChange); }; + +const extractMissingFonts = (node: TextNode, fonts: Set) => { + const styledTextSegments = node.getStyledTextSegments(['fontName']); + + styledTextSegments.forEach(segment => { + if (isGoogleFont(segment.fontName) || isLocalFont(segment.fontName)) { + return; + } + + fonts.add(segment.fontName.family); + }); +}; diff --git a/plugin-src/handleExportMessage.ts b/plugin-src/handleExportMessage.ts index 93dbdd5..2cd49c9 100644 --- a/plugin-src/handleExportMessage.ts +++ b/plugin-src/handleExportMessage.ts @@ -2,8 +2,6 @@ import { transformDocumentNode } from '@plugin/transformers'; import { setCustomFontId } from '@plugin/translators/text/font/custom'; export const handleExportMessage = async (missingFontIds: Record) => { - await figma.loadAllPagesAsync(); - Object.entries(missingFontIds).forEach(([fontFamily, fontId]) => { setCustomFontId(fontFamily, fontId); }); diff --git a/plugin-src/transformers/transformDocumentNode.ts b/plugin-src/transformers/transformDocumentNode.ts index 784572f..b1abb1c 100644 --- a/plugin-src/transformers/transformDocumentNode.ts +++ b/plugin-src/transformers/transformDocumentNode.ts @@ -5,9 +5,28 @@ import { PenpotDocument } from '@ui/types'; import { transformPageNode } from '.'; export const transformDocumentNode = async (node: DocumentNode): Promise => { + const children = []; + let currentPage = 0; + + figma.ui.postMessage({ + type: 'PROGRESS_TOTAL_PAGES', + data: node.children.length + }); + + for (const page of node.children) { + figma.ui.postMessage({ + type: 'PROGRESS_PROCESSED_PAGES', + data: currentPage++ + }); + + await page.loadAsync(); + + children.push(await transformPageNode(page)); + } + return { name: node.name, - children: await Promise.all(node.children.map(child => transformPageNode(child))), + children, components: componentsLibrary.all() }; }; diff --git a/plugin-src/transformers/transformPageNode.ts b/plugin-src/transformers/transformPageNode.ts index 5ed6ffd..3f658bc 100644 --- a/plugin-src/transformers/transformPageNode.ts +++ b/plugin-src/transformers/transformPageNode.ts @@ -1,4 +1,4 @@ -import { transformChildren } from '@plugin/transformers/partials'; +import { translateChildren } from '@plugin/translators'; import { translatePageFill } from '@plugin/translators/fills'; import { PenpotPage } from '@ui/lib/types/penpotPage'; @@ -9,6 +9,6 @@ export const transformPageNode = async (node: PageNode): Promise => options: { background: node.backgrounds.length ? translatePageFill(node.backgrounds[0]) : undefined }, - ...(await transformChildren(node)) + children: await translateChildren(node.children) }; }; diff --git a/plugin-src/transformers/transformSceneNode.ts b/plugin-src/transformers/transformSceneNode.ts index 3231636..8f163d4 100644 --- a/plugin-src/transformers/transformSceneNode.ts +++ b/plugin-src/transformers/transformSceneNode.ts @@ -18,31 +18,52 @@ export const transformSceneNode = async ( baseX: number = 0, baseY: number = 0 ): Promise => { + let penpotNode: PenpotNode | undefined; + + figma.ui.postMessage({ + type: 'PROGRESS_NODE', + data: node.name + }); + switch (node.type) { case 'RECTANGLE': - return await transformRectangleNode(node, baseX, baseY); + penpotNode = await transformRectangleNode(node, baseX, baseY); + break; case 'ELLIPSE': - return await transformEllipseNode(node, baseX, baseY); + penpotNode = await transformEllipseNode(node, baseX, baseY); + break; case 'SECTION': case 'FRAME': - return await transformFrameNode(node, baseX, baseY); + penpotNode = await transformFrameNode(node, baseX, baseY); + break; case 'GROUP': - return await transformGroupNode(node, baseX, baseY); + penpotNode = await transformGroupNode(node, baseX, baseY); + break; case 'TEXT': - return await transformTextNode(node, baseX, baseY); + penpotNode = await transformTextNode(node, baseX, baseY); + break; case 'VECTOR': - return await transformVectorNode(node, baseX, baseY); + penpotNode = await transformVectorNode(node, baseX, baseY); + break; case 'STAR': case 'POLYGON': case 'LINE': - return await transformPathNode(node, baseX, baseY); + penpotNode = await transformPathNode(node, baseX, baseY); + break; case 'BOOLEAN_OPERATION': - return await transformBooleanNode(node, baseX, baseY); + penpotNode = await transformBooleanNode(node, baseX, baseY); + break; case 'COMPONENT': - return await transformComponentNode(node, baseX, baseY); + penpotNode = await transformComponentNode(node, baseX, baseY); + break; case 'INSTANCE': - return await transformInstanceNode(node, baseX, baseY); + penpotNode = await transformInstanceNode(node, baseX, baseY); + break; } - console.error(`Unsupported node type: ${node.type}`); + if (penpotNode === undefined) { + console.error(`Unsupported node type: ${node.type}`); + } + + return penpotNode; }; diff --git a/plugin-src/translators/translateChildren.ts b/plugin-src/translators/translateChildren.ts index 5e0c250..ed9ad19 100644 --- a/plugin-src/translators/translateChildren.ts +++ b/plugin-src/translators/translateChildren.ts @@ -1,4 +1,5 @@ import { transformGroupNodeLike, transformSceneNode } from '@plugin/transformers'; +import { sleep } from '@plugin/utils'; import { PenpotNode } from '@ui/types'; @@ -32,10 +33,18 @@ export const translateMaskChildren = async ( export const translateChildren = async ( children: readonly SceneNode[], - baseX: number, - baseY: number + baseX: number = 0, + baseY: number = 0 ): Promise => { - return (await Promise.all(children.map(child => transformSceneNode(child, baseX, baseY)))).filter( - (child): child is PenpotNode => !!child - ); + const transformedChildren: PenpotNode[] = []; + + for (const child of children) { + const penpotNode = await transformSceneNode(child, baseX, baseY); + + if (penpotNode) transformedChildren.push(penpotNode); + + await sleep(0); + } + + return transformedChildren; }; diff --git a/plugin-src/utils/index.ts b/plugin-src/utils/index.ts index 4d21894..67ff2e1 100644 --- a/plugin-src/utils/index.ts +++ b/plugin-src/utils/index.ts @@ -6,3 +6,4 @@ export * from './detectMimeType'; export * from './getBoundingBox'; export * from './matrixInvert'; export * from './rgbToHex'; +export * from './sleep'; diff --git a/plugin-src/utils/sleep.ts b/plugin-src/utils/sleep.ts new file mode 100644 index 0000000..c5374b0 --- /dev/null +++ b/plugin-src/utils/sleep.ts @@ -0,0 +1 @@ +export const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); diff --git a/ui-src/App.tsx b/ui-src/App.tsx index 4bfc98b..f4bc015 100644 --- a/ui-src/App.tsx +++ b/ui-src/App.tsx @@ -22,7 +22,7 @@ export const App = () => { return ( MAX_HEIGHT}> - + { + const [currentNode, setCurrentNode] = useState(); + const [totalPages, setTotalPages] = useState(); + const [processedPages, setProcessedPages] = useState(); + + const onMessage = (event: MessageEvent<{ pluginMessage: { type: string; data: unknown } }>) => { + if (event.data.pluginMessage?.type === 'PROGRESS_NODE') { + setCurrentNode(event.data.pluginMessage.data as string); + } else if (event.data.pluginMessage?.type === 'PROGRESS_TOTAL_PAGES') { + setTotalPages(event.data.pluginMessage.data as number); + } else if (event.data.pluginMessage?.type === 'PROGRESS_PROCESSED_PAGES') { + setProcessedPages(event.data.pluginMessage.data as number); + } + }; + + useEffect(() => { + window.addEventListener('message', onMessage); + + return () => { + window.removeEventListener('message', onMessage); + }; + }, []); + + const truncateText = (text: string, maxChars: number) => { + if (text.length <= maxChars) { + return text; + } + + return text.slice(0, maxChars) + '...'; + }; + + return ( + + + + {processedPages} of {totalPages} pages exported 💪 + {currentNode ? ( + <> +
+ Currently exporting layer +
+ {'“' + truncateText(currentNode, 40) + '”'} + + ) : undefined} +
+
+ ); +}; diff --git a/ui-src/components/PenpotExporter.tsx b/ui-src/components/PenpotExporter.tsx index a794b68..93d1e0c 100644 --- a/ui-src/components/PenpotExporter.tsx +++ b/ui-src/components/PenpotExporter.tsx @@ -6,6 +6,7 @@ import { Stack } from '@ui/components/Stack'; import { parse } from '@ui/parser'; import { PenpotDocument } from '@ui/types'; +import { ExporterProgress } from './ExporterProgress'; import { MissingFontsSection } from './MissingFontsSection'; type FormValues = Record; @@ -95,16 +96,20 @@ export const PenpotExporter = () => { return (
- + - - - - + {exporting ? ( + + ) : ( + + + + + )}