0
Fork 0
mirror of https://github.com/penpot/penpot-exporter-figma-plugin.git synced 2024-12-22 05:33:02 -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,
width: 'width' in node ? node.width : 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) {
// Find any fill of 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
// 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.
'exportAsync' in node &&
node.exportAsync({ format: 'PNG' }).then(value => {
const b64 = figma.base64Encode(value);
figma.ui.postMessage({
type: 'IMAGE',
data: {
id: node.id,
value: 'data:' + detectMimeType(b64) + ';base64,' + b64
}
});
});
const value = await node.exportAsync({ format: 'PNG' });
const b64 = figma.base64Encode(value);
result.imageFill = '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 });
const root: NodeData | TextData = await traverse(figma.root); // start the traversal at the root
figma.ui.postMessage({ type: 'FIGMAFILE', data: root });
figma.ui.onmessage = msg => {
figma.ui.onmessage = async msg => {
if (msg.type === 'export') {
const root: NodeData | TextData = await traverse(figma.root); // start the traversal at the root
figma.ui.postMessage({ type: 'FIGMAFILE', data: root });
}
if (msg.type === 'cancel') {
figma.closePlugin();
}

View file

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

View file

@ -57,6 +57,12 @@ button:focus-visible {
outline-color: var(--color-border-focus);
}
button:disabled {
background-color: black;
color: var(--color-text);
cursor: not-allowed;
}
button.brand {
--color-bg: var(--color-bg-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 slugify from 'slugify';
import fonts from './gfonts.json';
import { NodeData, TextData, TextDataChildren } from './interfaces';
import { PenpotFile, createFile } from './penpot';
import './ui.css';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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;
};
const gfonts = new Set(fonts);
type PenpotExporterState = {
isDebug: boolean;
penpotFileData: string;
missingFonts: Set<string>;
figmaFileData: string;
figmaRootNode: NodeData | null;
images: { [id: string]: FigmaImageData };
exporting: boolean;
};
export default class PenpotExporter extends React.Component<unknown, PenpotExporterState> {
state: PenpotExporterState = {
isDebug: false,
penpotFileData: '',
figmaFileData: '',
missingFonts: new Set(),
figmaRootNode: null,
images: {}
exporting: false
};
componentDidMount = () => {
@ -292,24 +274,18 @@ export default class PenpotExporter extends React.Component<unknown, PenpotExpor
});
}
createPenpotImage(
file: PenpotFile,
node: NodeData,
baseX: number,
baseY: number,
image: FigmaImageData
) {
createPenpotImage(file: PenpotFile, node: NodeData, baseX: number, baseY: number) {
file.createImage({
name: node.name,
x: node.x + baseX,
y: node.y + baseY,
width: image.width,
height: image.height,
width: node.width,
height: node.height,
metadata: {
width: image.width,
height: image.height
width: node.width,
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.
const [adjustedX, adjustedY] = this.calculateAdjustment(node);
this.createPenpotImage(
file,
node,
baseX + adjustedX,
baseY + adjustedY,
this.state.images[node.id]
);
this.createPenpotImage(file, node, baseX + adjustedX, baseY + adjustedY);
} else if (node.type == 'PAGE') {
this.createPenpotPage(file, node);
} else if (node.type == 'FRAME') {
@ -364,13 +334,7 @@ export default class PenpotExporter extends React.Component<unknown, PenpotExpor
}
}
createPenpotFile() {
const node = this.state.figmaRootNode;
if (node === null) {
throw new Error('No Figma file data found');
}
createPenpotFile(node: NodeData) {
const file = createFile(node.name);
for (const page of node.children) {
this.createPenpotItem(file, page, 0, 0);
@ -379,16 +343,8 @@ export default class PenpotExporter extends React.Component<unknown, PenpotExpor
}
onCreatePenpot = () => {
const file = this.createPenpotFile();
const penpotFileMap = file.asMap();
this.setState(() => ({
penpotFileData: JSON.stringify(
penpotFileMap,
(key, value) => (value instanceof Map ? [...value] : value),
4
)
}));
file.export();
this.setState(() => ({ exporting: true }));
parent.postMessage({ pluginMessage: { type: 'export' } }, '*');
};
onCancel = () => {
@ -398,30 +354,9 @@ export default class PenpotExporter extends React.Component<unknown, PenpotExpor
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onMessage = (event: any) => {
if (event.data.pluginMessage.type == 'FIGMAFILE') {
this.setState(() => ({
figmaFileData: JSON.stringify(
event.data.pluginMessage.data,
(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;
const file = this.createPenpotFile(event.data.pluginMessage.data);
file.export();
this.setState(() => ({ exporting: false }));
}
};
@ -434,19 +369,8 @@ export default class PenpotExporter extends React.Component<unknown, PenpotExpor
if (isMissingFonts) {
height += this.state.missingFonts.size * 20;
width = 400;
parent.postMessage({ pluginMessage: { type: 'resize', width: width, height: height } }, '*');
}
if (this.state.isDebug) {
height += 600;
width = 800;
}
parent.postMessage({ pluginMessage: { type: 'resize', width: width, height: height } }, '*');
};
toggleDebug = (event: React.ChangeEvent<HTMLInputElement>) => {
const isDebug = event.currentTarget.checked;
this.setState(() => ({ isDebug: isDebug }));
};
renderFontWarnings = () => {
@ -476,34 +400,10 @@ export default class PenpotExporter extends React.Component<unknown, PenpotExpor
<small>Ensure fonts are installed in Penpot before importing.</small>
<div id="missing-fonts-list">{this.renderFontWarnings()}</div>
</div>
<div>
<input type="checkbox" id="chkDebug" name="chkDebug" onChange={this.toggleDebug} />
<label htmlFor="chkDebug">Show debug data</label>
</div>
</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>
<button className="brand" onClick={this.onCreatePenpot}>
Export
<button className="brand" disabled={this.state.exporting} onClick={this.onCreatePenpot}>
{this.state.exporting ? 'Exporting...' : 'Export to Penpot'}
</button>
<button onClick={this.onCancel}>Cancel</button>
</footer>

View file

@ -9,6 +9,7 @@
"module": "ESNext",
"moduleResolution": "Node10",
"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
}
}