From 2557cbdacc7387d0f7268aa58fa89c9bff99d16a Mon Sep 17 00:00:00 2001 From: Jordi Sala Morales Date: Mon, 13 May 2024 13:21:46 +0200 Subject: [PATCH] Implement flatten (#105) * Implement flatten * fix comment * Add changelog --- .changeset/honest-olives-deny.md | 5 ++ .changeset/strong-ties-mate.md | 5 ++ plugin-src/transformers/index.ts | 1 + .../partials/transformDimensionAndPosition.ts | 17 +++++ .../partials/transformVectorPaths.ts | 67 ++++++++++++++++++- plugin-src/transformers/transformGroupNode.ts | 12 +++- plugin-src/transformers/transformPathNode.ts | 5 +- plugin-src/transformers/transformSceneNode.ts | 6 +- .../transformers/transformVectorNode.ts | 25 +++++++ .../translators/translateVectorPaths.ts | 51 +++++++------- plugin-src/utils/getBoundingBox.ts | 25 +++++++ plugin-src/utils/index.ts | 1 + ui-src/lib/types/penpotFile.ts | 2 - 13 files changed, 186 insertions(+), 36 deletions(-) create mode 100644 .changeset/honest-olives-deny.md create mode 100644 .changeset/strong-ties-mate.md create mode 100644 plugin-src/transformers/transformVectorNode.ts create mode 100644 plugin-src/utils/getBoundingBox.ts diff --git a/.changeset/honest-olives-deny.md b/.changeset/honest-olives-deny.md new file mode 100644 index 0000000..782d832 --- /dev/null +++ b/.changeset/honest-olives-deny.md @@ -0,0 +1,5 @@ +--- +"penpot-exporter": minor +--- + +Implement Flatten object translation diff --git a/.changeset/strong-ties-mate.md b/.changeset/strong-ties-mate.md new file mode 100644 index 0000000..4f9618f --- /dev/null +++ b/.changeset/strong-ties-mate.md @@ -0,0 +1,5 @@ +--- +"penpot-exporter": patch +--- + +Fix complex svgs with multiple different fills diff --git a/plugin-src/transformers/index.ts b/plugin-src/transformers/index.ts index 1e7a9fa..ca595df 100644 --- a/plugin-src/transformers/index.ts +++ b/plugin-src/transformers/index.ts @@ -7,3 +7,4 @@ export * from './transformPathNode'; export * from './transformRectangleNode'; export * from './transformSceneNode'; export * from './transformTextNode'; +export * from './transformVectorNode'; diff --git a/plugin-src/transformers/partials/transformDimensionAndPosition.ts b/plugin-src/transformers/partials/transformDimensionAndPosition.ts index 01cdaab..ecc02f3 100644 --- a/plugin-src/transformers/partials/transformDimensionAndPosition.ts +++ b/plugin-src/transformers/partials/transformDimensionAndPosition.ts @@ -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 + }; +}; diff --git a/plugin-src/transformers/partials/transformVectorPaths.ts b/plugin-src/transformers/partials/transformVectorPaths.ts index a18ce09..83bc145 100644 --- a/plugin-src/transformers/partials/transformVectorPaths.ts +++ b/plugin-src/transformers/partials/transformVectorPaths.ts @@ -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 => { + 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 => { + 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) + }; +}; diff --git a/plugin-src/transformers/transformGroupNode.ts b/plugin-src/transformers/transformGroupNode.ts index a091101..2a6afc8 100644 --- a/plugin-src/transformers/transformGroupNode.ts +++ b/plugin-src/transformers/transformGroupNode.ts @@ -13,10 +13,20 @@ export const transformGroupNode = async ( baseX: number, baseY: number ): Promise => { + 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), diff --git a/plugin-src/transformers/transformPathNode.ts b/plugin-src/transformers/transformPathNode.ts index e219ace..aee1c7f 100644 --- a/plugin-src/transformers/transformPathNode.ts +++ b/plugin-src/transformers/transformPathNode.ts @@ -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 => { 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), diff --git a/plugin-src/transformers/transformSceneNode.ts b/plugin-src/transformers/transformSceneNode.ts index 086e647..4ff3af4 100644 --- a/plugin-src/transformers/transformSceneNode.ts +++ b/plugin-src/transformers/transformSceneNode.ts @@ -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); } diff --git a/plugin-src/transformers/transformVectorNode.ts b/plugin-src/transformers/transformVectorNode.ts new file mode 100644 index 0000000..8efe819 --- /dev/null +++ b/plugin-src/transformers/transformVectorNode.ts @@ -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 => { + if (node.fills !== figma.mixed) return transformPathNode(node, baseX, baseY); + + return { + ...transformGroupNodeLike(node, baseX, baseY), + ...(await transformVectorPathsAsChildren(node, baseX, baseY)) + }; +}; diff --git a/plugin-src/translators/translateVectorPaths.ts b/plugin-src/translators/translateVectorPaths.ts index 503eee8..469ce46 100644 --- a/plugin-src/translators/translateVectorPaths.ts +++ b/plugin-src/translators/translateVectorPaths.ts @@ -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', diff --git a/plugin-src/utils/getBoundingBox.ts b/plugin-src/utils/getBoundingBox.ts new file mode 100644 index 0000000..70fa741 --- /dev/null +++ b/plugin-src/utils/getBoundingBox.ts @@ -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; +}; diff --git a/plugin-src/utils/index.ts b/plugin-src/utils/index.ts index 0d6dab2..724c468 100644 --- a/plugin-src/utils/index.ts +++ b/plugin-src/utils/index.ts @@ -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'; diff --git a/ui-src/lib/types/penpotFile.ts b/ui-src/lib/types/penpotFile.ts index c66bb3b..43f42ff 100644 --- a/ui-src/lib/types/penpotFile.ts +++ b/ui-src/lib/types/penpotFile.ts @@ -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;