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:
parent
887f0b9205
commit
2557cbdacc
13 changed files with 186 additions and 36 deletions
5
.changeset/honest-olives-deny.md
Normal file
5
.changeset/honest-olives-deny.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"penpot-exporter": minor
|
||||
---
|
||||
|
||||
Implement Flatten object translation
|
5
.changeset/strong-ties-mate.md
Normal file
5
.changeset/strong-ties-mate.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"penpot-exporter": patch
|
||||
---
|
||||
|
||||
Fix complex svgs with multiple different fills
|
|
@ -7,3 +7,4 @@ export * from './transformPathNode';
|
|||
export * from './transformRectangleNode';
|
||||
export * from './transformSceneNode';
|
||||
export * from './transformTextNode';
|
||||
export * from './transformVectorNode';
|
||||
|
|
|
@ -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
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
25
plugin-src/transformers/transformVectorNode.ts
Normal file
25
plugin-src/transformers/transformVectorNode.ts
Normal 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))
|
||||
};
|
||||
};
|
|
@ -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',
|
||||
|
|
25
plugin-src/utils/getBoundingBox.ts
Normal file
25
plugin-src/utils/getBoundingBox.ts
Normal 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;
|
||||
};
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue