From c2d16dd3b0387700f0ad6296fdde016a8a838c59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=A1nchez?= Date: Mon, 8 Apr 2024 17:50:01 +0200 Subject: [PATCH] updated structure --- src/converters/createPenpotBoard.ts | 24 ++ src/converters/createPenpotCircle.ts | 19 + src/converters/createPenpotFile.ts | 11 + src/converters/createPenpotGroup.ts | 16 + src/converters/createPenpotImage.ts | 22 ++ src/converters/createPenpotItem.ts | 46 +++ src/converters/createPenpotPage.ts | 11 + src/converters/createPenpotRectangle.ts | 19 + src/converters/createPenpotText.ts | 78 +++++ src/converters/index.ts | 9 + src/translators/index.ts | 8 + src/translators/translateFills.ts | 25 ++ src/translators/translateFontStyle.ts | 3 + .../translateGradientLinearFill.ts | 30 ++ src/translators/translateHorizontalAlign.ts | 9 + src/translators/translateSolidFill.ts | 8 + src/translators/translateTextDecoration.ts | 12 + src/translators/translateTextTransform.ts | 15 + src/translators/translateVerticalAlign.ts | 9 + src/ui.tsx | 325 +----------------- src/utils/calculateAdjustment.ts | 17 + src/utils/index.ts | 2 + src/utils/rgbToHex.ts | 7 + src/validators/index.ts | 1 + src/validators/validateFont.ts | 10 + 25 files changed, 420 insertions(+), 316 deletions(-) create mode 100644 src/converters/createPenpotBoard.ts create mode 100644 src/converters/createPenpotCircle.ts create mode 100644 src/converters/createPenpotFile.ts create mode 100644 src/converters/createPenpotGroup.ts create mode 100644 src/converters/createPenpotImage.ts create mode 100644 src/converters/createPenpotItem.ts create mode 100644 src/converters/createPenpotPage.ts create mode 100644 src/converters/createPenpotRectangle.ts create mode 100644 src/converters/createPenpotText.ts create mode 100644 src/converters/index.ts create mode 100644 src/translators/index.ts create mode 100644 src/translators/translateFills.ts create mode 100644 src/translators/translateFontStyle.ts create mode 100644 src/translators/translateGradientLinearFill.ts create mode 100644 src/translators/translateHorizontalAlign.ts create mode 100644 src/translators/translateSolidFill.ts create mode 100644 src/translators/translateTextDecoration.ts create mode 100644 src/translators/translateTextTransform.ts create mode 100644 src/translators/translateVerticalAlign.ts create mode 100644 src/utils/calculateAdjustment.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/rgbToHex.ts create mode 100644 src/validators/index.ts create mode 100644 src/validators/validateFont.ts diff --git a/src/converters/createPenpotBoard.ts b/src/converters/createPenpotBoard.ts new file mode 100644 index 0000000..4bec289 --- /dev/null +++ b/src/converters/createPenpotBoard.ts @@ -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(); +}; diff --git a/src/converters/createPenpotCircle.ts b/src/converters/createPenpotCircle.ts new file mode 100644 index 0000000..d68899a --- /dev/null +++ b/src/converters/createPenpotCircle.ts @@ -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*/) + }); +}; diff --git a/src/converters/createPenpotFile.ts b/src/converters/createPenpotFile.ts new file mode 100644 index 0000000..1e1bbdc --- /dev/null +++ b/src/converters/createPenpotFile.ts @@ -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; +}; diff --git a/src/converters/createPenpotGroup.ts b/src/converters/createPenpotGroup.ts new file mode 100644 index 0000000..ac41579 --- /dev/null +++ b/src/converters/createPenpotGroup.ts @@ -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(); +}; diff --git a/src/converters/createPenpotImage.ts b/src/converters/createPenpotImage.ts new file mode 100644 index 0000000..d015df1 --- /dev/null +++ b/src/converters/createPenpotImage.ts @@ -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 + }); +}; diff --git a/src/converters/createPenpotItem.ts b/src/converters/createPenpotItem.ts new file mode 100644 index 0000000..d7cd75b --- /dev/null +++ b/src/converters/createPenpotItem.ts @@ -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); + } +}; diff --git a/src/converters/createPenpotPage.ts b/src/converters/createPenpotPage.ts new file mode 100644 index 0000000..4f324c2 --- /dev/null +++ b/src/converters/createPenpotPage.ts @@ -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(); +}; diff --git a/src/converters/createPenpotRectangle.ts b/src/converters/createPenpotRectangle.ts new file mode 100644 index 0000000..56b9f64 --- /dev/null +++ b/src/converters/createPenpotRectangle.ts @@ -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*/) + }); +}; diff --git a/src/converters/createPenpotText.ts b/src/converters/createPenpotText.ts new file mode 100644 index 0000000..4504636 --- /dev/null +++ b/src/converters/createPenpotText.ts @@ -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 + } + ] + } + ] + } + }); +}; diff --git a/src/converters/index.ts b/src/converters/index.ts new file mode 100644 index 0000000..6512bf5 --- /dev/null +++ b/src/converters/index.ts @@ -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'; diff --git a/src/translators/index.ts b/src/translators/index.ts new file mode 100644 index 0000000..3d86e99 --- /dev/null +++ b/src/translators/index.ts @@ -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'; diff --git a/src/translators/translateFills.ts b/src/translators/translateFills.ts new file mode 100644 index 0000000..778db5c --- /dev/null +++ b/src/translators/translateFills.ts @@ -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; +}; diff --git a/src/translators/translateFontStyle.ts b/src/translators/translateFontStyle.ts new file mode 100644 index 0000000..4243919 --- /dev/null +++ b/src/translators/translateFontStyle.ts @@ -0,0 +1,3 @@ +export const translateFontStyle = (style: string) => { + return style.toLowerCase().replace(/\s/g, ''); +}; diff --git a/src/translators/translateGradientLinearFill.ts b/src/translators/translateGradientLinearFill.ts new file mode 100644 index 0000000..06d3018 --- /dev/null +++ b/src/translators/translateGradientLinearFill.ts @@ -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 + }; +}; diff --git a/src/translators/translateHorizontalAlign.ts b/src/translators/translateHorizontalAlign.ts new file mode 100644 index 0000000..dccb18a --- /dev/null +++ b/src/translators/translateHorizontalAlign.ts @@ -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'); +}; diff --git a/src/translators/translateSolidFill.ts b/src/translators/translateSolidFill.ts new file mode 100644 index 0000000..17925a9 --- /dev/null +++ b/src/translators/translateSolidFill.ts @@ -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 + }; +}; diff --git a/src/translators/translateTextDecoration.ts b/src/translators/translateTextDecoration.ts new file mode 100644 index 0000000..e95afb2 --- /dev/null +++ b/src/translators/translateTextDecoration.ts @@ -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'; +}; diff --git a/src/translators/translateTextTransform.ts b/src/translators/translateTextTransform.ts new file mode 100644 index 0000000..1861683 --- /dev/null +++ b/src/translators/translateTextTransform.ts @@ -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'; +}; diff --git a/src/translators/translateVerticalAlign.ts b/src/translators/translateVerticalAlign.ts new file mode 100644 index 0000000..94f31e4 --- /dev/null +++ b/src/translators/translateVerticalAlign.ts @@ -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'); +}; diff --git a/src/ui.tsx b/src/ui.tsx index b615dc2..175d9b3 100644 --- a/src/ui.tsx +++ b/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; exporting: boolean; @@ -32,315 +27,13 @@ export default class PenpotExporter extends React.Component { - 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 { 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 })); } diff --git a/src/utils/calculateAdjustment.ts b/src/utils/calculateAdjustment.ts new file mode 100644 index 0000000..da4f1c6 --- /dev/null +++ b/src/utils/calculateAdjustment.ts @@ -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]; +}; diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..1cfdeff --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from './calculateAdjustment'; +export * from './rgbToHex'; diff --git a/src/utils/rgbToHex.ts b/src/utils/rgbToHex.ts new file mode 100644 index 0000000..31e50c7 --- /dev/null +++ b/src/utils/rgbToHex.ts @@ -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); +}; diff --git a/src/validators/index.ts b/src/validators/index.ts new file mode 100644 index 0000000..dabea7f --- /dev/null +++ b/src/validators/index.ts @@ -0,0 +1 @@ +export * from './validateFont'; diff --git a/src/validators/validateFont.ts b/src/validators/validateFont.ts new file mode 100644 index 0000000..f2d7fe3 --- /dev/null +++ b/src/validators/validateFont.ts @@ -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); +};