Skip to content

Commit

Permalink
Implement rotation for vector lines (#160)
Browse files Browse the repository at this point in the history
* Implement rotation for vector lines

* wip

* Improve rotations for lines

* add changelog

* add layout attributes to line
  • Loading branch information
jordisala1991 authored Jun 14, 2024
1 parent b85a4f7 commit af81fc7
Show file tree
Hide file tree
Showing 14 changed files with 235 additions and 138 deletions.
5 changes: 5 additions & 0 deletions .changeset/sour-radios-know.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"penpot-exporter": minor
---

Apply rotations to lines
1 change: 1 addition & 0 deletions plugin-src/transformers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
11 changes: 11 additions & 0 deletions plugin-src/transformers/partials/transformDimensionAndPosition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ export const transformDimension = (
};
};

export const transformPosition = (
node: DimensionAndPositionMixin,
baseX: number,
baseY: number
): Pick<ShapeGeomAttributes, 'x' | 'y'> => {
return {
x: node.x + baseX,
y: node.y + baseY
};
};

export const transformDimensionAndPosition = (
node: DimensionAndPositionMixin,
baseX: number,
Expand Down
35 changes: 9 additions & 26 deletions plugin-src/transformers/partials/transformRotationAndPosition.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand All @@ -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],
Expand All @@ -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
};
};
32 changes: 3 additions & 29 deletions plugin-src/transformers/partials/transformVectorPaths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { PathAttributes, PathShape } from '@ui/lib/types/shapes/pathShape';
import { translateCommandsToSegments, translateWindingRule } from '@plugin/translators/vectors';

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,
Expand Down Expand Up @@ -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, '');
};
Expand Down
37 changes: 37 additions & 0 deletions plugin-src/transformers/transformLineNode.ts
Original file line number Diff line number Diff line change
@@ -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)
};
};
14 changes: 5 additions & 9 deletions plugin-src/transformers/transformPathNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
5 changes: 4 additions & 1 deletion plugin-src/transformers/transformSceneNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
transformFrameNode,
transformGroupNode,
transformInstanceNode,
transformLineNode,
transformPathNode,
transformRectangleNode,
transformTextNode,
Expand Down Expand Up @@ -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':
Expand Down
1 change: 1 addition & 0 deletions plugin-src/translators/vectors/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './translateCommandsToSegments';
export * from './translateLineNode';
export * from './translateVectorPaths';
export * from './translateWindingRule';
57 changes: 57 additions & 0 deletions plugin-src/translators/vectors/translateCommandsToSegments.ts
Original file line number Diff line number Diff line change
@@ -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
}
};
};
70 changes: 56 additions & 14 deletions plugin-src/translators/vectors/translateLineNode.ts
Original file line number Diff line number Diff line change
@@ -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);
};
Loading

0 comments on commit af81fc7

Please sign in to comment.