diff --git a/.changeset/sour-radios-know.md b/.changeset/sour-radios-know.md new file mode 100644 index 0000000..ac642ea --- /dev/null +++ b/.changeset/sour-radios-know.md @@ -0,0 +1,5 @@ +--- +"penpot-exporter": minor +--- + +Apply rotations to lines diff --git a/plugin-src/transformers/index.ts b/plugin-src/transformers/index.ts index 86abd9b..9d527b7 100644 --- a/plugin-src/transformers/index.ts +++ b/plugin-src/transformers/index.ts @@ -5,6 +5,7 @@ export * from './transformEllipseNode'; export * from './transformFrameNode'; export * from './transformGroupNode'; export * from './transformInstanceNode'; +export * from './transformLineNode'; export * from './transformPageNode'; export * from './transformPathNode'; export * from './transformRectangleNode'; diff --git a/plugin-src/transformers/partials/transformDimensionAndPosition.ts b/plugin-src/transformers/partials/transformDimensionAndPosition.ts index 866af82..c7fd3e7 100644 --- a/plugin-src/transformers/partials/transformDimensionAndPosition.ts +++ b/plugin-src/transformers/partials/transformDimensionAndPosition.ts @@ -11,6 +11,17 @@ export const transformDimension = ( }; }; +export const transformPosition = ( + node: DimensionAndPositionMixin, + baseX: number, + baseY: number +): Pick => { + return { + x: node.x + baseX, + y: node.y + baseY + }; +}; + export const transformDimensionAndPosition = ( node: DimensionAndPositionMixin, baseX: number, diff --git a/plugin-src/transformers/partials/transformRotationAndPosition.ts b/plugin-src/transformers/partials/transformRotationAndPosition.ts index d4bb0ef..a53bfc9 100644 --- a/plugin-src/transformers/partials/transformRotationAndPosition.ts +++ b/plugin-src/transformers/partials/transformRotationAndPosition.ts @@ -1,5 +1,6 @@ +import { applyInverseRotation, hasRotation } from '@plugin/utils'; + import { ShapeBaseAttributes, ShapeGeomAttributes } from '@ui/lib/types/shapes/shape'; -import { Point } from '@ui/lib/types/utils/point'; export const transformRotationAndPosition = ( node: LayoutMixin, @@ -11,7 +12,7 @@ export const transformRotationAndPosition = ( const x = node.x + baseX; const y = node.y + baseY; - if (rotation === 0 || !node.absoluteBoundingBox) { + if (!hasRotation(rotation) || !node.absoluteBoundingBox) { return { x, y, @@ -21,10 +22,14 @@ export const transformRotationAndPosition = ( }; } - const point = getRotatedPoint({ x, y }, node.absoluteTransform, node.absoluteBoundingBox); + const referencePoint = applyInverseRotation( + { x, y }, + node.absoluteTransform, + node.absoluteBoundingBox + ); return { - ...point, + ...referencePoint, rotation: -rotation < 0 ? -rotation + 360 : -rotation, transform: { a: node.absoluteTransform[0][0], @@ -44,25 +49,3 @@ export const transformRotationAndPosition = ( } }; }; - -const getRotatedPoint = (point: Point, transform: Transform, boundingBox: Rect): Point => { - const centerPoint = { - x: boundingBox.x + boundingBox.width / 2, - y: boundingBox.y + boundingBox.height / 2 - }; - - const relativePoint = { - x: point.x - centerPoint.x, - y: point.y - centerPoint.y - }; - - const rotatedPoint = { - x: relativePoint.x * transform[0][0] + relativePoint.y * transform[1][0], - y: relativePoint.x * transform[0][1] + relativePoint.y * transform[1][1] - }; - - return { - x: centerPoint.x + rotatedPoint.x, - y: centerPoint.y + rotatedPoint.y - }; -}; diff --git a/plugin-src/transformers/partials/transformVectorPaths.ts b/plugin-src/transformers/partials/transformVectorPaths.ts index 680014f..32c2608 100644 --- a/plugin-src/transformers/partials/transformVectorPaths.ts +++ b/plugin-src/transformers/partials/transformVectorPaths.ts @@ -10,26 +10,9 @@ import { transformStrokesFromVector } from '@plugin/transformers/partials'; import { translateFills } from '@plugin/translators/fills'; -import { - translateCommandsToSegments, - translateLineNode, - translateVectorPaths, - translateWindingRule -} from '@plugin/translators/vectors'; +import { translateCommandsToSegments, translateWindingRule } from '@plugin/translators/vectors'; -import { PathAttributes, PathShape } from '@ui/lib/types/shapes/pathShape'; - -export const transformVectorPathsAsContent = ( - node: StarNode | LineNode | PolygonNode, - baseX: number, - baseY: number -): PathAttributes => { - const vectorPaths = getVectorPaths(node); - - return { - content: translateVectorPaths(vectorPaths, baseX + node.x, baseY + node.y) - }; -}; +import { PathShape } from '@ui/lib/types/shapes/pathShape'; export const transformVectorPaths = ( node: VectorNode, @@ -59,21 +42,12 @@ export const transformVectorPaths = ( return [...geometryShapes, ...pathShapes]; }; -const getVectorPaths = (node: StarNode | LineNode | PolygonNode): VectorPaths => { - switch (node.type) { - case 'STAR': - case 'POLYGON': - return node.fillGeometry; - case 'LINE': - return translateLineNode(node); - } -}; - const normalizePath = (path: string): string => { // Round to 2 decimal places all numbers const str = path.replace(/(\d+\.\d+|\d+)/g, (match: string) => { return parseFloat(match).toFixed(2); }); + // remove spaces return str.replace(/\s/g, ''); }; diff --git a/plugin-src/transformers/transformLineNode.ts b/plugin-src/transformers/transformLineNode.ts new file mode 100644 index 0000000..81e1fc0 --- /dev/null +++ b/plugin-src/transformers/transformLineNode.ts @@ -0,0 +1,37 @@ +import { + transformBlend, + transformConstraints, + transformEffects, + transformFigmaIds, + transformLayoutAttributes, + transformPosition, + transformProportion, + transformSceneNode, + transformStrokes +} from '@plugin/transformers/partials'; +import { translateLineNode } from '@plugin/translators/vectors'; + +import { PathShape } from '@ui/lib/types/shapes/pathShape'; + +/** + * In order to match the normal representation of a line in Penpot, we will assume that + * the line is never rotated, so we calculate its normal position. + * + * To represent the line rotated we do take into account the rotation of the line, but only in its content. + */ +export const transformLineNode = (node: LineNode, baseX: number, baseY: number): PathShape => { + return { + type: 'path', + name: node.name, + content: translateLineNode(node, baseX, baseY), + ...transformFigmaIds(node), + ...transformStrokes(node), + ...transformEffects(node), + ...transformPosition(node, baseX, baseY), + ...transformSceneNode(node), + ...transformBlend(node), + ...transformProportion(node), + ...transformLayoutAttributes(node), + ...transformConstraints(node) + }; +}; diff --git a/plugin-src/transformers/transformPathNode.ts b/plugin-src/transformers/transformPathNode.ts index 0eb6e67..1c2e387 100644 --- a/plugin-src/transformers/transformPathNode.ts +++ b/plugin-src/transformers/transformPathNode.ts @@ -8,29 +8,25 @@ import { transformLayoutAttributes, transformProportion, transformSceneNode, - transformStrokes, - transformVectorPathsAsContent + transformStrokes } from '@plugin/transformers/partials'; +import { translateVectorPaths } from '@plugin/translators/vectors'; import { PathShape } from '@ui/lib/types/shapes/pathShape'; -const hasFillGeometry = (node: StarNode | LineNode | PolygonNode): boolean => { - return 'fillGeometry' in node && node.fillGeometry.length > 0; -}; - export const transformPathNode = ( - node: StarNode | LineNode | PolygonNode, + node: StarNode | PolygonNode, baseX: number, baseY: number ): PathShape => { return { type: 'path', name: node.name, + content: translateVectorPaths(node.fillGeometry, baseX + node.x, baseY + node.y), ...transformFigmaIds(node), - ...(hasFillGeometry(node) ? transformFills(node) : []), + ...transformFills(node), ...transformStrokes(node), ...transformEffects(node), - ...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 93af76e..8fb735a 100644 --- a/plugin-src/transformers/transformSceneNode.ts +++ b/plugin-src/transformers/transformSceneNode.ts @@ -7,6 +7,7 @@ import { transformFrameNode, transformGroupNode, transformInstanceNode, + transformLineNode, transformPathNode, transformRectangleNode, transformTextNode, @@ -46,9 +47,11 @@ export const transformSceneNode = async ( case 'VECTOR': penpotNode = transformVectorNode(node, baseX, baseY); break; + case 'LINE': + penpotNode = transformLineNode(node, baseX, baseY); + break; case 'STAR': case 'POLYGON': - case 'LINE': penpotNode = transformPathNode(node, baseX, baseY); break; case 'BOOLEAN_OPERATION': diff --git a/plugin-src/translators/vectors/index.ts b/plugin-src/translators/vectors/index.ts index 69d18de..b1edf93 100644 --- a/plugin-src/translators/vectors/index.ts +++ b/plugin-src/translators/vectors/index.ts @@ -1,3 +1,4 @@ +export * from './translateCommandsToSegments'; export * from './translateLineNode'; export * from './translateVectorPaths'; export * from './translateWindingRule'; diff --git a/plugin-src/translators/vectors/translateCommandsToSegments.ts b/plugin-src/translators/vectors/translateCommandsToSegments.ts new file mode 100644 index 0000000..607e15c --- /dev/null +++ b/plugin-src/translators/vectors/translateCommandsToSegments.ts @@ -0,0 +1,57 @@ +import { Command, CurveToCommand, LineToCommand, MoveToCommand } from 'svg-path-parser'; + +import { Segment } from '@ui/lib/types/shapes/pathShape'; + +export const translateCommandsToSegments = ( + commands: Command[], + baseX: number, + baseY: number +): Segment[] => { + return commands.map(command => { + switch (command.command) { + case 'moveto': + return translateMoveToCommand(command, baseX, baseY); + case 'lineto': + return translateLineToCommand(command, baseX, baseY); + case 'curveto': + return translateCurveToCommand(command, baseX, baseY); + case 'closepath': + default: + return { + command: 'close-path' + }; + } + }); +}; + +const translateMoveToCommand = (command: MoveToCommand, baseX: number, baseY: number): Segment => { + return { + command: 'move-to', + params: { x: command.x + baseX, y: command.y + baseY } + }; +}; + +const translateLineToCommand = (command: LineToCommand, baseX: number, baseY: number): Segment => { + return { + command: 'line-to', + params: { x: command.x + baseX, y: command.y + baseY } + }; +}; + +const translateCurveToCommand = ( + command: CurveToCommand, + baseX: number, + baseY: number +): Segment => { + return { + command: 'curve-to', + params: { + c1x: command.x1 + baseX, + c1y: command.y1 + baseY, + c2x: command.x2 + baseX, + c2y: command.y2 + baseY, + x: command.x + baseX, + y: command.y + baseY + } + }; +}; diff --git a/plugin-src/translators/vectors/translateLineNode.ts b/plugin-src/translators/vectors/translateLineNode.ts index b10436d..c944dcc 100644 --- a/plugin-src/translators/vectors/translateLineNode.ts +++ b/plugin-src/translators/vectors/translateLineNode.ts @@ -1,23 +1,65 @@ -export const translateLineNode = (node: LineNode): VectorPaths => { - const commands = [ +import { Command } from 'svg-path-parser'; + +import { applyInverseRotation, applyRotation, hasRotation } from '@plugin/utils'; + +import { Segment } from '@ui/lib/types/shapes/pathShape'; + +import { translateCommandsToSegments } from '.'; + +export const translateLineNode = (node: LineNode, baseX: number, baseY: number): Segment[] => { + if (!hasRotation(node.rotation) || !node.absoluteBoundingBox) { + return translateCommandsToSegments( + [ + { + x: 0, + y: 0, + command: 'moveto', + code: 'M' + }, + { + x: node.width, + y: 0, + command: 'lineto', + code: 'L' + } + ], + baseX + node.x, + baseY + node.y + ); + } + + const startPoint = applyRotation( + { x: 0, y: 0 }, + node.absoluteTransform, + node.absoluteBoundingBox + ); + + const endPoint = applyRotation( + { x: node.width, y: 0 }, + node.absoluteTransform, + node.absoluteBoundingBox + ); + + const commands: Command[] = [ { + x: startPoint.x, + y: startPoint.y, command: 'moveto', - code: 'M', - x: 0, - y: 0 + code: 'M' }, { + x: endPoint.x, + y: endPoint.y, command: 'lineto', - code: 'L', - x: node.width, - y: node.height + code: 'L' } ]; - return [ - { - windingRule: 'NONE', - data: commands.reduce((acc, { code, x, y }) => acc + `${code} ${x} ${y}`, '') - } - ]; + const referencePoint = applyInverseRotation( + { x: node.x, y: node.y }, + node.absoluteTransform, + node.absoluteBoundingBox + ); + + return translateCommandsToSegments(commands, baseX + referencePoint.x, baseY + referencePoint.y); }; diff --git a/plugin-src/translators/vectors/translateVectorPaths.ts b/plugin-src/translators/vectors/translateVectorPaths.ts index c07af75..e6db64a 100644 --- a/plugin-src/translators/vectors/translateVectorPaths.ts +++ b/plugin-src/translators/vectors/translateVectorPaths.ts @@ -1,73 +1,19 @@ -import { Command, CurveToCommand, LineToCommand, MoveToCommand, parseSVG } from 'svg-path-parser'; +import { parseSVG } from 'svg-path-parser'; import { Segment } from '@ui/lib/types/shapes/pathShape'; +import { translateCommandsToSegments } from '.'; + export const translateVectorPaths = ( paths: VectorPaths, baseX: number, baseY: number ): Segment[] => { - let segments: Segment[] = []; + const segments: Segment[] = []; for (const path of paths) { - const normalizedPaths = parseSVG(path.data); - - segments = [...segments, ...translateCommandsToSegments(normalizedPaths, baseX, baseY)]; + segments.push(...translateCommandsToSegments(parseSVG(path.data), baseX, baseY)); } return segments; }; - -export const translateCommandsToSegments = ( - commands: Command[], - baseX: number, - baseY: number -): Segment[] => { - return commands.map(command => { - switch (command.command) { - case 'moveto': - return translateMoveToCommand(command, baseX, baseY); - case 'lineto': - return translateLineToCommand(command, baseX, baseY); - case 'curveto': - return translateCurveToCommand(command, baseX, baseY); - case 'closepath': - default: - return { - command: 'close-path' - }; - } - }); -}; - -const translateMoveToCommand = (command: MoveToCommand, baseX: number, baseY: number): Segment => { - return { - command: 'move-to', - params: { x: command.x + baseX, y: command.y + baseY } - }; -}; - -const translateLineToCommand = (command: LineToCommand, baseX: number, baseY: number): Segment => { - return { - command: 'line-to', - params: { x: command.x + baseX, y: command.y + baseY } - }; -}; - -const translateCurveToCommand = ( - command: CurveToCommand, - baseX: number, - baseY: number -): Segment => { - return { - command: 'curve-to', - params: { - c1x: command.x1 + baseX, - c1y: command.y1 + baseY, - c2x: command.x2 + baseX, - c2y: command.y2 + baseY, - x: command.x + baseX, - y: command.y + baseY - } - }; -}; diff --git a/plugin-src/utils/applyRotation.ts b/plugin-src/utils/applyRotation.ts new file mode 100644 index 0000000..4a9e1da --- /dev/null +++ b/plugin-src/utils/applyRotation.ts @@ -0,0 +1,40 @@ +import { Point } from '@ui/lib/types/utils/point'; + +const ROTATION_TOLERANCE = 0.000001; + +export const applyRotation = (point: Point, transform: Transform, boundingBox: Rect): Point => { + const centerPoint = calculateCenter(boundingBox); + + const rotatedPoint = applyMatrix(transform, { + x: point.x - centerPoint.x, + y: point.y - centerPoint.y + }); + + return { + x: centerPoint.x + rotatedPoint.x, + y: centerPoint.y + rotatedPoint.y + }; +}; + +export const applyInverseRotation = ( + point: Point, + transform: Transform, + boundingBox: Rect +): Point => applyRotation(point, inverseMatrix(transform), boundingBox); + +export const hasRotation = (rotation: number): boolean => Math.abs(rotation) > ROTATION_TOLERANCE; + +const inverseMatrix = (matrix: Transform): Transform => [ + [matrix[0][0], matrix[1][0], matrix[0][2]], + [matrix[0][1], matrix[1][1], matrix[1][2]] +]; + +const applyMatrix = (matrix: Transform, point: Point): Point => ({ + x: point.x * matrix[0][0] + point.y * matrix[0][1], + y: point.x * matrix[1][0] + point.y * matrix[1][1] +}); + +const calculateCenter = (boundingBox: Rect): Point => ({ + x: boundingBox.x + boundingBox.width / 2, + y: boundingBox.y + boundingBox.height / 2 +}); diff --git a/plugin-src/utils/index.ts b/plugin-src/utils/index.ts index 113505f..425aa91 100644 --- a/plugin-src/utils/index.ts +++ b/plugin-src/utils/index.ts @@ -1,4 +1,5 @@ export * from './applyMatrixToPoint'; +export * from './applyRotation'; export * from './calculateAdjustment'; export * from './calculateLinearGradient'; export * from './calculateRadialGradient';