From 6c67200dc60ba290928166020375387068278514 Mon Sep 17 00:00:00 2001 From: Jordi Sala Morales Date: Fri, 12 Apr 2024 13:55:42 +0200 Subject: [PATCH] Move logic from UI to Code (#15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * draft * wip * WIP * fix group * wip * add todos * fix * fixes * fixes * image * fix * fix * calculate adjustment * text * text * improve texts * fix * gradient wip * commented gradient * simplify code * cleanup * more cleanup --------- Co-authored-by: Alex Sánchez --- src/common/interfaces.ts | 55 ------------- src/plugin/figma/index.ts | 1 - src/plugin/figma/traverseNode.ts | 24 ------ .../messageHandlers/handleExportMessage.ts | 10 +-- src/plugin/transformers/index.ts | 9 +++ .../transformers/transformDocumentNode.ts | 9 +++ .../transformers/transformEllipseNode.ts | 18 +++++ src/plugin/transformers/transformFrameNode.ts | 22 ++++++ src/plugin/transformers/transformGroupNode.ts | 14 ++++ src/plugin/transformers/transformImageNode.ts | 29 +++++++ src/plugin/transformers/transformPageNode.ts | 9 +++ .../transformers/transformRectangleNode.ts | 18 +++++ src/plugin/transformers/transformSceneNode.ts | 44 +++++++++++ src/plugin/transformers/transformTextNode.ts | 61 +++++++++++++++ src/plugin/translators/index.ts | 5 ++ src/plugin/translators/translateFills.ts | 43 +++++++++++ .../translateGradientLinearFill.ts | 5 +- .../translators/translateSolidFill.ts | 2 +- .../translators/translateTextDecoration.ts | 10 +++ .../translators/translateTextTransform.ts | 12 +++ src/plugin/utils/calculateAdjustment.ts | 17 ++++ src/plugin/utils/detectMimeType.ts | 4 +- src/plugin/utils/getImageFill.ts | 18 ----- src/plugin/utils/getNodeData.ts | 16 ---- src/plugin/utils/getTextData.ts | 34 -------- src/plugin/utils/index.ts | 5 +- src/{ui => plugin}/utils/rgbToHex.ts | 0 src/ui/PenpotExporter.tsx | 21 ++--- src/ui/converters/createGradientFill.ts | 18 +++++ src/ui/converters/createPenpotArtboard.ts | 21 +++++ src/ui/converters/createPenpotBoard.ts | 24 ------ src/ui/converters/createPenpotCircle.ts | 26 +++---- src/ui/converters/createPenpotFile.ts | 16 ++-- src/ui/converters/createPenpotGroup.ts | 25 +++--- src/ui/converters/createPenpotImage.ts | 27 ++----- src/ui/converters/createPenpotItem.ts | 52 ++++--------- src/ui/converters/createPenpotPage.ts | 15 ++-- src/ui/converters/createPenpotRectangle.ts | 26 +++---- src/ui/converters/createPenpotText.ts | 77 +++---------------- src/ui/converters/index.ts | 2 +- src/ui/lib/types/circle/circleAttributes.d.ts | 6 -- src/ui/lib/types/circle/circleAttributes.ts | 8 ++ ...rameAttributes.d.ts => frameAttributes.ts} | 4 +- src/ui/lib/types/frame/frameShape.d.ts | 3 +- ...roupAttributes.d.ts => groupAttributes.ts} | 4 +- src/ui/lib/types/group/groupShape.d.ts | 3 +- ...mageAttributes.d.ts => imageAttributes.ts} | 5 +- src/ui/lib/types/penpotDocument.d.ts | 6 ++ src/ui/lib/types/penpotNode.d.ts | 8 ++ src/ui/lib/types/penpotPage.d.ts | 6 ++ src/ui/lib/types/rect/rectAttributes.d.ts | 6 -- src/ui/lib/types/rect/rectAttributes.ts | 8 ++ ...{textAttributes.d.ts => textAttributes.ts} | 4 +- src/ui/lib/types/utils/children.d.ts | 3 + .../utils/{gradient.d.ts => gradient.ts} | 5 +- src/ui/translators/index.ts | 6 +- src/ui/translators/translateFillGradients.ts | 13 ++++ src/ui/translators/translateFills.ts | 26 ------- src/ui/translators/translateTextDecoration.ts | 12 --- src/ui/translators/translateTextTransform.ts | 15 ---- src/ui/utils/calculateAdjustment.ts | 17 ---- src/ui/utils/index.ts | 2 - src/ui/validators/validateFont.ts | 4 +- 63 files changed, 545 insertions(+), 473 deletions(-) delete mode 100644 src/common/interfaces.ts delete mode 100644 src/plugin/figma/index.ts delete mode 100644 src/plugin/figma/traverseNode.ts create mode 100644 src/plugin/transformers/index.ts create mode 100644 src/plugin/transformers/transformDocumentNode.ts create mode 100644 src/plugin/transformers/transformEllipseNode.ts create mode 100644 src/plugin/transformers/transformFrameNode.ts create mode 100644 src/plugin/transformers/transformGroupNode.ts create mode 100644 src/plugin/transformers/transformImageNode.ts create mode 100644 src/plugin/transformers/transformPageNode.ts create mode 100644 src/plugin/transformers/transformRectangleNode.ts create mode 100644 src/plugin/transformers/transformSceneNode.ts create mode 100644 src/plugin/transformers/transformTextNode.ts create mode 100644 src/plugin/translators/index.ts create mode 100644 src/plugin/translators/translateFills.ts rename src/{ui => plugin}/translators/translateGradientLinearFill.ts (92%) rename src/{ui => plugin}/translators/translateSolidFill.ts (80%) create mode 100644 src/plugin/translators/translateTextDecoration.ts create mode 100644 src/plugin/translators/translateTextTransform.ts create mode 100644 src/plugin/utils/calculateAdjustment.ts delete mode 100644 src/plugin/utils/getImageFill.ts delete mode 100644 src/plugin/utils/getNodeData.ts delete mode 100644 src/plugin/utils/getTextData.ts rename src/{ui => plugin}/utils/rgbToHex.ts (100%) create mode 100644 src/ui/converters/createGradientFill.ts create mode 100644 src/ui/converters/createPenpotArtboard.ts delete mode 100644 src/ui/converters/createPenpotBoard.ts delete mode 100644 src/ui/lib/types/circle/circleAttributes.d.ts create mode 100644 src/ui/lib/types/circle/circleAttributes.ts rename src/ui/lib/types/frame/{frameAttributes.d.ts => frameAttributes.ts} (68%) rename src/ui/lib/types/group/{groupAttributes.d.ts => groupAttributes.ts} (51%) rename src/ui/lib/types/image/{imageAttributes.d.ts => imageAttributes.ts} (66%) create mode 100644 src/ui/lib/types/penpotDocument.d.ts create mode 100644 src/ui/lib/types/penpotNode.d.ts create mode 100644 src/ui/lib/types/penpotPage.d.ts delete mode 100644 src/ui/lib/types/rect/rectAttributes.d.ts create mode 100644 src/ui/lib/types/rect/rectAttributes.ts rename src/ui/lib/types/text/{textAttributes.d.ts => textAttributes.ts} (61%) create mode 100644 src/ui/lib/types/utils/children.d.ts rename src/ui/lib/types/utils/{gradient.d.ts => gradient.ts} (51%) create mode 100644 src/ui/translators/translateFillGradients.ts delete mode 100644 src/ui/translators/translateFills.ts delete mode 100644 src/ui/translators/translateTextDecoration.ts delete mode 100644 src/ui/translators/translateTextTransform.ts delete mode 100644 src/ui/utils/calculateAdjustment.ts delete mode 100644 src/ui/utils/index.ts diff --git a/src/common/interfaces.ts b/src/common/interfaces.ts deleted file mode 100644 index b72d1d6..0000000 --- a/src/common/interfaces.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { PenpotFile } from '../ui/lib/penpot'; - -export type NodeData = { - id: string; - name: string; - type: string; - children: NodeData[]; - x: number; - y: number; - width: number; - height: number; - fills: readonly Paint[]; - imageFill?: string; -}; - -export type TextDataChildren = Pick< - StyledTextSegment, - | 'fills' - | 'characters' - | 'start' - | 'end' - | 'fontSize' - | 'fontName' - | 'fontWeight' - | 'textDecoration' - | 'textCase' - | 'lineHeight' - | 'letterSpacing' ->; - -export type TextData = Pick< - NodeData, - 'id' | 'name' | 'type' | 'x' | 'y' | 'width' | 'height' | 'fills' -> & { - fontName: FontName; - fontSize: string; - fontWeight: string; - characters: string; - lineHeight: LineHeight; - letterSpacing: LetterSpacing; - textCase: TextCase; - textDecoration: TextDecoration; - textAlignHorizontal: 'CENTER' | 'LEFT' | 'RIGHT' | 'JUSTIFIED'; - textAlignVertical: 'CENTER' | 'TOP' | 'BOTTOM'; - children: TextDataChildren[]; -}; - -export type ExportFile = { - penpotFile: PenpotFile; - fontNames: Set; -}; - -export interface Signatures { - [key: string]: string; -} diff --git a/src/plugin/figma/index.ts b/src/plugin/figma/index.ts deleted file mode 100644 index 0a100b8..0000000 --- a/src/plugin/figma/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './traverseNode'; diff --git a/src/plugin/figma/traverseNode.ts b/src/plugin/figma/traverseNode.ts deleted file mode 100644 index 43cbecb..0000000 --- a/src/plugin/figma/traverseNode.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { NodeData, TextData } from '../../common/interfaces'; -import { getImageFill, getNodeData, getTextData } from '../utils'; - -export async function traverse(baseNode: BaseNode): Promise { - const children: (NodeData | TextData)[] = []; - if ('children' in baseNode && baseNode.type !== 'INSTANCE') { - for (const child of baseNode.children) { - children.push(await traverse(child)); - } - } - - const nodeData = getNodeData(baseNode, children); - - if (nodeData.fills) { - nodeData.imageFill = await getImageFill(baseNode, nodeData); - } - - const textNode = getTextData(baseNode, nodeData); - if (textNode) { - return textNode; - } - - return nodeData; -} diff --git a/src/plugin/messageHandlers/handleExportMessage.ts b/src/plugin/messageHandlers/handleExportMessage.ts index aeabbf3..b00a050 100644 --- a/src/plugin/messageHandlers/handleExportMessage.ts +++ b/src/plugin/messageHandlers/handleExportMessage.ts @@ -1,8 +1,8 @@ -import { NodeData, TextData } from '../../common/interfaces'; -import { traverse } from '../figma'; +import { transformDocumentNode } from '../transformers'; export async function handleExportMessage() { - await figma.loadAllPagesAsync(); // ensures all PageNodes are loaded - const root: NodeData | TextData = await traverse(figma.root); // start the traversal at the root - figma.ui.postMessage({ type: 'FIGMAFILE', data: root }); + await figma.loadAllPagesAsync(); + + const penpotNode = await transformDocumentNode(figma.root); + figma.ui.postMessage({ type: 'FIGMAFILE', data: penpotNode }); } diff --git a/src/plugin/transformers/index.ts b/src/plugin/transformers/index.ts new file mode 100644 index 0000000..e15ef31 --- /dev/null +++ b/src/plugin/transformers/index.ts @@ -0,0 +1,9 @@ +export * from './transformDocumentNode'; +export * from './transformEllipseNode'; +export * from './transformFrameNode'; +export * from './transformGroupNode'; +export * from './transformPageNode'; +export * from './transformRectangleNode'; +export * from './transformSceneNode'; +export * from './transformImageNode'; +export * from './transformTextNode'; diff --git a/src/plugin/transformers/transformDocumentNode.ts b/src/plugin/transformers/transformDocumentNode.ts new file mode 100644 index 0000000..c268c37 --- /dev/null +++ b/src/plugin/transformers/transformDocumentNode.ts @@ -0,0 +1,9 @@ +import { PenpotDocument } from '../../ui/lib/types/penpotDocument'; +import { transformPageNode } from './transformPageNode'; + +export const transformDocumentNode = async (node: DocumentNode): Promise => { + return { + name: node.name, + children: await Promise.all(node.children.map(child => transformPageNode(child))) + }; +}; diff --git a/src/plugin/transformers/transformEllipseNode.ts b/src/plugin/transformers/transformEllipseNode.ts new file mode 100644 index 0000000..9e21006 --- /dev/null +++ b/src/plugin/transformers/transformEllipseNode.ts @@ -0,0 +1,18 @@ +import { CircleShape } from '../../ui/lib/types/circle/circleShape'; +import { translateFills } from '../translators/translateFills'; + +export const transformEllipseNode = ( + node: EllipseNode, + baseX: number, + baseY: number +): CircleShape => { + return { + type: 'circle', + 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/plugin/transformers/transformFrameNode.ts b/src/plugin/transformers/transformFrameNode.ts new file mode 100644 index 0000000..de90e23 --- /dev/null +++ b/src/plugin/transformers/transformFrameNode.ts @@ -0,0 +1,22 @@ +import { FrameShape } from '../../ui/lib/types/frame/frameShape'; +import { translateFills } from '../translators/translateFills'; +import { transformSceneNode } from './transformSceneNode'; + +export const transformFrameNode = async ( + node: FrameNode, + baseX: number, + baseY: number +): Promise => { + return { + type: 'frame', + 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), + children: await Promise.all( + node.children.map(child => transformSceneNode(child, baseX + node.x, baseY + node.y)) + ) + }; +}; diff --git a/src/plugin/transformers/transformGroupNode.ts b/src/plugin/transformers/transformGroupNode.ts new file mode 100644 index 0000000..26f2e76 --- /dev/null +++ b/src/plugin/transformers/transformGroupNode.ts @@ -0,0 +1,14 @@ +import { GroupShape } from '../../ui/lib/types/group/groupShape'; +import { transformSceneNode } from './transformSceneNode'; + +export const transformGroupNode = async ( + node: GroupNode, + baseX: number, + baseY: number +): Promise => { + return { + type: 'group', + name: node.name, + children: await Promise.all(node.children.map(child => transformSceneNode(child, baseX, baseY))) + }; +}; diff --git a/src/plugin/transformers/transformImageNode.ts b/src/plugin/transformers/transformImageNode.ts new file mode 100644 index 0000000..028689b --- /dev/null +++ b/src/plugin/transformers/transformImageNode.ts @@ -0,0 +1,29 @@ +import { ImageShape } from '../../ui/lib/types/image/imageShape'; +import { detectMimeType } from '../utils'; + +export const transformImageNode = async ( + node: SceneNode, + baseX: number, + baseY: number +): Promise => { + let dataUri = ''; + if ('exportAsync' in node) { + const value = await node.exportAsync({ format: 'PNG' }); + const b64 = figma.base64Encode(value); + dataUri = 'data:' + detectMimeType(b64) + ';base64,' + b64; + } + + return { + type: 'image', + 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: dataUri + }; +}; diff --git a/src/plugin/transformers/transformPageNode.ts b/src/plugin/transformers/transformPageNode.ts new file mode 100644 index 0000000..a32681f --- /dev/null +++ b/src/plugin/transformers/transformPageNode.ts @@ -0,0 +1,9 @@ +import { PenpotPage } from '../../ui/lib/types/penpotPage'; +import { transformSceneNode } from './transformSceneNode'; + +export const transformPageNode = async (node: PageNode): Promise => { + return { + name: node.name, + children: await Promise.all(node.children.map(child => transformSceneNode(child))) + }; +}; diff --git a/src/plugin/transformers/transformRectangleNode.ts b/src/plugin/transformers/transformRectangleNode.ts new file mode 100644 index 0000000..798baf2 --- /dev/null +++ b/src/plugin/transformers/transformRectangleNode.ts @@ -0,0 +1,18 @@ +import { RectShape } from '../../ui/lib/types/rect/rectShape'; +import { translateFills } from '../translators/translateFills'; + +export const transformRectangleNode = ( + node: RectangleNode, + baseX: number, + baseY: number +): RectShape => { + return { + type: 'rect', + 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/plugin/transformers/transformSceneNode.ts b/src/plugin/transformers/transformSceneNode.ts new file mode 100644 index 0000000..921e7fa --- /dev/null +++ b/src/plugin/transformers/transformSceneNode.ts @@ -0,0 +1,44 @@ +import { + transformEllipseNode, + transformFrameNode, + transformGroupNode, + transformImageNode, + transformRectangleNode, + transformTextNode +} from '.'; +import { PenpotNode } from '../../ui/lib/types/penpotNode'; +import { calculateAdjustment } from '../utils'; + +export const transformSceneNode = async ( + node: SceneNode, + baseX: number = 0, + baseY: number = 0 +): Promise => { + // @TODO: when penpot 2.0, manage image as fills for the basic types + if ( + 'fills' in node && + node.fills !== figma.mixed && + node.fills.find(fill => fill.type === 'IMAGE') + ) { + // 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); + return await transformImageNode(node, baseX + adjustedX, baseY + adjustedY); + } + + switch (node.type) { + case 'RECTANGLE': + return transformRectangleNode(node, baseX, baseY); + case 'ELLIPSE': + return transformEllipseNode(node, baseX, baseY); + case 'FRAME': + return await transformFrameNode(node, baseX, baseY); + case 'GROUP': + return await transformGroupNode(node, baseX, baseY); + case 'TEXT': + return transformTextNode(node, baseX, baseY); + } + + throw new Error(`Unsupported node type: ${node.type}`); +}; diff --git a/src/plugin/transformers/transformTextNode.ts b/src/plugin/transformers/transformTextNode.ts new file mode 100644 index 0000000..2d61ccf --- /dev/null +++ b/src/plugin/transformers/transformTextNode.ts @@ -0,0 +1,61 @@ +import { TextNode as PenpotTextNode } from '../../ui/lib/types/text/textContent'; +import { TextShape } from '../../ui/lib/types/text/textShape'; +import { translateFills, translateTextDecoration, translateTextTransform } from '../translators'; + +export const transformTextNode = (node: TextNode, baseX: number, baseY: number): TextShape => { + const styledTextSegments = node.getStyledTextSegments([ + 'fontName', + 'fontSize', + 'fontWeight', + 'lineHeight', + 'letterSpacing', + 'textCase', + 'textDecoration', + 'fills' + ]); + + const children: PenpotTextNode[] = styledTextSegments.map(segment => { + figma.ui.postMessage({ type: 'FONT_NAME', data: segment.fontName.family }); + + return { + text: segment.characters, + fills: translateFills(segment.fills, node.width, node.height), + fontFamily: segment.fontName.family, + fontSize: segment.fontSize.toString(), + fontStyle: segment.fontName.style, + fontWeight: segment.fontWeight.toString(), + textDecoration: translateTextDecoration(segment), + textTransform: translateTextTransform(segment) + }; + }); + + return { + type: 'text', + name: node.name, + x: node.x + baseX, + y: node.y + baseY, + width: node.width, + height: node.height, + content: { + type: 'root', + children: [ + { + type: 'paragraph-set', + children: [ + { + type: 'paragraph', + fills: translateFills(node.fills, node.width, node.height), + fontFamily: children[0].fontFamily, + fontSize: children[0].fontSize, + fontStyle: children[0].fontStyle, + fontWeight: children[0].fontWeight, + textDecoration: children[0].textDecoration, + textTransform: children[0].textTransform, + children: children + } + ] + } + ] + } + }; +}; diff --git a/src/plugin/translators/index.ts b/src/plugin/translators/index.ts new file mode 100644 index 0000000..70254a2 --- /dev/null +++ b/src/plugin/translators/index.ts @@ -0,0 +1,5 @@ +export * from './translateFills'; +// export * from './translateGradientLinearFill'; +export * from './translateSolidFill'; +export * from './translateTextDecoration'; +export * from './translateTextTransform'; diff --git a/src/plugin/translators/translateFills.ts b/src/plugin/translators/translateFills.ts new file mode 100644 index 0000000..0188b71 --- /dev/null +++ b/src/plugin/translators/translateFills.ts @@ -0,0 +1,43 @@ +import { Fill } from '../../ui/lib/types/utils/fill'; +// import { translateGradientLinearFill } from './translateGradientLinearFill'; +import { translateSolidFill } from './translateSolidFill'; + +const translateFill = ( + fill: Paint, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + width: number, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + height: number +): Fill | undefined => { + switch (fill.type) { + case 'SOLID': + return translateSolidFill(fill); + // @TODO: fix this + // case 'GRADIENT_LINEAR': + // return translateGradientLinearFill(fill, width, height); + } + + console.error('Color type ' + fill.type + ' not supported yet'); +}; + +export const translateFills = ( + fills: readonly Paint[] | typeof figma.mixed, + width: number, + height: number +): Fill[] => { + // @TODO: think better variable name + // @TODO: make it work with figma.mixed + const fills2 = fills === figma.mixed ? [] : fills; + + const penpotFills = []; + + for (const fill of fills2) { + const penpotFill = translateFill(fill, width, height); + + if (penpotFill) { + penpotFills.unshift(penpotFill); + } + } + + return penpotFills; +}; diff --git a/src/ui/translators/translateGradientLinearFill.ts b/src/plugin/translators/translateGradientLinearFill.ts similarity index 92% rename from src/ui/translators/translateGradientLinearFill.ts rename to src/plugin/translators/translateGradientLinearFill.ts index edbe6eb..adf358e 100644 --- a/src/ui/translators/translateGradientLinearFill.ts +++ b/src/plugin/translators/translateGradientLinearFill.ts @@ -1,6 +1,6 @@ import { extractLinearGradientParamsFromTransform } from '@figma-plugin/helpers'; -import { Fill } from '../lib/types/utils/fill'; +import { Fill } from '../../ui/lib/types/utils/fill'; import { rgbToHex } from '../utils'; export const translateGradientLinearFill = ( @@ -9,9 +9,10 @@ export const translateGradientLinearFill = ( height: number ): Fill => { const points = extractLinearGradientParamsFromTransform(width, height, fill.gradientTransform); + return { fillColorGradient: { - type: Symbol.for('linear'), + type: 'linear', startX: points.start[0] / width, startY: points.start[1] / height, endX: points.end[0] / width, diff --git a/src/ui/translators/translateSolidFill.ts b/src/plugin/translators/translateSolidFill.ts similarity index 80% rename from src/ui/translators/translateSolidFill.ts rename to src/plugin/translators/translateSolidFill.ts index 014a87e..57eef9f 100644 --- a/src/ui/translators/translateSolidFill.ts +++ b/src/plugin/translators/translateSolidFill.ts @@ -1,4 +1,4 @@ -import { Fill } from '../lib/types/utils/fill'; +import { Fill } from '../../ui/lib/types/utils/fill'; import { rgbToHex } from '../utils'; export const translateSolidFill = (fill: SolidPaint): Fill => { diff --git a/src/plugin/translators/translateTextDecoration.ts b/src/plugin/translators/translateTextDecoration.ts new file mode 100644 index 0000000..b6c59ad --- /dev/null +++ b/src/plugin/translators/translateTextDecoration.ts @@ -0,0 +1,10 @@ +export const translateTextDecoration = (segment: Pick) => { + switch (segment.textDecoration) { + case 'STRIKETHROUGH': + return 'line-through'; + case 'UNDERLINE': + return 'underline'; + default: + return 'none'; + } +}; diff --git a/src/plugin/translators/translateTextTransform.ts b/src/plugin/translators/translateTextTransform.ts new file mode 100644 index 0000000..5608453 --- /dev/null +++ b/src/plugin/translators/translateTextTransform.ts @@ -0,0 +1,12 @@ +export const translateTextTransform = (segment: Pick) => { + switch (segment.textCase) { + case 'UPPER': + return 'uppercase'; + case 'LOWER': + return 'lowercase'; + case 'TITLE': + return 'capitalize'; + default: + return 'none'; + } +}; diff --git a/src/plugin/utils/calculateAdjustment.ts b/src/plugin/utils/calculateAdjustment.ts new file mode 100644 index 0000000..539a9f2 --- /dev/null +++ b/src/plugin/utils/calculateAdjustment.ts @@ -0,0 +1,17 @@ +export const calculateAdjustment = (node: SceneNode) => { + // 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; + if ('children' in node) { + 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/plugin/utils/detectMimeType.ts b/src/plugin/utils/detectMimeType.ts index c3500d8..aec7289 100644 --- a/src/plugin/utils/detectMimeType.ts +++ b/src/plugin/utils/detectMimeType.ts @@ -1,4 +1,6 @@ -import { Signatures } from '../../common/interfaces'; +export interface Signatures { + [key: string]: string; +} const signatures: Signatures = { 'R0lGODdh': 'image/gif', diff --git a/src/plugin/utils/getImageFill.ts b/src/plugin/utils/getImageFill.ts deleted file mode 100644 index 871189c..0000000 --- a/src/plugin/utils/getImageFill.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { NodeData } from '../../common/interfaces'; -import { detectMimeType } from './detectMimeType'; - -export async function getImageFill(baseNode: BaseNode, node: NodeData): Promise { - // Find any fill of type image - const imageFill = node.fills.find(fill => fill.type === 'IMAGE'); - if (imageFill && 'exportAsync' in baseNode) { - // 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. - const value = await baseNode.exportAsync({ format: 'PNG' }); - const b64 = figma.base64Encode(value); - - return 'data:' + detectMimeType(b64) + ';base64,' + b64; - } - - return ''; -} diff --git a/src/plugin/utils/getNodeData.ts b/src/plugin/utils/getNodeData.ts deleted file mode 100644 index fb588f7..0000000 --- a/src/plugin/utils/getNodeData.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NodeData, TextData } from '../../common/interfaces'; - -export const getNodeData = (node: BaseNode, children: (NodeData | TextData)[]): NodeData => { - return { - id: node.id, - type: node.type, - name: node.name, - children: children, - x: 'x' in node ? node.x : 0, - 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 - imageFill: '' - } as NodeData; -}; diff --git a/src/plugin/utils/getTextData.ts b/src/plugin/utils/getTextData.ts deleted file mode 100644 index 291f987..0000000 --- a/src/plugin/utils/getTextData.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { NodeData, TextData } from '../../common/interfaces'; - -export const getTextData = (baseNode: BaseNode, nodeData: NodeData): TextData | undefined => { - if (baseNode.type === 'TEXT') { - const styledTextSegments = baseNode.getStyledTextSegments([ - 'fontName', - 'fontSize', - 'fontWeight', - 'lineHeight', - 'letterSpacing', - 'textCase', - 'textDecoration', - 'fills' - ]); - - if (styledTextSegments[0]) { - return { - ...nodeData, - fontName: styledTextSegments[0].fontName, - fontSize: styledTextSegments[0].fontSize.toString(), - fontWeight: styledTextSegments[0].fontWeight.toString(), - characters: baseNode.characters, - lineHeight: styledTextSegments[0].lineHeight, - letterSpacing: styledTextSegments[0].letterSpacing, - fills: styledTextSegments[0].fills, - textCase: styledTextSegments[0].textCase, - textDecoration: styledTextSegments[0].textDecoration, - textAlignHorizontal: baseNode.textAlignHorizontal, - textAlignVertical: baseNode.textAlignVertical, - children: styledTextSegments - } as TextData; - } - } -}; diff --git a/src/plugin/utils/index.ts b/src/plugin/utils/index.ts index 29de70c..8715359 100644 --- a/src/plugin/utils/index.ts +++ b/src/plugin/utils/index.ts @@ -1,4 +1,3 @@ export * from './detectMimeType'; -export * from './getImageFill'; -export * from './getNodeData'; -export * from './getTextData'; +export * from './rgbToHex'; +export * from './calculateAdjustment'; diff --git a/src/ui/utils/rgbToHex.ts b/src/plugin/utils/rgbToHex.ts similarity index 100% rename from src/ui/utils/rgbToHex.ts rename to src/plugin/utils/rgbToHex.ts diff --git a/src/ui/PenpotExporter.tsx b/src/ui/PenpotExporter.tsx index 81831bf..13446e1 100644 --- a/src/ui/PenpotExporter.tsx +++ b/src/ui/PenpotExporter.tsx @@ -1,8 +1,8 @@ import { useEffect, useState } from 'react'; import slugify from 'slugify'; -import { NodeData } from '../common/interfaces'; import { createPenpotFile } from './converters'; +import { PenpotDocument } from './lib/types/penpotDocument'; import { validateFont } from './validators'; export const PenpotExporter = () => { @@ -13,19 +13,20 @@ export const PenpotExporter = () => { setMissingFonts(missingFonts => missingFonts.add(font)); }; - const onMessage = (event: MessageEvent<{ pluginMessage: { type: string; data: NodeData } }>) => { + const onMessage = (event: MessageEvent<{ pluginMessage: { type: string; data: unknown } }>) => { if (event.data.pluginMessage.type == 'FIGMAFILE') { - const file = createPenpotFile(event.data.pluginMessage.data); + const document = event.data.pluginMessage.data as PenpotDocument; + const file = createPenpotFile(document); - file.fontNames.forEach(font => { - if (!validateFont(font)) { - addFontWarning(slugify(font.family.toLowerCase())); - } - }); - - file.penpotFile.export(); + file.export(); setExporting(false); + } else if (event.data.pluginMessage.type == 'FONT_NAME') { + const fontName = event.data.pluginMessage.data as string; + + if (!validateFont(fontName)) { + addFontWarning(slugify(fontName.toLowerCase())); + } } }; diff --git a/src/ui/converters/createGradientFill.ts b/src/ui/converters/createGradientFill.ts new file mode 100644 index 0000000..fca0b1a --- /dev/null +++ b/src/ui/converters/createGradientFill.ts @@ -0,0 +1,18 @@ +import { Gradient, LINEAR_TYPE, RADIAL_TYPE } from '../lib/types/utils/gradient'; + +export const createGradientFill = ({ type, ...rest }: Gradient): Gradient => { + switch (type) { + case 'linear': + return { + type: LINEAR_TYPE, + ...rest + }; + case 'radial': + return { + type: RADIAL_TYPE, + ...rest + }; + } + + throw new Error(`Unsupported gradient type: ${String(type)}`); +}; diff --git a/src/ui/converters/createPenpotArtboard.ts b/src/ui/converters/createPenpotArtboard.ts new file mode 100644 index 0000000..79f4486 --- /dev/null +++ b/src/ui/converters/createPenpotArtboard.ts @@ -0,0 +1,21 @@ +import { PenpotFile } from '../lib/penpot'; +import { FRAME_TYPE } from '../lib/types/frame/frameAttributes'; +import { FrameShape } from '../lib/types/frame/frameShape'; +import { createPenpotItem } from './createPenpotItem'; + +export const createPenpotArtboard = ( + file: PenpotFile, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + { type, children = [], ...rest }: FrameShape +) => { + file.addArtboard({ + type: FRAME_TYPE, + ...rest + }); + + for (const child of children) { + createPenpotItem(file, child); + } + + file.closeArtboard(); +}; diff --git a/src/ui/converters/createPenpotBoard.ts b/src/ui/converters/createPenpotBoard.ts deleted file mode 100644 index e3399db..0000000 --- a/src/ui/converters/createPenpotBoard.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createPenpotItem } from '.'; -import { ExportFile, NodeData } from '../../common/interfaces'; -import { translateFills } from '../translators'; - -export const createPenpotBoard = ( - file: ExportFile, - node: NodeData, - baseX: number, - baseY: number -) => { - file.penpotFile.addArtboard({ - type: Symbol.for('frame'), - 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.penpotFile.closeArtboard(); -}; diff --git a/src/ui/converters/createPenpotCircle.ts b/src/ui/converters/createPenpotCircle.ts index f10db6b..468a736 100644 --- a/src/ui/converters/createPenpotCircle.ts +++ b/src/ui/converters/createPenpotCircle.ts @@ -1,19 +1,13 @@ -import { ExportFile, NodeData } from '../../common/interfaces'; -import { translateFills } from '../translators'; +import { PenpotFile } from '../lib/penpot'; +import { CIRCLE_TYPE } from '../lib/types/circle/circleAttributes'; +import { CircleShape } from '../lib/types/circle/circleShape'; +import { translateFillGradients } from '../translators'; -export const createPenpotCircle = ( - file: ExportFile, - node: NodeData, - baseX: number, - baseY: number -) => { - file.penpotFile.createCircle({ - type: Symbol.for('circle'), - 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) +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const createPenpotCircle = (file: PenpotFile, { type, fills, ...rest }: CircleShape) => { + file.createCircle({ + type: CIRCLE_TYPE, + fills: translateFillGradients(fills), + ...rest }); }; diff --git a/src/ui/converters/createPenpotFile.ts b/src/ui/converters/createPenpotFile.ts index 058f2f0..28b2e31 100644 --- a/src/ui/converters/createPenpotFile.ts +++ b/src/ui/converters/createPenpotFile.ts @@ -1,11 +1,13 @@ -import { createPenpotItem } from '.'; -import { ExportFile, NodeData } from '../../common/interfaces'; +import { createPenpotPage } from '.'; import { createFile } from '../lib/penpot'; +import { PenpotDocument } from '../lib/types/penpotDocument'; -export const createPenpotFile = (node: NodeData) => { - const exportFile = { penpotFile: createFile(node.name), fontNames: new Set() }; - for (const page of node.children) { - createPenpotItem(exportFile as ExportFile, page, 0, 0); +export const createPenpotFile = (node: PenpotDocument) => { + const file = createFile(node.name); + + for (const page of node.children ?? []) { + createPenpotPage(file, page); } - return exportFile; + + return file; }; diff --git a/src/ui/converters/createPenpotGroup.ts b/src/ui/converters/createPenpotGroup.ts index 4d3fffb..764dc63 100644 --- a/src/ui/converters/createPenpotGroup.ts +++ b/src/ui/converters/createPenpotGroup.ts @@ -1,20 +1,21 @@ -import { createPenpotItem } from '.'; -import { ExportFile, NodeData } from '../../common/interfaces'; +import { PenpotFile } from '../lib/penpot'; +import { GROUP_TYPE } from '../lib/types/group/groupAttributes'; +import { GroupShape } from '../lib/types/group/groupShape'; +import { createPenpotItem } from './createPenpotItem'; export const createPenpotGroup = ( - file: ExportFile, - node: NodeData, - baseX: number, - baseY: number + file: PenpotFile, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + { type, children = [], ...rest }: GroupShape ) => { - file.penpotFile.addGroup({ - type: Symbol.for('group'), - name: node.name + file.addGroup({ + type: GROUP_TYPE, + ...rest }); - for (const child of node.children) { - createPenpotItem(file, child, baseX, baseY); + for (const child of children) { + createPenpotItem(file, child); } - file.penpotFile.closeGroup(); + file.closeGroup(); }; diff --git a/src/ui/converters/createPenpotImage.ts b/src/ui/converters/createPenpotImage.ts index 34351b8..0218da7 100644 --- a/src/ui/converters/createPenpotImage.ts +++ b/src/ui/converters/createPenpotImage.ts @@ -1,22 +1,11 @@ -import { ExportFile, NodeData } from '../../common/interfaces'; +import { PenpotFile } from '../lib/penpot'; +import { IMAGE_TYPE } from '../lib/types/image/imageAttributes'; +import { ImageShape } from '../lib/types/image/imageShape'; -export const createPenpotImage = ( - file: ExportFile, - node: NodeData, - baseX: number, - baseY: number -) => { - file.penpotFile.createImage({ - type: Symbol.for('image'), - 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 +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const createPenpotImage = (file: PenpotFile, { type, ...rest }: ImageShape) => { + file.createImage({ + type: IMAGE_TYPE, + ...rest }); }; diff --git a/src/ui/converters/createPenpotItem.ts b/src/ui/converters/createPenpotItem.ts index cabcd91..3c38dc2 100644 --- a/src/ui/converters/createPenpotItem.ts +++ b/src/ui/converters/createPenpotItem.ts @@ -1,45 +1,27 @@ import { - createPenpotBoard, + createPenpotArtboard, createPenpotCircle, createPenpotGroup, createPenpotImage, - createPenpotPage, createPenpotRectangle, createPenpotText } from '.'; -import { ExportFile, NodeData, TextData } from '../../common/interfaces'; -import { calculateAdjustment } from '../utils'; +import { PenpotFile } from '../lib/penpot'; +import { PenpotNode } from '../lib/types/penpotNode'; -export const createPenpotItem = ( - file: ExportFile, - 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); +export const createPenpotItem = (file: PenpotFile, node: PenpotNode) => { + switch (node.type) { + case 'rect': + return createPenpotRectangle(file, node); + case 'circle': + return createPenpotCircle(file, node); + case 'frame': + return createPenpotArtboard(file, node); + case 'group': + return createPenpotGroup(file, node); + case 'image': + return createPenpotImage(file, node); + case 'text': + return createPenpotText(file, node); } }; diff --git a/src/ui/converters/createPenpotPage.ts b/src/ui/converters/createPenpotPage.ts index a9dc8e7..f3c16fa 100644 --- a/src/ui/converters/createPenpotPage.ts +++ b/src/ui/converters/createPenpotPage.ts @@ -1,10 +1,13 @@ import { createPenpotItem } from '.'; -import { ExportFile, NodeData } from '../../common/interfaces'; +import { PenpotFile } from '../lib/penpot'; +import { PenpotPage } from '../lib/types/penpotPage'; -export const createPenpotPage = (file: ExportFile, node: NodeData) => { - file.penpotFile.addPage(node.name); - for (const child of node.children) { - createPenpotItem(file, child, 0, 0); +export const createPenpotPage = (file: PenpotFile, node: PenpotPage) => { + file.addPage(node.name); + + for (const child of node.children ?? []) { + createPenpotItem(file, child); } - file.penpotFile.closePage(); + + file.closePage(); }; diff --git a/src/ui/converters/createPenpotRectangle.ts b/src/ui/converters/createPenpotRectangle.ts index 0510104..d79a13b 100644 --- a/src/ui/converters/createPenpotRectangle.ts +++ b/src/ui/converters/createPenpotRectangle.ts @@ -1,19 +1,13 @@ -import { ExportFile, NodeData } from '../../common/interfaces'; -import { translateFills } from '../translators'; +import { PenpotFile } from '../lib/penpot'; +import { RECT_TYPE } from '../lib/types/rect/rectAttributes'; +import { RectShape } from '../lib/types/rect/rectShape'; +import { translateFillGradients } from '../translators'; -export const createPenpotRectangle = ( - file: ExportFile, - node: NodeData, - baseX: number, - baseY: number -) => { - file.penpotFile.createRect({ - type: Symbol.for('rect'), - 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) +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const createPenpotRectangle = (file: PenpotFile, { type, fills, ...rest }: RectShape) => { + file.createRect({ + type: RECT_TYPE, + fills: translateFillGradients(fills), + ...rest }); }; diff --git a/src/ui/converters/createPenpotText.ts b/src/ui/converters/createPenpotText.ts index cd8983c..3d58357 100644 --- a/src/ui/converters/createPenpotText.ts +++ b/src/ui/converters/createPenpotText.ts @@ -1,73 +1,14 @@ -// import slugify from 'slugify'; -import { ExportFile, TextData } from '../../common/interfaces'; -import { TextNode } from '../lib/types/text/textContent'; -import { - translateFills, // translateFontStyle, - // translateHorizontalAlign, - translateTextDecoration, - translateTextTransform // translateVerticalAlign -} from '../translators'; +import { PenpotFile } from '../lib/penpot'; +import { TEXT_TYPE } from '../lib/types/text/textAttributes'; +import { TextShape } from '../lib/types/text/textShape'; export const createPenpotText = ( - file: ExportFile, - node: TextData, - baseX: number, - baseY: number + file: PenpotFile, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + { type, ...rest }: TextShape ) => { - const children = node.children.map(val => { - file.fontNames.add(val.fontName); - - return { - text: val.characters, - fills: translateFills(val.fills, node.width, node.height), - fontFamily: val.fontName.family, - fontSize: val.fontSize.toString(), - fontStyle: val.fontName.style, - fontWeight: val.fontWeight.toString(), - textDecoration: translateTextDecoration(val), - textTransform: translateTextTransform(val) - // lineHeight: val.lineHeight, - // textAlign: translateHorizontalAlign(node.textAlignHorizontal), - // fontId: 'gfont-' + slugify(val.fontName.family.toLowerCase()), - // fontVariantId: translateFontStyle(val.fontName.style), - // letterSpacing: val.letterSpacing, - } as TextNode; - }); - file.fontNames.add(node.fontName); - - file.penpotFile.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: [ - { - type: 'paragraph', - fills: translateFills(node.fills, node.width, node.height), - fontFamily: node.fontName.family, - fontSize: node.fontSize.toString(), - fontStyle: node.fontName.style, - fontWeight: node.fontWeight.toString(), - textDecoration: translateTextDecoration(node), - textTransform: translateTextTransform(node), - children: children - // lineHeight: node.lineHeight, - // textAlign: translateHorizontalAlign(node.textAlignHorizontal), - // fontId: 'gfont-' + slugify(node.fontName.family.toLowerCase()), - // letterSpacing: node.letterSpacing, - } - ] - } - ] - } + file.createText({ + type: TEXT_TYPE, + ...rest }); }; diff --git a/src/ui/converters/index.ts b/src/ui/converters/index.ts index 6512bf5..93de897 100644 --- a/src/ui/converters/index.ts +++ b/src/ui/converters/index.ts @@ -1,4 +1,4 @@ -export * from './createPenpotBoard'; +export * from './createPenpotArtboard'; export * from './createPenpotCircle'; export * from './createPenpotFile'; export * from './createPenpotGroup'; diff --git a/src/ui/lib/types/circle/circleAttributes.d.ts b/src/ui/lib/types/circle/circleAttributes.d.ts deleted file mode 100644 index 7630fba..0000000 --- a/src/ui/lib/types/circle/circleAttributes.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Uuid } from '../utils/uuid'; - -export type CircleAttributes = { - id?: Uuid; - type: symbol; -}; diff --git a/src/ui/lib/types/circle/circleAttributes.ts b/src/ui/lib/types/circle/circleAttributes.ts new file mode 100644 index 0000000..ceaf842 --- /dev/null +++ b/src/ui/lib/types/circle/circleAttributes.ts @@ -0,0 +1,8 @@ +import { Uuid } from '../utils/uuid'; + +export const CIRCLE_TYPE: unique symbol = Symbol.for('circle'); + +export type CircleAttributes = { + type: 'circle' | typeof CIRCLE_TYPE; + id?: Uuid; +}; diff --git a/src/ui/lib/types/frame/frameAttributes.d.ts b/src/ui/lib/types/frame/frameAttributes.ts similarity index 68% rename from src/ui/lib/types/frame/frameAttributes.d.ts rename to src/ui/lib/types/frame/frameAttributes.ts index f847b29..089b98d 100644 --- a/src/ui/lib/types/frame/frameAttributes.d.ts +++ b/src/ui/lib/types/frame/frameAttributes.ts @@ -1,8 +1,10 @@ import { Uuid } from '../utils/uuid'; +export const FRAME_TYPE: unique symbol = Symbol.for('frame'); + export type FrameAttributes = { + type: 'frame' | typeof FRAME_TYPE; id?: Uuid; - type: symbol; shapes?: Uuid[]; fileThumbnail?: boolean; hideFillOnExport?: boolean; diff --git a/src/ui/lib/types/frame/frameShape.d.ts b/src/ui/lib/types/frame/frameShape.d.ts index 96e39b7..b73c79e 100644 --- a/src/ui/lib/types/frame/frameShape.d.ts +++ b/src/ui/lib/types/frame/frameShape.d.ts @@ -1,4 +1,5 @@ import { Shape } from '../shape'; +import { Children } from '../utils/children'; import { FrameAttributes } from './frameAttributes'; -export type FrameShape = Shape & FrameAttributes; +export type FrameShape = Shape & FrameAttributes & Children; diff --git a/src/ui/lib/types/group/groupAttributes.d.ts b/src/ui/lib/types/group/groupAttributes.ts similarity index 51% rename from src/ui/lib/types/group/groupAttributes.d.ts rename to src/ui/lib/types/group/groupAttributes.ts index 9d7814c..6982a9f 100644 --- a/src/ui/lib/types/group/groupAttributes.d.ts +++ b/src/ui/lib/types/group/groupAttributes.ts @@ -1,7 +1,9 @@ import { Uuid } from '../utils/uuid'; +export const GROUP_TYPE: unique symbol = Symbol.for('group'); + export type GroupAttributes = { + type: 'group' | typeof GROUP_TYPE; id?: Uuid; - type: symbol; shapes?: Uuid[]; }; diff --git a/src/ui/lib/types/group/groupShape.d.ts b/src/ui/lib/types/group/groupShape.d.ts index b8b7181..8057e31 100644 --- a/src/ui/lib/types/group/groupShape.d.ts +++ b/src/ui/lib/types/group/groupShape.d.ts @@ -1,4 +1,5 @@ import { Shape } from '../shape'; +import { Children } from '../utils/children'; import { GroupAttributes } from './groupAttributes'; -export type GroupShape = Shape & GroupAttributes; +export type GroupShape = Shape & GroupAttributes & Children; diff --git a/src/ui/lib/types/image/imageAttributes.d.ts b/src/ui/lib/types/image/imageAttributes.ts similarity index 66% rename from src/ui/lib/types/image/imageAttributes.d.ts rename to src/ui/lib/types/image/imageAttributes.ts index 31dd001..9d62fda 100644 --- a/src/ui/lib/types/image/imageAttributes.d.ts +++ b/src/ui/lib/types/image/imageAttributes.ts @@ -1,9 +1,10 @@ import { Uuid } from '../utils/uuid'; +export const IMAGE_TYPE: unique symbol = Symbol.for('image'); + export type ImageAttributes = { id?: Uuid; - type: symbol; - // TODO: Investigate where it comes from + type: 'image' | typeof IMAGE_TYPE; dataUri?: string; metadata: { width: number; diff --git a/src/ui/lib/types/penpotDocument.d.ts b/src/ui/lib/types/penpotDocument.d.ts new file mode 100644 index 0000000..bb53908 --- /dev/null +++ b/src/ui/lib/types/penpotDocument.d.ts @@ -0,0 +1,6 @@ +import { PenpotPage } from './penpotPage'; + +export type PenpotDocument = { + name: string; + children?: PenpotPage[]; +}; diff --git a/src/ui/lib/types/penpotNode.d.ts b/src/ui/lib/types/penpotNode.d.ts new file mode 100644 index 0000000..f0e6d59 --- /dev/null +++ b/src/ui/lib/types/penpotNode.d.ts @@ -0,0 +1,8 @@ +import { CircleShape } from './circle/circleShape'; +import { FrameShape } from './frame/frameShape'; +import { GroupShape } from './group/groupShape'; +import { ImageShape } from './image/imageShape'; +import { RectShape } from './rect/rectShape'; +import { TextShape } from './text/textShape'; + +export type PenpotNode = FrameShape | GroupShape | RectShape | CircleShape | TextShape | ImageShape; diff --git a/src/ui/lib/types/penpotPage.d.ts b/src/ui/lib/types/penpotPage.d.ts new file mode 100644 index 0000000..c18d9d9 --- /dev/null +++ b/src/ui/lib/types/penpotPage.d.ts @@ -0,0 +1,6 @@ +import { PenpotNode } from './penpotNode'; + +export type PenpotPage = { + name: string; + children?: PenpotNode[]; +}; diff --git a/src/ui/lib/types/rect/rectAttributes.d.ts b/src/ui/lib/types/rect/rectAttributes.d.ts deleted file mode 100644 index e487c06..0000000 --- a/src/ui/lib/types/rect/rectAttributes.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Uuid } from '../utils/uuid'; - -export type RectAttributes = { - id?: Uuid; - type: symbol; -}; diff --git a/src/ui/lib/types/rect/rectAttributes.ts b/src/ui/lib/types/rect/rectAttributes.ts new file mode 100644 index 0000000..3c651b1 --- /dev/null +++ b/src/ui/lib/types/rect/rectAttributes.ts @@ -0,0 +1,8 @@ +import { Uuid } from '../utils/uuid'; + +export const RECT_TYPE: unique symbol = Symbol.for('rect'); + +export type RectAttributes = { + type: 'rect' | typeof RECT_TYPE; + id?: Uuid; +}; diff --git a/src/ui/lib/types/text/textAttributes.d.ts b/src/ui/lib/types/text/textAttributes.ts similarity index 61% rename from src/ui/lib/types/text/textAttributes.d.ts rename to src/ui/lib/types/text/textAttributes.ts index b6d8b3c..22301d6 100644 --- a/src/ui/lib/types/text/textAttributes.d.ts +++ b/src/ui/lib/types/text/textAttributes.ts @@ -1,8 +1,10 @@ import { Uuid } from '../utils/uuid'; import { TextContent } from './textContent'; +export const TEXT_TYPE: unique symbol = Symbol.for('text'); + export type TextAttributes = { id?: Uuid; - type: symbol; + type: 'text' | typeof TEXT_TYPE; content?: TextContent; }; diff --git a/src/ui/lib/types/utils/children.d.ts b/src/ui/lib/types/utils/children.d.ts new file mode 100644 index 0000000..2dc2e83 --- /dev/null +++ b/src/ui/lib/types/utils/children.d.ts @@ -0,0 +1,3 @@ +import { PenpotNode } from '../penpotNode'; + +export type Children = { children?: PenpotNode[] }; diff --git a/src/ui/lib/types/utils/gradient.d.ts b/src/ui/lib/types/utils/gradient.ts similarity index 51% rename from src/ui/lib/types/utils/gradient.d.ts rename to src/ui/lib/types/utils/gradient.ts index b561ae1..4bd6217 100644 --- a/src/ui/lib/types/utils/gradient.d.ts +++ b/src/ui/lib/types/utils/gradient.ts @@ -1,5 +1,8 @@ +export const LINEAR_TYPE: unique symbol = Symbol.for('linear'); +export const RADIAL_TYPE: unique symbol = Symbol.for('radial'); + export type Gradient = { - type: symbol; // linear or radial + type: 'linear' | 'radial' | typeof LINEAR_TYPE | typeof RADIAL_TYPE; // symbol startX: number; startY: number; endX: number; diff --git a/src/ui/translators/index.ts b/src/ui/translators/index.ts index 3d86e99..c570ed8 100644 --- a/src/ui/translators/index.ts +++ b/src/ui/translators/index.ts @@ -1,8 +1,4 @@ -export * from './translateFills'; export * from './translateFontStyle'; -export * from './translateGradientLinearFill'; export * from './translateHorizontalAlign'; -export * from './translateSolidFill'; -export * from './translateTextDecoration'; -export * from './translateTextTransform'; export * from './translateVerticalAlign'; +export * from './translateFillGradients'; diff --git a/src/ui/translators/translateFillGradients.ts b/src/ui/translators/translateFillGradients.ts new file mode 100644 index 0000000..014d083 --- /dev/null +++ b/src/ui/translators/translateFillGradients.ts @@ -0,0 +1,13 @@ +import { createGradientFill } from '../converters/createGradientFill'; +import { Fill } from '../lib/types/utils/fill'; + +export const translateFillGradients = (fills?: Fill[]): Fill[] | undefined => { + if (!fills) return fills; + return fills.map(fill => { + if (fill.fillColorGradient) { + fill.fillColorGradient = createGradientFill(fill.fillColorGradient); + } + + return fill; + }); +}; diff --git a/src/ui/translators/translateFills.ts b/src/ui/translators/translateFills.ts deleted file mode 100644 index a10848d..0000000 --- a/src/ui/translators/translateFills.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { translateGradientLinearFill, translateSolidFill } from '.'; -import { Fill } from '../lib/types/utils/fill'; - -const translateFill = (fill: Paint, width: number, height: number): Fill | null => { - 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): Fill[] => { - 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/ui/translators/translateTextDecoration.ts b/src/ui/translators/translateTextDecoration.ts deleted file mode 100644 index 97129f5..0000000 --- a/src/ui/translators/translateTextDecoration.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { TextData, TextDataChildren } from '../../common/interfaces'; - -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/ui/translators/translateTextTransform.ts b/src/ui/translators/translateTextTransform.ts deleted file mode 100644 index 5967e3f..0000000 --- a/src/ui/translators/translateTextTransform.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { TextData, TextDataChildren } from '../../common/interfaces'; - -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/ui/utils/calculateAdjustment.ts b/src/ui/utils/calculateAdjustment.ts deleted file mode 100644 index 4f723f9..0000000 --- a/src/ui/utils/calculateAdjustment.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { NodeData } from '../../common/interfaces'; - -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/ui/utils/index.ts b/src/ui/utils/index.ts deleted file mode 100644 index 1cfdeff..0000000 --- a/src/ui/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './calculateAdjustment'; -export * from './rgbToHex'; diff --git a/src/ui/validators/validateFont.ts b/src/ui/validators/validateFont.ts index f2d7fe3..508b104 100644 --- a/src/ui/validators/validateFont.ts +++ b/src/ui/validators/validateFont.ts @@ -4,7 +4,7 @@ import fonts from '../gfonts.json'; const gfonts = new Set(fonts); -export const validateFont = (fontName: FontName): boolean => { - const name = slugify(fontName.family.toLowerCase()); +export const validateFont = (fontFamily: string): boolean => { + const name = slugify(fontFamily.toLowerCase()); return gfonts.has(name); };