0
Fork 0
mirror of https://github.com/penpot/penpot-exporter-figma-plugin.git synced 2024-12-22 05:33:02 -05:00

Implement flatten (#105)

* Implement flatten

* fix comment

* Add changelog
This commit is contained in:
Jordi Sala Morales 2024-05-13 13:21:46 +02:00 committed by GitHub
parent 887f0b9205
commit 2557cbdacc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 186 additions and 36 deletions

View file

@ -0,0 +1,5 @@
---
"penpot-exporter": minor
---
Implement Flatten object translation

View file

@ -0,0 +1,5 @@
---
"penpot-exporter": patch
---
Fix complex svgs with multiple different fills

View file

@ -7,3 +7,4 @@ export * from './transformPathNode';
export * from './transformRectangleNode';
export * from './transformSceneNode';
export * from './transformTextNode';
export * from './transformVectorNode';

View file

@ -1,3 +1,5 @@
import { getBoundingBox } from '@plugin/utils';
import { ShapeGeomAttributes } from '@ui/lib/types/shapes/shape';
export const transformDimensionAndPosition = (
@ -12,3 +14,18 @@ export const transformDimensionAndPosition = (
height: node.height
};
};
export const transformDimensionAndPositionFromVectorPath = (
vectorPath: VectorPath,
baseX: number,
baseY: number
): ShapeGeomAttributes => {
const boundingBox = getBoundingBox(vectorPath);
return {
x: boundingBox.x1 + baseX,
y: boundingBox.y1 + baseY,
width: boundingBox.x2 - boundingBox.x1,
height: boundingBox.y2 - boundingBox.y1
};
};

View file

@ -1,6 +1,17 @@
import { createLineGeometry, translateVectorPaths } from '@plugin/translators';
import {
transformBlend,
transformDimensionAndPositionFromVectorPath,
transformEffects,
transformProportion,
transformSceneNode,
transformStrokes
} from '@plugin/transformers/partials';
import { createLineGeometry, translateVectorPath, translateVectorPaths } from '@plugin/translators';
import { translateFills } from '@plugin/translators';
import { PathAttributes } from '@ui/lib/types/shapes/pathShape';
import { PathShape } from '@ui/lib/types/shapes/pathShape';
import { Children } from '@ui/lib/types/utils/children';
const getVectorPaths = (node: VectorNode | StarNode | LineNode | PolygonNode): VectorPaths => {
switch (node.type) {
@ -14,7 +25,7 @@ const getVectorPaths = (node: VectorNode | StarNode | LineNode | PolygonNode): V
}
};
export const transformVectorPaths = (
export const transformVectorPathsAsContent = (
node: VectorNode | StarNode | LineNode | PolygonNode,
baseX: number,
baseY: number
@ -22,7 +33,57 @@ export const transformVectorPaths = (
const vectorPaths = getVectorPaths(node);
return {
type: 'path',
content: translateVectorPaths(vectorPaths, baseX + node.x, baseY + node.y)
};
};
export const transformVectorPathsAsChildren = async (
node: VectorNode,
baseX: number,
baseY: number
): Promise<Children> => {
return {
children: await Promise.all(
node.vectorPaths.map((vectorPath, index) =>
transformVectorPath(
node,
vectorPath,
(node.vectorNetwork.regions ?? [])[index],
baseX,
baseY
)
)
)
};
};
const transformVectorPath = async (
node: VectorNode,
vectorPath: VectorPath,
vectorRegion: VectorRegion | undefined,
baseX: number,
baseY: number
): Promise<PathShape> => {
const dimensionAndPosition = transformDimensionAndPositionFromVectorPath(
vectorPath,
baseX,
baseY
);
return {
type: 'path',
name: 'svg-path',
content: translateVectorPath(vectorPath, baseX + node.x, baseY + node.y),
fills: await translateFills(
vectorRegion?.fills ?? node.fills,
dimensionAndPosition.width,
dimensionAndPosition.height
),
...(await transformStrokes(node)),
...transformEffects(node),
...dimensionAndPosition,
...transformSceneNode(node),
...transformBlend(node),
...transformProportion(node)
};
};

View file

@ -13,10 +13,20 @@ export const transformGroupNode = async (
baseX: number,
baseY: number
): Promise<GroupShape> => {
return {
...transformGroupNodeLike(node, baseX, baseY),
...(await transformChildren(node, baseX, baseY))
};
};
export const transformGroupNodeLike = (
node: BaseNodeMixin & DimensionAndPositionMixin & BlendMixin & SceneNodeMixin & MinimalBlendMixin,
baseX: number,
baseY: number
): GroupShape => {
return {
type: 'group',
name: node.name,
...(await transformChildren(node, baseX, baseY)),
...transformDimensionAndPosition(node, baseX, baseY),
...transformEffects(node),
...transformSceneNode(node),

View file

@ -6,7 +6,7 @@ import {
transformProportion,
transformSceneNode,
transformStrokes,
transformVectorPaths
transformVectorPathsAsContent
} from '@plugin/transformers/partials';
import { PathShape } from '@ui/lib/types/shapes/pathShape';
@ -21,11 +21,12 @@ export const transformPathNode = async (
baseY: number
): Promise<PathShape> => {
return {
type: 'path',
name: node.name,
...(hasFillGeometry(node) ? await transformFills(node) : []),
...(await transformStrokes(node)),
...transformEffects(node),
...transformVectorPaths(node, baseX, baseY),
...transformVectorPathsAsContent(node, baseX, baseY),
...transformDimensionAndPosition(node, baseX, baseY),
...transformSceneNode(node),
...transformBlend(node),

View file

@ -6,7 +6,8 @@ import {
transformGroupNode,
transformPathNode,
transformRectangleNode,
transformTextNode
transformTextNode,
transformVectorNode
} from '.';
export const transformSceneNode = async (
@ -26,9 +27,10 @@ export const transformSceneNode = async (
return await transformGroupNode(node, baseX, baseY);
case 'TEXT':
return await transformTextNode(node, baseX, baseY);
case 'VECTOR':
return await transformVectorNode(node, baseX, baseY);
case 'STAR':
case 'POLYGON':
case 'VECTOR':
case 'LINE':
return await transformPathNode(node, baseX, baseY);
}

View file

@ -0,0 +1,25 @@
import { transformVectorPathsAsChildren } from '@plugin/transformers/partials';
import { GroupShape } from '@ui/lib/types/shapes/groupShape';
import { PathShape } from '@ui/lib/types/shapes/pathShape';
import { transformGroupNodeLike, transformPathNode } from '.';
/*
* Vector nodes can have multiple vector paths, each with its own fills.
*
* If the fills are not mixed, we treat it like a normal `PathShape`.
* If the fills are mixed, we treat the vector node as a `GroupShape` with multiple `PathShape` children.
*/
export const transformVectorNode = async (
node: VectorNode,
baseX: number,
baseY: number
): Promise<GroupShape | PathShape> => {
if (node.fills !== figma.mixed) return transformPathNode(node, baseX, baseY);
return {
...transformGroupNodeLike(node, baseX, baseY),
...(await transformVectorPathsAsChildren(node, baseX, baseY))
};
};

View file

@ -16,32 +16,7 @@ export const translateVectorPaths = (
return segments;
};
export const createLineGeometry = (node: LineNode): VectorPaths => {
const commands: (MoveToCommand | LineToCommand)[] = [];
commands.push({
command: 'moveto',
code: 'M',
x: 0,
y: 0
});
commands.push({
command: 'lineto',
code: 'L',
x: node.width,
y: node.height
});
return [
{
windingRule: 'NONZERO',
data: commands.map(({ code, x, y }) => `${code} ${x} ${y}`).join(' ') + ' Z'
}
];
};
const translateVectorPath = (path: VectorPath, baseX: number, baseY: number): Segment[] => {
export const translateVectorPath = (path: VectorPath, baseX: number, baseY: number): Segment[] => {
const normalizedPaths = parseSVG(path.data);
return normalizedPaths.map(command => {
@ -61,6 +36,30 @@ const translateVectorPath = (path: VectorPath, baseX: number, baseY: number): Se
});
};
export const createLineGeometry = (node: LineNode): VectorPaths => {
const commands = [
{
command: 'moveto',
code: 'M',
x: 0,
y: 0
},
{
command: 'lineto',
code: 'L',
x: node.width,
y: node.height
}
];
return [
{
windingRule: 'NONZERO',
data: commands.map(({ code, x, y }) => `${code} ${x} ${y}`).join(' ') + ' Z'
}
];
};
const translateMoveToCommand = (command: MoveToCommand, baseX: number, baseY: number): Segment => {
return {
command: 'move-to',

View file

@ -0,0 +1,25 @@
import { parseSVG } from 'svg-path-parser';
type BoundingBox = { x1: number; y1: number; x2: number; y2: number };
export const getBoundingBox = (vectorPath: VectorPath): BoundingBox => {
const path = parseSVG(vectorPath.data);
if (!path.length) return { x1: 0, y1: 0, x2: 0, y2: 0 };
const bounds = { x1: Infinity, y1: Infinity, x2: -Infinity, y2: -Infinity };
for (const points of path) {
switch (points.code) {
case 'M':
case 'L':
case 'C':
bounds.x1 = Math.min(bounds.x1, points.x);
bounds.y1 = Math.min(bounds.y1, points.y);
bounds.x2 = Math.max(bounds.x2, points.x);
bounds.y2 = Math.max(bounds.y2, points.y);
}
}
return bounds;
};

View file

@ -2,5 +2,6 @@ export * from './applyMatrixToPoint';
export * from './calculateAdjustment';
export * from './calculateLinearGradient';
export * from './detectMimeType';
export * from './getBoundingBox';
export * from './matrixInvert';
export * from './rgbToHex';

View file

@ -20,8 +20,6 @@ export interface PenpotFile {
createCircle(circle: CircleShape): void;
createPath(path: PathShape): void;
createText(options: TextShape): void;
// createSVG(svg: any): void;
// closeSVG(): void;
// addLibraryColor(color: any): void;
// updateLibraryColor(color: any): void;
// deleteLibraryColor(color: any): void;