mirror of
https://github.com/penpot/penpot-exporter-figma-plugin.git
synced 2024-12-22 05:33:02 -05:00
Move logic from UI to Code (#15)
* 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 <sion333@gmail.com>
This commit is contained in:
parent
41c42ce9a6
commit
6c67200dc6
63 changed files with 545 additions and 473 deletions
|
@ -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<FontName>;
|
||||
};
|
||||
|
||||
export interface Signatures {
|
||||
[key: string]: string;
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from './traverseNode';
|
|
@ -1,24 +0,0 @@
|
|||
import { NodeData, TextData } from '../../common/interfaces';
|
||||
import { getImageFill, getNodeData, getTextData } from '../utils';
|
||||
|
||||
export async function traverse(baseNode: BaseNode): Promise<NodeData | TextData> {
|
||||
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;
|
||||
}
|
|
@ -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 });
|
||||
}
|
||||
|
|
9
src/plugin/transformers/index.ts
Normal file
9
src/plugin/transformers/index.ts
Normal file
|
@ -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';
|
9
src/plugin/transformers/transformDocumentNode.ts
Normal file
9
src/plugin/transformers/transformDocumentNode.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { PenpotDocument } from '../../ui/lib/types/penpotDocument';
|
||||
import { transformPageNode } from './transformPageNode';
|
||||
|
||||
export const transformDocumentNode = async (node: DocumentNode): Promise<PenpotDocument> => {
|
||||
return {
|
||||
name: node.name,
|
||||
children: await Promise.all(node.children.map(child => transformPageNode(child)))
|
||||
};
|
||||
};
|
18
src/plugin/transformers/transformEllipseNode.ts
Normal file
18
src/plugin/transformers/transformEllipseNode.ts
Normal file
|
@ -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)
|
||||
};
|
||||
};
|
22
src/plugin/transformers/transformFrameNode.ts
Normal file
22
src/plugin/transformers/transformFrameNode.ts
Normal file
|
@ -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<FrameShape> => {
|
||||
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))
|
||||
)
|
||||
};
|
||||
};
|
14
src/plugin/transformers/transformGroupNode.ts
Normal file
14
src/plugin/transformers/transformGroupNode.ts
Normal file
|
@ -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<GroupShape> => {
|
||||
return {
|
||||
type: 'group',
|
||||
name: node.name,
|
||||
children: await Promise.all(node.children.map(child => transformSceneNode(child, baseX, baseY)))
|
||||
};
|
||||
};
|
29
src/plugin/transformers/transformImageNode.ts
Normal file
29
src/plugin/transformers/transformImageNode.ts
Normal file
|
@ -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<ImageShape> => {
|
||||
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
|
||||
};
|
||||
};
|
9
src/plugin/transformers/transformPageNode.ts
Normal file
9
src/plugin/transformers/transformPageNode.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { PenpotPage } from '../../ui/lib/types/penpotPage';
|
||||
import { transformSceneNode } from './transformSceneNode';
|
||||
|
||||
export const transformPageNode = async (node: PageNode): Promise<PenpotPage> => {
|
||||
return {
|
||||
name: node.name,
|
||||
children: await Promise.all(node.children.map(child => transformSceneNode(child)))
|
||||
};
|
||||
};
|
18
src/plugin/transformers/transformRectangleNode.ts
Normal file
18
src/plugin/transformers/transformRectangleNode.ts
Normal file
|
@ -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)
|
||||
};
|
||||
};
|
44
src/plugin/transformers/transformSceneNode.ts
Normal file
44
src/plugin/transformers/transformSceneNode.ts
Normal file
|
@ -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<PenpotNode> => {
|
||||
// @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}`);
|
||||
};
|
61
src/plugin/transformers/transformTextNode.ts
Normal file
61
src/plugin/transformers/transformTextNode.ts
Normal file
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
};
|
5
src/plugin/translators/index.ts
Normal file
5
src/plugin/translators/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export * from './translateFills';
|
||||
// export * from './translateGradientLinearFill';
|
||||
export * from './translateSolidFill';
|
||||
export * from './translateTextDecoration';
|
||||
export * from './translateTextTransform';
|
43
src/plugin/translators/translateFills.ts
Normal file
43
src/plugin/translators/translateFills.ts
Normal file
|
@ -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;
|
||||
};
|
|
@ -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,
|
|
@ -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 => {
|
10
src/plugin/translators/translateTextDecoration.ts
Normal file
10
src/plugin/translators/translateTextDecoration.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export const translateTextDecoration = (segment: Pick<StyledTextSegment, 'textDecoration'>) => {
|
||||
switch (segment.textDecoration) {
|
||||
case 'STRIKETHROUGH':
|
||||
return 'line-through';
|
||||
case 'UNDERLINE':
|
||||
return 'underline';
|
||||
default:
|
||||
return 'none';
|
||||
}
|
||||
};
|
12
src/plugin/translators/translateTextTransform.ts
Normal file
12
src/plugin/translators/translateTextTransform.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
export const translateTextTransform = (segment: Pick<StyledTextSegment, 'textCase'>) => {
|
||||
switch (segment.textCase) {
|
||||
case 'UPPER':
|
||||
return 'uppercase';
|
||||
case 'LOWER':
|
||||
return 'lowercase';
|
||||
case 'TITLE':
|
||||
return 'capitalize';
|
||||
default:
|
||||
return 'none';
|
||||
}
|
||||
};
|
17
src/plugin/utils/calculateAdjustment.ts
Normal file
17
src/plugin/utils/calculateAdjustment.ts
Normal file
|
@ -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];
|
||||
};
|
|
@ -1,4 +1,6 @@
|
|||
import { Signatures } from '../../common/interfaces';
|
||||
export interface Signatures {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
const signatures: Signatures = {
|
||||
'R0lGODdh': 'image/gif',
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
import { NodeData } from '../../common/interfaces';
|
||||
import { detectMimeType } from './detectMimeType';
|
||||
|
||||
export async function getImageFill(baseNode: BaseNode, node: NodeData): Promise<string> {
|
||||
// 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 '';
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,4 +1,3 @@
|
|||
export * from './detectMimeType';
|
||||
export * from './getImageFill';
|
||||
export * from './getNodeData';
|
||||
export * from './getTextData';
|
||||
export * from './rgbToHex';
|
||||
export * from './calculateAdjustment';
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
18
src/ui/converters/createGradientFill.ts
Normal file
18
src/ui/converters/createGradientFill.ts
Normal file
|
@ -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)}`);
|
||||
};
|
21
src/ui/converters/createPenpotArtboard.ts
Normal file
21
src/ui/converters/createPenpotArtboard.ts
Normal file
|
@ -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();
|
||||
};
|
|
@ -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();
|
||||
};
|
|
@ -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
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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<FontName>() };
|
||||
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;
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export * from './createPenpotBoard';
|
||||
export * from './createPenpotArtboard';
|
||||
export * from './createPenpotCircle';
|
||||
export * from './createPenpotFile';
|
||||
export * from './createPenpotGroup';
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
import { Uuid } from '../utils/uuid';
|
||||
|
||||
export type CircleAttributes = {
|
||||
id?: Uuid;
|
||||
type: symbol;
|
||||
};
|
8
src/ui/lib/types/circle/circleAttributes.ts
Normal file
8
src/ui/lib/types/circle/circleAttributes.ts
Normal file
|
@ -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;
|
||||
};
|
|
@ -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;
|
3
src/ui/lib/types/frame/frameShape.d.ts
vendored
3
src/ui/lib/types/frame/frameShape.d.ts
vendored
|
@ -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;
|
||||
|
|
|
@ -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[];
|
||||
};
|
3
src/ui/lib/types/group/groupShape.d.ts
vendored
3
src/ui/lib/types/group/groupShape.d.ts
vendored
|
@ -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;
|
||||
|
|
|
@ -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;
|
6
src/ui/lib/types/penpotDocument.d.ts
vendored
Normal file
6
src/ui/lib/types/penpotDocument.d.ts
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { PenpotPage } from './penpotPage';
|
||||
|
||||
export type PenpotDocument = {
|
||||
name: string;
|
||||
children?: PenpotPage[];
|
||||
};
|
8
src/ui/lib/types/penpotNode.d.ts
vendored
Normal file
8
src/ui/lib/types/penpotNode.d.ts
vendored
Normal file
|
@ -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;
|
6
src/ui/lib/types/penpotPage.d.ts
vendored
Normal file
6
src/ui/lib/types/penpotPage.d.ts
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { PenpotNode } from './penpotNode';
|
||||
|
||||
export type PenpotPage = {
|
||||
name: string;
|
||||
children?: PenpotNode[];
|
||||
};
|
6
src/ui/lib/types/rect/rectAttributes.d.ts
vendored
6
src/ui/lib/types/rect/rectAttributes.d.ts
vendored
|
@ -1,6 +0,0 @@
|
|||
import { Uuid } from '../utils/uuid';
|
||||
|
||||
export type RectAttributes = {
|
||||
id?: Uuid;
|
||||
type: symbol;
|
||||
};
|
8
src/ui/lib/types/rect/rectAttributes.ts
Normal file
8
src/ui/lib/types/rect/rectAttributes.ts
Normal file
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
};
|
3
src/ui/lib/types/utils/children.d.ts
vendored
Normal file
3
src/ui/lib/types/utils/children.d.ts
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { PenpotNode } from '../penpotNode';
|
||||
|
||||
export type Children = { children?: PenpotNode[] };
|
|
@ -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;
|
|
@ -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';
|
||||
|
|
13
src/ui/translators/translateFillGradients.ts
Normal file
13
src/ui/translators/translateFillGradients.ts
Normal file
|
@ -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;
|
||||
});
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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';
|
||||
};
|
|
@ -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';
|
||||
};
|
|
@ -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];
|
||||
};
|
|
@ -1,2 +0,0 @@
|
|||
export * from './calculateAdjustment';
|
||||
export * from './rgbToHex';
|
|
@ -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);
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue