Skip to content

Commit

Permalink
Implement flatten (#105)
Browse files Browse the repository at this point in the history
* Implement flatten

* fix comment

* Add changelog
  • Loading branch information
jordisala1991 authored May 13, 2024
1 parent 887f0b9 commit 2557cbd
Show file tree
Hide file tree
Showing 13 changed files with 186 additions and 36 deletions.
5 changes: 5 additions & 0 deletions .changeset/honest-olives-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"penpot-exporter": minor
---

Implement Flatten object translation
5 changes: 5 additions & 0 deletions .changeset/strong-ties-mate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"penpot-exporter": patch
---

Fix complex svgs with multiple different fills
1 change: 1 addition & 0 deletions plugin-src/transformers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from './transformPathNode';
export * from './transformRectangleNode';
export * from './transformSceneNode';
export * from './transformTextNode';
export * from './transformVectorNode';
17 changes: 17 additions & 0 deletions plugin-src/transformers/partials/transformDimensionAndPosition.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { getBoundingBox } from '@plugin/utils';

import { ShapeGeomAttributes } from '@ui/lib/types/shapes/shape';

export const transformDimensionAndPosition = (
Expand All @@ -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
};
};
67 changes: 64 additions & 3 deletions plugin-src/transformers/partials/transformVectorPaths.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -14,15 +25,65 @@ const getVectorPaths = (node: VectorNode | StarNode | LineNode | PolygonNode): V
}
};

export const transformVectorPaths = (
export const transformVectorPathsAsContent = (
node: VectorNode | StarNode | LineNode | PolygonNode,
baseX: number,
baseY: number
): PathAttributes => {
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)
};
};
12 changes: 11 additions & 1 deletion plugin-src/transformers/transformGroupNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
5 changes: 3 additions & 2 deletions plugin-src/transformers/transformPathNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
transformProportion,
transformSceneNode,
transformStrokes,
transformVectorPaths
transformVectorPathsAsContent
} from '@plugin/transformers/partials';

import { PathShape } from '@ui/lib/types/shapes/pathShape';
Expand All @@ -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),
Expand Down
6 changes: 4 additions & 2 deletions plugin-src/transformers/transformSceneNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
transformGroupNode,
transformPathNode,
transformRectangleNode,
transformTextNode
transformTextNode,
transformVectorNode
} from '.';

export const transformSceneNode = async (
Expand All @@ -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);
}
Expand Down
25 changes: 25 additions & 0 deletions plugin-src/transformers/transformVectorNode.ts
Original file line number Diff line number Diff line change
@@ -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))
};
};
51 changes: 25 additions & 26 deletions plugin-src/translators/translateVectorPaths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand All @@ -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',
Expand Down
25 changes: 25 additions & 0 deletions plugin-src/utils/getBoundingBox.ts
Original file line number Diff line number Diff line change
@@ -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;
};
1 change: 1 addition & 0 deletions plugin-src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
2 changes: 0 additions & 2 deletions ui-src/lib/types/penpotFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 2557cbd

Please sign in to comment.