mirror of
https://github.com/penpot/penpot-exporter-figma-plugin.git
synced 2025-01-03 05:10:13 -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 { transformDocumentNode } from '../transformers';
|
||||||
import { traverse } from '../figma';
|
|
||||||
|
|
||||||
export async function handleExportMessage() {
|
export async function handleExportMessage() {
|
||||||
await figma.loadAllPagesAsync(); // ensures all PageNodes are loaded
|
await figma.loadAllPagesAsync();
|
||||||
const root: NodeData | TextData = await traverse(figma.root); // start the traversal at the root
|
|
||||||
figma.ui.postMessage({ type: 'FIGMAFILE', data: root });
|
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 { extractLinearGradientParamsFromTransform } from '@figma-plugin/helpers';
|
||||||
|
|
||||||
import { Fill } from '../lib/types/utils/fill';
|
import { Fill } from '../../ui/lib/types/utils/fill';
|
||||||
import { rgbToHex } from '../utils';
|
import { rgbToHex } from '../utils';
|
||||||
|
|
||||||
export const translateGradientLinearFill = (
|
export const translateGradientLinearFill = (
|
||||||
|
@ -9,9 +9,10 @@ export const translateGradientLinearFill = (
|
||||||
height: number
|
height: number
|
||||||
): Fill => {
|
): Fill => {
|
||||||
const points = extractLinearGradientParamsFromTransform(width, height, fill.gradientTransform);
|
const points = extractLinearGradientParamsFromTransform(width, height, fill.gradientTransform);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fillColorGradient: {
|
fillColorGradient: {
|
||||||
type: Symbol.for('linear'),
|
type: 'linear',
|
||||||
startX: points.start[0] / width,
|
startX: points.start[0] / width,
|
||||||
startY: points.start[1] / height,
|
startY: points.start[1] / height,
|
||||||
endX: points.end[0] / width,
|
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';
|
import { rgbToHex } from '../utils';
|
||||||
|
|
||||||
export const translateSolidFill = (fill: SolidPaint): Fill => {
|
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 = {
|
const signatures: Signatures = {
|
||||||
'R0lGODdh': 'image/gif',
|
'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 './detectMimeType';
|
||||||
export * from './getImageFill';
|
export * from './rgbToHex';
|
||||||
export * from './getNodeData';
|
export * from './calculateAdjustment';
|
||||||
export * from './getTextData';
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import slugify from 'slugify';
|
import slugify from 'slugify';
|
||||||
|
|
||||||
import { NodeData } from '../common/interfaces';
|
|
||||||
import { createPenpotFile } from './converters';
|
import { createPenpotFile } from './converters';
|
||||||
|
import { PenpotDocument } from './lib/types/penpotDocument';
|
||||||
import { validateFont } from './validators';
|
import { validateFont } from './validators';
|
||||||
|
|
||||||
export const PenpotExporter = () => {
|
export const PenpotExporter = () => {
|
||||||
|
@ -13,19 +13,20 @@ export const PenpotExporter = () => {
|
||||||
setMissingFonts(missingFonts => missingFonts.add(font));
|
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') {
|
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 => {
|
file.export();
|
||||||
if (!validateFont(font)) {
|
|
||||||
addFontWarning(slugify(font.family.toLowerCase()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
file.penpotFile.export();
|
|
||||||
|
|
||||||
setExporting(false);
|
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 { PenpotFile } from '../lib/penpot';
|
||||||
import { translateFills } from '../translators';
|
import { CIRCLE_TYPE } from '../lib/types/circle/circleAttributes';
|
||||||
|
import { CircleShape } from '../lib/types/circle/circleShape';
|
||||||
|
import { translateFillGradients } from '../translators';
|
||||||
|
|
||||||
export const createPenpotCircle = (
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
file: ExportFile,
|
export const createPenpotCircle = (file: PenpotFile, { type, fills, ...rest }: CircleShape) => {
|
||||||
node: NodeData,
|
file.createCircle({
|
||||||
baseX: number,
|
type: CIRCLE_TYPE,
|
||||||
baseY: number
|
fills: translateFillGradients(fills),
|
||||||
) => {
|
...rest
|
||||||
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)
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import { createPenpotItem } from '.';
|
import { createPenpotPage } from '.';
|
||||||
import { ExportFile, NodeData } from '../../common/interfaces';
|
|
||||||
import { createFile } from '../lib/penpot';
|
import { createFile } from '../lib/penpot';
|
||||||
|
import { PenpotDocument } from '../lib/types/penpotDocument';
|
||||||
|
|
||||||
export const createPenpotFile = (node: NodeData) => {
|
export const createPenpotFile = (node: PenpotDocument) => {
|
||||||
const exportFile = { penpotFile: createFile(node.name), fontNames: new Set<FontName>() };
|
const file = createFile(node.name);
|
||||||
for (const page of node.children) {
|
|
||||||
createPenpotItem(exportFile as ExportFile, page, 0, 0);
|
for (const page of node.children ?? []) {
|
||||||
|
createPenpotPage(file, page);
|
||||||
}
|
}
|
||||||
return exportFile;
|
|
||||||
|
return file;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
import { createPenpotItem } from '.';
|
import { PenpotFile } from '../lib/penpot';
|
||||||
import { ExportFile, NodeData } from '../../common/interfaces';
|
import { GROUP_TYPE } from '../lib/types/group/groupAttributes';
|
||||||
|
import { GroupShape } from '../lib/types/group/groupShape';
|
||||||
|
import { createPenpotItem } from './createPenpotItem';
|
||||||
|
|
||||||
export const createPenpotGroup = (
|
export const createPenpotGroup = (
|
||||||
file: ExportFile,
|
file: PenpotFile,
|
||||||
node: NodeData,
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
baseX: number,
|
{ type, children = [], ...rest }: GroupShape
|
||||||
baseY: number
|
|
||||||
) => {
|
) => {
|
||||||
file.penpotFile.addGroup({
|
file.addGroup({
|
||||||
type: Symbol.for('group'),
|
type: GROUP_TYPE,
|
||||||
name: node.name
|
...rest
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const child of node.children) {
|
for (const child of children) {
|
||||||
createPenpotItem(file, child, baseX, baseY);
|
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 = (
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
file: ExportFile,
|
export const createPenpotImage = (file: PenpotFile, { type, ...rest }: ImageShape) => {
|
||||||
node: NodeData,
|
file.createImage({
|
||||||
baseX: number,
|
type: IMAGE_TYPE,
|
||||||
baseY: number
|
...rest
|
||||||
) => {
|
|
||||||
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
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,45 +1,27 @@
|
||||||
import {
|
import {
|
||||||
createPenpotBoard,
|
createPenpotArtboard,
|
||||||
createPenpotCircle,
|
createPenpotCircle,
|
||||||
createPenpotGroup,
|
createPenpotGroup,
|
||||||
createPenpotImage,
|
createPenpotImage,
|
||||||
createPenpotPage,
|
|
||||||
createPenpotRectangle,
|
createPenpotRectangle,
|
||||||
createPenpotText
|
createPenpotText
|
||||||
} from '.';
|
} from '.';
|
||||||
import { ExportFile, NodeData, TextData } from '../../common/interfaces';
|
import { PenpotFile } from '../lib/penpot';
|
||||||
import { calculateAdjustment } from '../utils';
|
import { PenpotNode } from '../lib/types/penpotNode';
|
||||||
|
|
||||||
export const createPenpotItem = (
|
export const createPenpotItem = (file: PenpotFile, node: PenpotNode) => {
|
||||||
file: ExportFile,
|
switch (node.type) {
|
||||||
node: NodeData,
|
case 'rect':
|
||||||
baseX: number,
|
return createPenpotRectangle(file, node);
|
||||||
baseY: number
|
case 'circle':
|
||||||
) => {
|
return createPenpotCircle(file, node);
|
||||||
// We special-case images because an image in figma is a shape with one or many
|
case 'frame':
|
||||||
// image fills. Given that handling images in Penpot is a bit different, we
|
return createPenpotArtboard(file, node);
|
||||||
// rasterize a figma shape with any image fills to a PNG and then add it as a single
|
case 'group':
|
||||||
// Penpot image. Implication is that any node that has an image fill will only be
|
return createPenpotGroup(file, node);
|
||||||
// treated as an image, so we skip node type checks.
|
case 'image':
|
||||||
const hasImageFill = node.fills?.some((fill: Paint) => fill.type === 'IMAGE');
|
return createPenpotImage(file, node);
|
||||||
if (hasImageFill) {
|
case 'text':
|
||||||
// If the nested frames extended the bounds of the rasterized image, we need to
|
return createPenpotText(file, node);
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import { createPenpotItem } from '.';
|
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) => {
|
export const createPenpotPage = (file: PenpotFile, node: PenpotPage) => {
|
||||||
file.penpotFile.addPage(node.name);
|
file.addPage(node.name);
|
||||||
for (const child of node.children) {
|
|
||||||
createPenpotItem(file, child, 0, 0);
|
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 { PenpotFile } from '../lib/penpot';
|
||||||
import { translateFills } from '../translators';
|
import { RECT_TYPE } from '../lib/types/rect/rectAttributes';
|
||||||
|
import { RectShape } from '../lib/types/rect/rectShape';
|
||||||
|
import { translateFillGradients } from '../translators';
|
||||||
|
|
||||||
export const createPenpotRectangle = (
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
file: ExportFile,
|
export const createPenpotRectangle = (file: PenpotFile, { type, fills, ...rest }: RectShape) => {
|
||||||
node: NodeData,
|
file.createRect({
|
||||||
baseX: number,
|
type: RECT_TYPE,
|
||||||
baseY: number
|
fills: translateFillGradients(fills),
|
||||||
) => {
|
...rest
|
||||||
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)
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,73 +1,14 @@
|
||||||
// import slugify from 'slugify';
|
import { PenpotFile } from '../lib/penpot';
|
||||||
import { ExportFile, TextData } from '../../common/interfaces';
|
import { TEXT_TYPE } from '../lib/types/text/textAttributes';
|
||||||
import { TextNode } from '../lib/types/text/textContent';
|
import { TextShape } from '../lib/types/text/textShape';
|
||||||
import {
|
|
||||||
translateFills, // translateFontStyle,
|
|
||||||
// translateHorizontalAlign,
|
|
||||||
translateTextDecoration,
|
|
||||||
translateTextTransform // translateVerticalAlign
|
|
||||||
} from '../translators';
|
|
||||||
|
|
||||||
export const createPenpotText = (
|
export const createPenpotText = (
|
||||||
file: ExportFile,
|
file: PenpotFile,
|
||||||
node: TextData,
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
baseX: number,
|
{ type, ...rest }: TextShape
|
||||||
baseY: number
|
|
||||||
) => {
|
) => {
|
||||||
const children = node.children.map(val => {
|
file.createText({
|
||||||
file.fontNames.add(val.fontName);
|
type: TEXT_TYPE,
|
||||||
|
...rest
|
||||||
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,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export * from './createPenpotBoard';
|
export * from './createPenpotArtboard';
|
||||||
export * from './createPenpotCircle';
|
export * from './createPenpotCircle';
|
||||||
export * from './createPenpotFile';
|
export * from './createPenpotFile';
|
||||||
export * from './createPenpotGroup';
|
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';
|
import { Uuid } from '../utils/uuid';
|
||||||
|
|
||||||
|
export const FRAME_TYPE: unique symbol = Symbol.for('frame');
|
||||||
|
|
||||||
export type FrameAttributes = {
|
export type FrameAttributes = {
|
||||||
|
type: 'frame' | typeof FRAME_TYPE;
|
||||||
id?: Uuid;
|
id?: Uuid;
|
||||||
type: symbol;
|
|
||||||
shapes?: Uuid[];
|
shapes?: Uuid[];
|
||||||
fileThumbnail?: boolean;
|
fileThumbnail?: boolean;
|
||||||
hideFillOnExport?: 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 { Shape } from '../shape';
|
||||||
|
import { Children } from '../utils/children';
|
||||||
import { FrameAttributes } from './frameAttributes';
|
import { FrameAttributes } from './frameAttributes';
|
||||||
|
|
||||||
export type FrameShape = Shape & FrameAttributes;
|
export type FrameShape = Shape & FrameAttributes & Children;
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import { Uuid } from '../utils/uuid';
|
import { Uuid } from '../utils/uuid';
|
||||||
|
|
||||||
|
export const GROUP_TYPE: unique symbol = Symbol.for('group');
|
||||||
|
|
||||||
export type GroupAttributes = {
|
export type GroupAttributes = {
|
||||||
|
type: 'group' | typeof GROUP_TYPE;
|
||||||
id?: Uuid;
|
id?: Uuid;
|
||||||
type: symbol;
|
|
||||||
shapes?: Uuid[];
|
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 { Shape } from '../shape';
|
||||||
|
import { Children } from '../utils/children';
|
||||||
import { GroupAttributes } from './groupAttributes';
|
import { GroupAttributes } from './groupAttributes';
|
||||||
|
|
||||||
export type GroupShape = Shape & GroupAttributes;
|
export type GroupShape = Shape & GroupAttributes & Children;
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { Uuid } from '../utils/uuid';
|
import { Uuid } from '../utils/uuid';
|
||||||
|
|
||||||
|
export const IMAGE_TYPE: unique symbol = Symbol.for('image');
|
||||||
|
|
||||||
export type ImageAttributes = {
|
export type ImageAttributes = {
|
||||||
id?: Uuid;
|
id?: Uuid;
|
||||||
type: symbol;
|
type: 'image' | typeof IMAGE_TYPE;
|
||||||
// TODO: Investigate where it comes from
|
|
||||||
dataUri?: string;
|
dataUri?: string;
|
||||||
metadata: {
|
metadata: {
|
||||||
width: number;
|
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 { Uuid } from '../utils/uuid';
|
||||||
import { TextContent } from './textContent';
|
import { TextContent } from './textContent';
|
||||||
|
|
||||||
|
export const TEXT_TYPE: unique symbol = Symbol.for('text');
|
||||||
|
|
||||||
export type TextAttributes = {
|
export type TextAttributes = {
|
||||||
id?: Uuid;
|
id?: Uuid;
|
||||||
type: symbol;
|
type: 'text' | typeof TEXT_TYPE;
|
||||||
content?: TextContent;
|
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 = {
|
export type Gradient = {
|
||||||
type: symbol; // linear or radial
|
type: 'linear' | 'radial' | typeof LINEAR_TYPE | typeof RADIAL_TYPE; // symbol
|
||||||
startX: number;
|
startX: number;
|
||||||
startY: number;
|
startY: number;
|
||||||
endX: number;
|
endX: number;
|
|
@ -1,8 +1,4 @@
|
||||||
export * from './translateFills';
|
|
||||||
export * from './translateFontStyle';
|
export * from './translateFontStyle';
|
||||||
export * from './translateGradientLinearFill';
|
|
||||||
export * from './translateHorizontalAlign';
|
export * from './translateHorizontalAlign';
|
||||||
export * from './translateSolidFill';
|
|
||||||
export * from './translateTextDecoration';
|
|
||||||
export * from './translateTextTransform';
|
|
||||||
export * from './translateVerticalAlign';
|
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);
|
const gfonts = new Set(fonts);
|
||||||
|
|
||||||
export const validateFont = (fontName: FontName): boolean => {
|
export const validateFont = (fontFamily: string): boolean => {
|
||||||
const name = slugify(fontName.family.toLowerCase());
|
const name = slugify(fontFamily.toLowerCase());
|
||||||
return gfonts.has(name);
|
return gfonts.has(name);
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue