mirror of
https://github.com/penpot/penpot-exporter-figma-plugin.git
synced 2024-12-22 13:43:03 -05:00
Merge pull request #6 from Runroom/feature/converters
Updated structure
This commit is contained in:
commit
9d1e0d2abc
25 changed files with 420 additions and 316 deletions
24
src/converters/createPenpotBoard.ts
Normal file
24
src/converters/createPenpotBoard.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { createPenpotItem } from '.';
|
||||
import { NodeData } from '../interfaces';
|
||||
import { PenpotFile } from '../penpot';
|
||||
import { translateFills } from '../translators';
|
||||
|
||||
export const createPenpotBoard = (
|
||||
file: PenpotFile,
|
||||
node: NodeData,
|
||||
baseX: number,
|
||||
baseY: number
|
||||
) => {
|
||||
file.addArtboard({
|
||||
name: node.name,
|
||||
x: node.x + baseX,
|
||||
y: node.y + baseY,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
fills: translateFills(node.fills /*, node.width, node.height*/)
|
||||
});
|
||||
for (const child of node.children) {
|
||||
createPenpotItem(file, child, node.x + baseX, node.y + baseY);
|
||||
}
|
||||
file.closeArtboard();
|
||||
};
|
19
src/converters/createPenpotCircle.ts
Normal file
19
src/converters/createPenpotCircle.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { NodeData } from '../interfaces';
|
||||
import { PenpotFile } from '../penpot';
|
||||
import { translateFills } from '../translators';
|
||||
|
||||
export const createPenpotCircle = (
|
||||
file: PenpotFile,
|
||||
node: NodeData,
|
||||
baseX: number,
|
||||
baseY: number
|
||||
) => {
|
||||
file.createCircle({
|
||||
name: node.name,
|
||||
x: node.x + baseX,
|
||||
y: node.y + baseY,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
fills: translateFills(node.fills /*, node.width, node.height*/)
|
||||
});
|
||||
};
|
11
src/converters/createPenpotFile.ts
Normal file
11
src/converters/createPenpotFile.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { createPenpotItem } from '.';
|
||||
import { NodeData } from '../interfaces';
|
||||
import { createFile } from '../penpot';
|
||||
|
||||
export const createPenpotFile = (node: NodeData) => {
|
||||
const file = createFile(node.name);
|
||||
for (const page of node.children) {
|
||||
createPenpotItem(file, page, 0, 0);
|
||||
}
|
||||
return file;
|
||||
};
|
16
src/converters/createPenpotGroup.ts
Normal file
16
src/converters/createPenpotGroup.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { createPenpotItem } from '.';
|
||||
import { NodeData } from '../interfaces';
|
||||
import { PenpotFile } from '../penpot';
|
||||
|
||||
export const createPenpotGroup = (
|
||||
file: PenpotFile,
|
||||
node: NodeData,
|
||||
baseX: number,
|
||||
baseY: number
|
||||
) => {
|
||||
file.addGroup({ name: node.name });
|
||||
for (const child of node.children) {
|
||||
createPenpotItem(file, child, baseX, baseY);
|
||||
}
|
||||
file.closeGroup();
|
||||
};
|
22
src/converters/createPenpotImage.ts
Normal file
22
src/converters/createPenpotImage.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { NodeData } from '../interfaces';
|
||||
import { PenpotFile } from '../penpot';
|
||||
|
||||
export const createPenpotImage = (
|
||||
file: PenpotFile,
|
||||
node: NodeData,
|
||||
baseX: number,
|
||||
baseY: number
|
||||
) => {
|
||||
file.createImage({
|
||||
name: node.name,
|
||||
x: node.x + baseX,
|
||||
y: node.y + baseY,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
metadata: {
|
||||
width: node.width,
|
||||
height: node.height
|
||||
},
|
||||
dataUri: node.imageFill
|
||||
});
|
||||
};
|
46
src/converters/createPenpotItem.ts
Normal file
46
src/converters/createPenpotItem.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import {
|
||||
createPenpotBoard,
|
||||
createPenpotCircle,
|
||||
createPenpotGroup,
|
||||
createPenpotImage,
|
||||
createPenpotPage,
|
||||
createPenpotRectangle,
|
||||
createPenpotText
|
||||
} from '.';
|
||||
import { NodeData, TextData } from '../interfaces';
|
||||
import { PenpotFile } from '../penpot';
|
||||
import { calculateAdjustment } from '../utils';
|
||||
|
||||
export const createPenpotItem = (
|
||||
file: PenpotFile,
|
||||
node: NodeData,
|
||||
baseX: number,
|
||||
baseY: number
|
||||
) => {
|
||||
// We special-case images because an image in figma is a shape with one or many
|
||||
// image fills. Given that handling images in Penpot is a bit different, we
|
||||
// rasterize a figma shape with any image fills to a PNG and then add it as a single
|
||||
// Penpot image. Implication is that any node that has an image fill will only be
|
||||
// treated as an image, so we skip node type checks.
|
||||
const hasImageFill = node.fills?.some((fill: Paint) => fill.type === 'IMAGE');
|
||||
if (hasImageFill) {
|
||||
// If the nested frames extended the bounds of the rasterized image, we need to
|
||||
// account for this both in position on the canvas and the calculated width and
|
||||
// height of the image.
|
||||
const [adjustedX, adjustedY] = calculateAdjustment(node);
|
||||
|
||||
createPenpotImage(file, node, baseX + adjustedX, baseY + adjustedY);
|
||||
} else if (node.type == 'PAGE') {
|
||||
createPenpotPage(file, node);
|
||||
} else if (node.type == 'FRAME') {
|
||||
createPenpotBoard(file, node, baseX, baseY);
|
||||
} else if (node.type == 'GROUP') {
|
||||
createPenpotGroup(file, node, baseX, baseY);
|
||||
} else if (node.type == 'RECTANGLE') {
|
||||
createPenpotRectangle(file, node, baseX, baseY);
|
||||
} else if (node.type == 'ELLIPSE') {
|
||||
createPenpotCircle(file, node, baseX, baseY);
|
||||
} else if (node.type == 'TEXT') {
|
||||
createPenpotText(file, node as unknown as TextData, baseX, baseY);
|
||||
}
|
||||
};
|
11
src/converters/createPenpotPage.ts
Normal file
11
src/converters/createPenpotPage.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { createPenpotItem } from '.';
|
||||
import { NodeData } from '../interfaces';
|
||||
import { PenpotFile } from '../penpot';
|
||||
|
||||
export const createPenpotPage = (file: PenpotFile, node: NodeData) => {
|
||||
file.addPage(node.name);
|
||||
for (const child of node.children) {
|
||||
createPenpotItem(file, child, 0, 0);
|
||||
}
|
||||
file.closePage();
|
||||
};
|
19
src/converters/createPenpotRectangle.ts
Normal file
19
src/converters/createPenpotRectangle.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { NodeData } from '../interfaces';
|
||||
import { PenpotFile } from '../penpot';
|
||||
import { translateFills } from '../translators';
|
||||
|
||||
export const createPenpotRectangle = (
|
||||
file: PenpotFile,
|
||||
node: NodeData,
|
||||
baseX: number,
|
||||
baseY: number
|
||||
) => {
|
||||
file.createRect({
|
||||
name: node.name,
|
||||
x: node.x + baseX,
|
||||
y: node.y + baseY,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
fills: translateFills(node.fills /*, node.width, node.height*/)
|
||||
});
|
||||
};
|
78
src/converters/createPenpotText.ts
Normal file
78
src/converters/createPenpotText.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
import slugify from 'slugify';
|
||||
|
||||
import { TextData } from '../interfaces';
|
||||
import { PenpotFile } from '../penpot';
|
||||
import {
|
||||
translateFills,
|
||||
translateFontStyle,
|
||||
translateHorizontalAlign,
|
||||
translateTextDecoration,
|
||||
translateTextTransform,
|
||||
translateVerticalAlign
|
||||
} from '../translators';
|
||||
import { validateFont } from '../validators';
|
||||
|
||||
export const createPenpotText = (
|
||||
file: PenpotFile,
|
||||
node: TextData,
|
||||
baseX: number,
|
||||
baseY: number
|
||||
) => {
|
||||
const children = node.children.map(val => {
|
||||
validateFont(val.fontName);
|
||||
|
||||
return {
|
||||
lineHeight: val.lineHeight,
|
||||
fontStyle: 'normal',
|
||||
textAlign: translateHorizontalAlign(node.textAlignHorizontal),
|
||||
fontId: 'gfont-' + slugify(val.fontName.family.toLowerCase()),
|
||||
fontSize: val.fontSize.toString(),
|
||||
fontWeight: val.fontWeight.toString(),
|
||||
fontVariantId: translateFontStyle(val.fontName.style),
|
||||
textDecoration: translateTextDecoration(val),
|
||||
textTransform: translateTextTransform(val),
|
||||
letterSpacing: val.letterSpacing,
|
||||
fills: translateFills(val.fills /*, node.width, node.height*/),
|
||||
fontFamily: val.fontName.family,
|
||||
text: val.characters
|
||||
};
|
||||
});
|
||||
|
||||
validateFont(node.fontName);
|
||||
|
||||
file.createText({
|
||||
name: node.name,
|
||||
x: node.x + baseX,
|
||||
y: node.y + baseY,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
rotation: 0,
|
||||
type: Symbol.for('text'),
|
||||
content: {
|
||||
type: 'root',
|
||||
verticalAlign: translateVerticalAlign(node.textAlignVertical),
|
||||
children: [
|
||||
{
|
||||
type: 'paragraph-set',
|
||||
children: [
|
||||
{
|
||||
lineHeight: node.lineHeight,
|
||||
fontStyle: 'normal',
|
||||
children: children,
|
||||
textTransform: translateTextTransform(node),
|
||||
textAlign: translateHorizontalAlign(node.textAlignHorizontal),
|
||||
fontId: 'gfont-' + slugify(node.fontName.family.toLowerCase()),
|
||||
fontSize: node.fontSize.toString(),
|
||||
fontWeight: node.fontWeight.toString(),
|
||||
type: 'paragraph',
|
||||
textDecoration: translateTextDecoration(node),
|
||||
letterSpacing: node.letterSpacing,
|
||||
fills: translateFills(node.fills /*, node.width, node.height*/),
|
||||
fontFamily: node.fontName.family
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
};
|
9
src/converters/index.ts
Normal file
9
src/converters/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
export * from './createPenpotBoard';
|
||||
export * from './createPenpotCircle';
|
||||
export * from './createPenpotFile';
|
||||
export * from './createPenpotGroup';
|
||||
export * from './createPenpotImage';
|
||||
export * from './createPenpotItem';
|
||||
export * from './createPenpotPage';
|
||||
export * from './createPenpotRectangle';
|
||||
export * from './createPenpotText';
|
8
src/translators/index.ts
Normal file
8
src/translators/index.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export * from './translateFills';
|
||||
export * from './translateFontStyle';
|
||||
export * from './translateGradientLinearFill';
|
||||
export * from './translateHorizontalAlign';
|
||||
export * from './translateSolidFill';
|
||||
export * from './translateTextDecoration';
|
||||
export * from './translateTextTransform';
|
||||
export * from './translateVerticalAlign';
|
25
src/translators/translateFills.ts
Normal file
25
src/translators/translateFills.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { translateGradientLinearFill, translateSolidFill } from '.';
|
||||
|
||||
const translateFill = (fill: Paint /*, width: number, height: number*/) => {
|
||||
if (fill.type === 'SOLID') {
|
||||
return translateSolidFill(fill);
|
||||
} else if (fill.type === 'GRADIENT_LINEAR') {
|
||||
return translateGradientLinearFill(fill /*, width, height*/);
|
||||
} else {
|
||||
console.error('Color type ' + fill.type + ' not supported yet');
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const translateFills = (fills: readonly Paint[] /*, width: number, height: number*/) => {
|
||||
const penpotFills = [];
|
||||
let penpotFill = null;
|
||||
for (const fill of fills) {
|
||||
penpotFill = translateFill(fill /*, width, height*/);
|
||||
|
||||
if (penpotFill !== null) {
|
||||
penpotFills.unshift(penpotFill);
|
||||
}
|
||||
}
|
||||
return penpotFills;
|
||||
};
|
3
src/translators/translateFontStyle.ts
Normal file
3
src/translators/translateFontStyle.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export const translateFontStyle = (style: string) => {
|
||||
return style.toLowerCase().replace(/\s/g, '');
|
||||
};
|
30
src/translators/translateGradientLinearFill.ts
Normal file
30
src/translators/translateGradientLinearFill.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { rgbToHex } from '../utils';
|
||||
|
||||
export const translateGradientLinearFill = (
|
||||
fill: GradientPaint /*, width: number, height: number*/
|
||||
) => {
|
||||
// const points = extractLinearGradientParamsFromTransform(width, height, fill.gradientTransform);
|
||||
return {
|
||||
fillColorGradient: {
|
||||
type: Symbol.for('linear'),
|
||||
width: 1,
|
||||
// startX: points.start[0] / width,
|
||||
// startY: points.start[1] / height,
|
||||
// endX: points.end[0] / width,
|
||||
// endY: points.end[1] / height,
|
||||
stops: [
|
||||
{
|
||||
color: rgbToHex(fill.gradientStops[0].color),
|
||||
offset: fill.gradientStops[0].position,
|
||||
opacity: fill.gradientStops[0].color.a * (fill.opacity ?? 1)
|
||||
},
|
||||
{
|
||||
color: rgbToHex(fill.gradientStops[1].color),
|
||||
offset: fill.gradientStops[1].position,
|
||||
opacity: fill.gradientStops[1].color.a * (fill.opacity ?? 1)
|
||||
}
|
||||
]
|
||||
},
|
||||
fillOpacity: fill.visible === false ? 0 : undefined
|
||||
};
|
||||
};
|
9
src/translators/translateHorizontalAlign.ts
Normal file
9
src/translators/translateHorizontalAlign.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
export const translateHorizontalAlign = (align: string) => {
|
||||
if (align === 'RIGHT') {
|
||||
return Symbol.for('right');
|
||||
}
|
||||
if (align === 'CENTER') {
|
||||
return Symbol.for('center');
|
||||
}
|
||||
return Symbol.for('left');
|
||||
};
|
8
src/translators/translateSolidFill.ts
Normal file
8
src/translators/translateSolidFill.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { rgbToHex } from '../utils';
|
||||
|
||||
export const translateSolidFill = (fill: SolidPaint) => {
|
||||
return {
|
||||
fillColor: rgbToHex(fill.color),
|
||||
fillOpacity: fill.visible === false ? 0 : fill.opacity
|
||||
};
|
||||
};
|
12
src/translators/translateTextDecoration.ts
Normal file
12
src/translators/translateTextDecoration.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { TextData, TextDataChildren } from '../interfaces.js';
|
||||
|
||||
export const translateTextDecoration = (node: TextData | TextDataChildren) => {
|
||||
const textDecoration = node.textDecoration;
|
||||
if (textDecoration === 'STRIKETHROUGH') {
|
||||
return 'line-through';
|
||||
}
|
||||
if (textDecoration === 'UNDERLINE') {
|
||||
return 'underline';
|
||||
}
|
||||
return 'none';
|
||||
};
|
15
src/translators/translateTextTransform.ts
Normal file
15
src/translators/translateTextTransform.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { TextData, TextDataChildren } from '../interfaces.js';
|
||||
|
||||
export const translateTextTransform = (node: TextData | TextDataChildren) => {
|
||||
const textCase = node.textCase;
|
||||
if (textCase === 'UPPER') {
|
||||
return 'uppercase';
|
||||
}
|
||||
if (textCase === 'LOWER') {
|
||||
return 'lowercase';
|
||||
}
|
||||
if (textCase === 'TITLE') {
|
||||
return 'capitalize';
|
||||
}
|
||||
return 'none';
|
||||
};
|
9
src/translators/translateVerticalAlign.ts
Normal file
9
src/translators/translateVerticalAlign.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
export const translateVerticalAlign = (align: string) => {
|
||||
if (align === 'BOTTOM') {
|
||||
return Symbol.for('bottom');
|
||||
}
|
||||
if (align === 'CENTER') {
|
||||
return Symbol.for('center');
|
||||
}
|
||||
return Symbol.for('top');
|
||||
};
|
325
src/ui.tsx
325
src/ui.tsx
|
@ -1,14 +1,9 @@
|
|||
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 { createPenpotFile } from './converters';
|
||||
import './ui.css';
|
||||
|
||||
const gfonts = new Set(fonts);
|
||||
|
||||
type PenpotExporterState = {
|
||||
missingFonts: Set<string>;
|
||||
exporting: boolean;
|
||||
|
@ -32,315 +27,13 @@ export default class PenpotExporter extends React.Component<unknown, PenpotExpor
|
|||
window.removeEventListener('message', this.onMessage);
|
||||
};
|
||||
|
||||
rgbToHex = (color: RGB) => {
|
||||
const r = Math.round(255 * color.r);
|
||||
const g = Math.round(255 * color.g);
|
||||
const b = Math.round(255 * color.b);
|
||||
const rgb = (r << 16) | (g << 8) | (b << 0);
|
||||
return '#' + (0x1000000 + rgb).toString(16).slice(1);
|
||||
};
|
||||
|
||||
translateSolidFill(fill: SolidPaint) {
|
||||
return {
|
||||
fillColor: this.rgbToHex(fill.color),
|
||||
fillOpacity: fill.visible === false ? 0 : fill.opacity
|
||||
};
|
||||
}
|
||||
|
||||
translateGradientLinearFill(fill: GradientPaint /*, width: number, height: number*/) {
|
||||
// const points = extractLinearGradientParamsFromTransform(width, height, fill.gradientTransform);
|
||||
return {
|
||||
fillColorGradient: {
|
||||
type: Symbol.for('linear'),
|
||||
width: 1,
|
||||
// startX: points.start[0] / width,
|
||||
// startY: points.start[1] / height,
|
||||
// endX: points.end[0] / width,
|
||||
// endY: points.end[1] / height,
|
||||
stops: [
|
||||
{
|
||||
color: this.rgbToHex(fill.gradientStops[0].color),
|
||||
offset: fill.gradientStops[0].position,
|
||||
opacity: fill.gradientStops[0].color.a * (fill.opacity ?? 1)
|
||||
},
|
||||
{
|
||||
color: this.rgbToHex(fill.gradientStops[1].color),
|
||||
offset: fill.gradientStops[1].position,
|
||||
opacity: fill.gradientStops[1].color.a * (fill.opacity ?? 1)
|
||||
}
|
||||
]
|
||||
},
|
||||
fillOpacity: fill.visible === false ? 0 : undefined
|
||||
};
|
||||
}
|
||||
|
||||
translateFill(fill: Paint /*, width: number, height: number*/) {
|
||||
if (fill.type === 'SOLID') {
|
||||
return this.translateSolidFill(fill);
|
||||
} else if (fill.type === 'GRADIENT_LINEAR') {
|
||||
return this.translateGradientLinearFill(fill /*, width, height*/);
|
||||
} else {
|
||||
console.error('Color type ' + fill.type + ' not supported yet');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
translateFills(fills: readonly Paint[] /*, width: number, height: number*/) {
|
||||
const penpotFills = [];
|
||||
let penpotFill = null;
|
||||
for (const fill of fills) {
|
||||
penpotFill = this.translateFill(fill /*, width, height*/);
|
||||
|
||||
if (penpotFill !== null) {
|
||||
penpotFills.unshift(penpotFill);
|
||||
}
|
||||
}
|
||||
return penpotFills;
|
||||
}
|
||||
|
||||
addFontWarning(font: string) {
|
||||
const newMissingFonts = this.state.missingFonts;
|
||||
newMissingFonts.add(font);
|
||||
|
||||
this.setState(() => ({ missingFonts: newMissingFonts }));
|
||||
}
|
||||
|
||||
createPenpotPage(file: PenpotFile, node: NodeData) {
|
||||
file.addPage(node.name);
|
||||
for (const child of node.children) {
|
||||
this.createPenpotItem(file, child, 0, 0);
|
||||
}
|
||||
file.closePage();
|
||||
}
|
||||
|
||||
createPenpotBoard(file: PenpotFile, node: NodeData, baseX: number, baseY: number) {
|
||||
file.addArtboard({
|
||||
name: node.name,
|
||||
x: node.x + baseX,
|
||||
y: node.y + baseY,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
fills: this.translateFills(node.fills /*, node.width, node.height*/)
|
||||
});
|
||||
for (const child of node.children) {
|
||||
this.createPenpotItem(file, child, node.x + baseX, node.y + baseY);
|
||||
}
|
||||
file.closeArtboard();
|
||||
}
|
||||
|
||||
createPenpotGroup(file: PenpotFile, node: NodeData, baseX: number, baseY: number) {
|
||||
file.addGroup({ name: node.name });
|
||||
for (const child of node.children) {
|
||||
this.createPenpotItem(file, child, baseX, baseY);
|
||||
}
|
||||
file.closeGroup();
|
||||
}
|
||||
|
||||
createPenpotRectangle(file: PenpotFile, node: NodeData, baseX: number, baseY: number) {
|
||||
file.createRect({
|
||||
name: node.name,
|
||||
x: node.x + baseX,
|
||||
y: node.y + baseY,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
fills: this.translateFills(node.fills /*, node.width, node.height*/)
|
||||
});
|
||||
}
|
||||
|
||||
createPenpotCircle(file: PenpotFile, node: NodeData, baseX: number, baseY: number) {
|
||||
file.createCircle({
|
||||
name: node.name,
|
||||
x: node.x + baseX,
|
||||
y: node.y + baseY,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
fills: this.translateFills(node.fills /*, node.width, node.height*/)
|
||||
});
|
||||
}
|
||||
|
||||
translateHorizontalAlign(align: string) {
|
||||
if (align === 'RIGHT') {
|
||||
return Symbol.for('right');
|
||||
}
|
||||
if (align === 'CENTER') {
|
||||
return Symbol.for('center');
|
||||
}
|
||||
return Symbol.for('left');
|
||||
}
|
||||
|
||||
translateVerticalAlign(align: string) {
|
||||
if (align === 'BOTTOM') {
|
||||
return Symbol.for('bottom');
|
||||
}
|
||||
if (align === 'CENTER') {
|
||||
return Symbol.for('center');
|
||||
}
|
||||
return Symbol.for('top');
|
||||
}
|
||||
|
||||
translateFontStyle(style: string) {
|
||||
return style.toLowerCase().replace(/\s/g, '');
|
||||
}
|
||||
|
||||
getTextDecoration(node: TextData | TextDataChildren) {
|
||||
const textDecoration = node.textDecoration;
|
||||
if (textDecoration === 'STRIKETHROUGH') {
|
||||
return 'line-through';
|
||||
}
|
||||
if (textDecoration === 'UNDERLINE') {
|
||||
return 'underline';
|
||||
}
|
||||
return 'none';
|
||||
}
|
||||
|
||||
getTextTransform(node: TextData | TextDataChildren) {
|
||||
const textCase = node.textCase;
|
||||
if (textCase === 'UPPER') {
|
||||
return 'uppercase';
|
||||
}
|
||||
if (textCase === 'LOWER') {
|
||||
return 'lowercase';
|
||||
}
|
||||
if (textCase === 'TITLE') {
|
||||
return 'capitalize';
|
||||
}
|
||||
return 'none';
|
||||
}
|
||||
|
||||
validateFont(fontName: FontName) {
|
||||
const name = slugify(fontName.family.toLowerCase());
|
||||
if (!gfonts.has(name)) {
|
||||
this.addFontWarning(name);
|
||||
}
|
||||
}
|
||||
|
||||
createPenpotText(file: PenpotFile, node: TextData, baseX: number, baseY: number) {
|
||||
const children = node.children.map(val => {
|
||||
this.validateFont(val.fontName);
|
||||
|
||||
return {
|
||||
lineHeight: val.lineHeight,
|
||||
fontStyle: 'normal',
|
||||
textAlign: this.translateHorizontalAlign(node.textAlignHorizontal),
|
||||
fontId: 'gfont-' + slugify(val.fontName.family.toLowerCase()),
|
||||
fontSize: val.fontSize.toString(),
|
||||
fontWeight: val.fontWeight.toString(),
|
||||
fontVariantId: this.translateFontStyle(val.fontName.style),
|
||||
textDecoration: this.getTextDecoration(val),
|
||||
textTransform: this.getTextTransform(val),
|
||||
letterSpacing: val.letterSpacing,
|
||||
fills: this.translateFills(val.fills /*, node.width, node.height*/),
|
||||
fontFamily: val.fontName.family,
|
||||
text: val.characters
|
||||
};
|
||||
});
|
||||
|
||||
this.validateFont(node.fontName);
|
||||
|
||||
file.createText({
|
||||
name: node.name,
|
||||
x: node.x + baseX,
|
||||
y: node.y + baseY,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
rotation: 0,
|
||||
type: Symbol.for('text'),
|
||||
content: {
|
||||
type: 'root',
|
||||
verticalAlign: this.translateVerticalAlign(node.textAlignVertical),
|
||||
children: [
|
||||
{
|
||||
type: 'paragraph-set',
|
||||
children: [
|
||||
{
|
||||
lineHeight: node.lineHeight,
|
||||
fontStyle: 'normal',
|
||||
children: children,
|
||||
textTransform: this.getTextTransform(node),
|
||||
textAlign: this.translateHorizontalAlign(node.textAlignHorizontal),
|
||||
fontId: 'gfont-' + slugify(node.fontName.family.toLowerCase()),
|
||||
fontSize: node.fontSize.toString(),
|
||||
fontWeight: node.fontWeight.toString(),
|
||||
type: 'paragraph',
|
||||
textDecoration: this.getTextDecoration(node),
|
||||
letterSpacing: node.letterSpacing,
|
||||
fills: this.translateFills(node.fills /*, node.width, node.height*/),
|
||||
fontFamily: node.fontName.family
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createPenpotImage(file: PenpotFile, node: NodeData, baseX: number, baseY: number) {
|
||||
file.createImage({
|
||||
name: node.name,
|
||||
x: node.x + baseX,
|
||||
y: node.y + baseY,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
metadata: {
|
||||
width: node.width,
|
||||
height: node.height
|
||||
},
|
||||
dataUri: node.imageFill
|
||||
});
|
||||
}
|
||||
|
||||
calculateAdjustment(node: NodeData) {
|
||||
// For each child, check whether the X or Y position is less than 0 and less than the
|
||||
// current adjustment.
|
||||
let adjustedX = 0;
|
||||
let adjustedY = 0;
|
||||
for (const child of node.children) {
|
||||
if (child.x < adjustedX) {
|
||||
adjustedX = child.x;
|
||||
}
|
||||
if (child.y < adjustedY) {
|
||||
adjustedY = child.y;
|
||||
}
|
||||
}
|
||||
return [adjustedX, adjustedY];
|
||||
}
|
||||
|
||||
createPenpotItem(file: PenpotFile, node: NodeData, baseX: number, baseY: number) {
|
||||
// We special-case images because an image in figma is a shape with one or many
|
||||
// image fills. Given that handling images in Penpot is a bit different, we
|
||||
// rasterize a figma shape with any image fills to a PNG and then add it as a single
|
||||
// Penpot image. Implication is that any node that has an image fill will only be
|
||||
// treated as an image, so we skip node type checks.
|
||||
const hasImageFill = node.fills?.some((fill: Paint) => fill.type === 'IMAGE');
|
||||
if (hasImageFill) {
|
||||
// If the nested frames extended the bounds of the rasterized image, we need to
|
||||
// account for this both in position on the canvas and the calculated width and
|
||||
// height of the image.
|
||||
const [adjustedX, adjustedY] = this.calculateAdjustment(node);
|
||||
|
||||
this.createPenpotImage(file, node, baseX + adjustedX, baseY + adjustedY);
|
||||
} else if (node.type == 'PAGE') {
|
||||
this.createPenpotPage(file, node);
|
||||
} else if (node.type == 'FRAME') {
|
||||
this.createPenpotBoard(file, node, baseX, baseY);
|
||||
} else if (node.type == 'GROUP') {
|
||||
this.createPenpotGroup(file, node, baseX, baseY);
|
||||
} else if (node.type == 'RECTANGLE') {
|
||||
this.createPenpotRectangle(file, node, baseX, baseY);
|
||||
} else if (node.type == 'ELLIPSE') {
|
||||
this.createPenpotCircle(file, node, baseX, baseY);
|
||||
} else if (node.type == 'TEXT') {
|
||||
this.createPenpotText(file, node as unknown as TextData, baseX, baseY);
|
||||
}
|
||||
}
|
||||
|
||||
createPenpotFile(node: NodeData) {
|
||||
const file = createFile(node.name);
|
||||
for (const page of node.children) {
|
||||
this.createPenpotItem(file, page, 0, 0);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
// TODO: FIX THIS CODE
|
||||
// addFontWarning(font: string) {
|
||||
// const newMissingFonts = this.state.missingFonts;
|
||||
// newMissingFonts.add(font);
|
||||
//
|
||||
// this.setState(() => ({ missingFonts: newMissingFonts }));
|
||||
// }
|
||||
|
||||
onCreatePenpot = () => {
|
||||
this.setState(() => ({ exporting: true }));
|
||||
|
@ -354,7 +47,7 @@ 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') {
|
||||
const file = this.createPenpotFile(event.data.pluginMessage.data);
|
||||
const file = createPenpotFile(event.data.pluginMessage.data);
|
||||
file.export();
|
||||
this.setState(() => ({ exporting: false }));
|
||||
}
|
||||
|
|
17
src/utils/calculateAdjustment.ts
Normal file
17
src/utils/calculateAdjustment.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { NodeData } from '../interfaces.js';
|
||||
|
||||
export const calculateAdjustment = (node: NodeData) => {
|
||||
// For each child, check whether the X or Y position is less than 0 and less than the
|
||||
// current adjustment.
|
||||
let adjustedX = 0;
|
||||
let adjustedY = 0;
|
||||
for (const child of node.children) {
|
||||
if (child.x < adjustedX) {
|
||||
adjustedX = child.x;
|
||||
}
|
||||
if (child.y < adjustedY) {
|
||||
adjustedY = child.y;
|
||||
}
|
||||
}
|
||||
return [adjustedX, adjustedY];
|
||||
};
|
2
src/utils/index.ts
Normal file
2
src/utils/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from './calculateAdjustment';
|
||||
export * from './rgbToHex';
|
7
src/utils/rgbToHex.ts
Normal file
7
src/utils/rgbToHex.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export const rgbToHex = (color: RGB) => {
|
||||
const r = Math.round(255 * color.r);
|
||||
const g = Math.round(255 * color.g);
|
||||
const b = Math.round(255 * color.b);
|
||||
const rgb = (r << 16) | (g << 8) | (b << 0);
|
||||
return '#' + (0x1000000 + rgb).toString(16).slice(1);
|
||||
};
|
1
src/validators/index.ts
Normal file
1
src/validators/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './validateFont';
|
10
src/validators/validateFont.ts
Normal file
10
src/validators/validateFont.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import slugify from 'slugify';
|
||||
|
||||
import fonts from '../gfonts.json';
|
||||
|
||||
const gfonts = new Set(fonts);
|
||||
|
||||
export const validateFont = (fontName: FontName): boolean => {
|
||||
const name = slugify(fontName.family.toLowerCase());
|
||||
return gfonts.has(name);
|
||||
};
|
Loading…
Reference in a new issue