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

Merge pull request #4 from Runroom/feature/refactor

First Refactor
This commit is contained in:
Alex Sánchez 2024-04-08 17:05:56 +02:00 committed by GitHub
commit cedb9fc85d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 41 additions and 138 deletions

View file

@ -43,27 +43,21 @@ async function traverse(node: BaseNode): Promise<NodeData | TextData> {
y: 'y' in node ? node.y : 0, y: 'y' in node ? node.y : 0,
width: 'width' in node ? node.width : 0, width: 'width' in node ? node.width : 0,
height: 'height' in node ? node.height : 0, height: 'height' in node ? node.height : 0,
fills: 'fills' in node ? (node.fills === figma.mixed ? [] : node.fills) : [] // TODO: Support mixed fills fills: 'fills' in node ? (node.fills === figma.mixed ? [] : node.fills) : [], // TODO: Support mixed fills
imageFill: ''
}; };
if (result.fills) { if (result.fills) {
// Find any fill of type image // Find any fill of type image
const imageFill = result.fills.find(fill => fill.type === 'IMAGE'); const imageFill = result.fills.find(fill => fill.type === 'IMAGE');
if (imageFill) { if (imageFill && 'exportAsync' in node) {
// An "image" in Figma is a shape with one or more image fills, potentially blended with other fill // An "image" in Figma is a shape with one or more image fills, potentially blended with other fill
// types. Given the complexity of mirroring this exactly in Penpot, which treats images as first-class // types. Given the complexity of mirroring this exactly in Penpot, which treats images as first-class
// objects, we're going to simplify this by exporting this shape as a PNG image. // objects, we're going to simplify this by exporting this shape as a PNG image.
'exportAsync' in node && const value = await node.exportAsync({ format: 'PNG' });
node.exportAsync({ format: 'PNG' }).then(value => {
const b64 = figma.base64Encode(value); const b64 = figma.base64Encode(value);
figma.ui.postMessage({
type: 'IMAGE', result.imageFill = 'data:' + detectMimeType(b64) + ';base64,' + b64;
data: {
id: node.id,
value: 'data:' + detectMimeType(b64) + ';base64,' + b64
}
});
});
} }
} }
@ -105,10 +99,11 @@ async function traverse(node: BaseNode): Promise<NodeData | TextData> {
figma.showUI(__html__, { themeColors: true, height: 200, width: 300 }); figma.showUI(__html__, { themeColors: true, height: 200, width: 300 });
const root: NodeData | TextData = await traverse(figma.root); // start the traversal at the root figma.ui.onmessage = async msg => {
figma.ui.postMessage({ type: 'FIGMAFILE', data: root }); if (msg.type === 'export') {
const root: NodeData | TextData = await traverse(figma.root); // start the traversal at the root
figma.ui.onmessage = msg => { figma.ui.postMessage({ type: 'FIGMAFILE', data: root });
}
if (msg.type === 'cancel') { if (msg.type === 'cancel') {
figma.closePlugin(); figma.closePlugin();
} }

View file

@ -8,6 +8,7 @@ export type NodeData = {
width: number; width: number;
height: number; height: number;
fills: readonly Paint[]; fills: readonly Paint[];
imageFill?: string;
}; };
export type TextDataChildren = Pick< export type TextDataChildren = Pick<

View file

@ -57,6 +57,12 @@ button:focus-visible {
outline-color: var(--color-border-focus); outline-color: var(--color-border-focus);
} }
button:disabled {
background-color: black;
color: var(--color-text);
cursor: not-allowed;
}
button.brand { button.brand {
--color-bg: var(--color-bg-brand); --color-bg: var(--color-bg-brand);
--color-text: var(--color-text-brand); --color-text: var(--color-text-brand);

View file

@ -2,40 +2,22 @@ import * as React from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import slugify from 'slugify'; import slugify from 'slugify';
import fonts from './gfonts.json';
import { NodeData, TextData, TextDataChildren } from './interfaces'; import { NodeData, TextData, TextDataChildren } from './interfaces';
import { PenpotFile, createFile } from './penpot'; import { PenpotFile, createFile } from './penpot';
import './ui.css'; import './ui.css';
// eslint-disable-next-line @typescript-eslint/no-explicit-any const gfonts = new Set(fonts);
declare function require(path: string): any;
// Open resources/gfonts.json and create a set of matched font names
const gfonts = new Set();
require('./gfonts.json').forEach((font: string) => gfonts.add(font));
type FigmaImageData = {
value: string;
width: number;
height: number;
};
type PenpotExporterState = { type PenpotExporterState = {
isDebug: boolean;
penpotFileData: string;
missingFonts: Set<string>; missingFonts: Set<string>;
figmaFileData: string; exporting: boolean;
figmaRootNode: NodeData | null;
images: { [id: string]: FigmaImageData };
}; };
export default class PenpotExporter extends React.Component<unknown, PenpotExporterState> { export default class PenpotExporter extends React.Component<unknown, PenpotExporterState> {
state: PenpotExporterState = { state: PenpotExporterState = {
isDebug: false,
penpotFileData: '',
figmaFileData: '',
missingFonts: new Set(), missingFonts: new Set(),
figmaRootNode: null, exporting: false
images: {}
}; };
componentDidMount = () => { componentDidMount = () => {
@ -292,24 +274,18 @@ export default class PenpotExporter extends React.Component<unknown, PenpotExpor
}); });
} }
createPenpotImage( createPenpotImage(file: PenpotFile, node: NodeData, baseX: number, baseY: number) {
file: PenpotFile,
node: NodeData,
baseX: number,
baseY: number,
image: FigmaImageData
) {
file.createImage({ file.createImage({
name: node.name, name: node.name,
x: node.x + baseX, x: node.x + baseX,
y: node.y + baseY, y: node.y + baseY,
width: image.width, width: node.width,
height: image.height, height: node.height,
metadata: { metadata: {
width: image.width, width: node.width,
height: image.height height: node.height
}, },
dataUri: image.value dataUri: node.imageFill
}); });
} }
@ -342,13 +318,7 @@ export default class PenpotExporter extends React.Component<unknown, PenpotExpor
// height of the image. // height of the image.
const [adjustedX, adjustedY] = this.calculateAdjustment(node); const [adjustedX, adjustedY] = this.calculateAdjustment(node);
this.createPenpotImage( this.createPenpotImage(file, node, baseX + adjustedX, baseY + adjustedY);
file,
node,
baseX + adjustedX,
baseY + adjustedY,
this.state.images[node.id]
);
} else if (node.type == 'PAGE') { } else if (node.type == 'PAGE') {
this.createPenpotPage(file, node); this.createPenpotPage(file, node);
} else if (node.type == 'FRAME') { } else if (node.type == 'FRAME') {
@ -364,13 +334,7 @@ export default class PenpotExporter extends React.Component<unknown, PenpotExpor
} }
} }
createPenpotFile() { createPenpotFile(node: NodeData) {
const node = this.state.figmaRootNode;
if (node === null) {
throw new Error('No Figma file data found');
}
const file = createFile(node.name); const file = createFile(node.name);
for (const page of node.children) { for (const page of node.children) {
this.createPenpotItem(file, page, 0, 0); this.createPenpotItem(file, page, 0, 0);
@ -379,16 +343,8 @@ export default class PenpotExporter extends React.Component<unknown, PenpotExpor
} }
onCreatePenpot = () => { onCreatePenpot = () => {
const file = this.createPenpotFile(); this.setState(() => ({ exporting: true }));
const penpotFileMap = file.asMap(); parent.postMessage({ pluginMessage: { type: 'export' } }, '*');
this.setState(() => ({
penpotFileData: JSON.stringify(
penpotFileMap,
(key, value) => (value instanceof Map ? [...value] : value),
4
)
}));
file.export();
}; };
onCancel = () => { onCancel = () => {
@ -398,30 +354,9 @@ export default class PenpotExporter extends React.Component<unknown, PenpotExpor
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
onMessage = (event: any) => { onMessage = (event: any) => {
if (event.data.pluginMessage.type == 'FIGMAFILE') { if (event.data.pluginMessage.type == 'FIGMAFILE') {
this.setState(() => ({ const file = this.createPenpotFile(event.data.pluginMessage.data);
figmaFileData: JSON.stringify( file.export();
event.data.pluginMessage.data, this.setState(() => ({ exporting: false }));
(key, value) => (value instanceof Map ? [...value] : value),
4
),
figmaRootNode: event.data.pluginMessage.data
}));
} else if (event.data.pluginMessage.type == 'IMAGE') {
const data = event.data.pluginMessage.data;
const image = document.createElement('img');
image.addEventListener('load', () => {
// Get byte array from response
this.setState(state => {
state.images[data.id] = {
value: data.value,
width: image.naturalWidth,
height: image.naturalHeight
};
return state;
});
});
image.src = data.value;
} }
}; };
@ -434,19 +369,8 @@ export default class PenpotExporter extends React.Component<unknown, PenpotExpor
if (isMissingFonts) { if (isMissingFonts) {
height += this.state.missingFonts.size * 20; height += this.state.missingFonts.size * 20;
width = 400; width = 400;
}
if (this.state.isDebug) {
height += 600;
width = 800;
}
parent.postMessage({ pluginMessage: { type: 'resize', width: width, height: height } }, '*'); parent.postMessage({ pluginMessage: { type: 'resize', width: width, height: height } }, '*');
}; }
toggleDebug = (event: React.ChangeEvent<HTMLInputElement>) => {
const isDebug = event.currentTarget.checked;
this.setState(() => ({ isDebug: isDebug }));
}; };
renderFontWarnings = () => { renderFontWarnings = () => {
@ -476,34 +400,10 @@ export default class PenpotExporter extends React.Component<unknown, PenpotExpor
<small>Ensure fonts are installed in Penpot before importing.</small> <small>Ensure fonts are installed in Penpot before importing.</small>
<div id="missing-fonts-list">{this.renderFontWarnings()}</div> <div id="missing-fonts-list">{this.renderFontWarnings()}</div>
</div> </div>
<div>
<input type="checkbox" id="chkDebug" name="chkDebug" onChange={this.toggleDebug} />
<label htmlFor="chkDebug">Show debug data</label>
</div>
</section> </section>
<div style={{ display: this.state.isDebug ? '' : 'none' }}>
<section>
<textarea
style={{ width: '790px', height: '270px' }}
id="figma-file-data"
value={this.state.figmaFileData}
readOnly
/>
<label htmlFor="figma-file-data">Figma file data</label>
</section>
<section>
<textarea
style={{ width: '790px', height: '270px' }}
id="penpot-file-data"
value={this.state.penpotFileData}
readOnly
/>
<label htmlFor="penpot-file-data">Penpot file data</label>
</section>
</div>
<footer> <footer>
<button className="brand" onClick={this.onCreatePenpot}> <button className="brand" disabled={this.state.exporting} onClick={this.onCreatePenpot}>
Export {this.state.exporting ? 'Exporting...' : 'Export to Penpot'}
</button> </button>
<button onClick={this.onCancel}>Cancel</button> <button onClick={this.onCancel}>Cancel</button>
</footer> </footer>

View file

@ -9,6 +9,7 @@
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Node10", "moduleResolution": "Node10",
"strict": true, "strict": true,
"typeRoots": ["src/penpot.d.ts", "node_modules/@figma", "node_modules/@types"] "typeRoots": ["src/penpot.d.ts", "node_modules/@figma", "node_modules/@types"],
"resolveJsonModule": true
} }
} }