mirror of
https://github.com/penpot/penpot-exporter-figma-plugin.git
synced 2024-12-22 05:33:02 -05:00
Apply rotations to any figure (#168)
* Apply rotations to any figure * add changelog * apply rotations to curves too
This commit is contained in:
parent
4591369e3c
commit
202e7f4fda
17 changed files with 246 additions and 196 deletions
5
.changeset/wise-olives-teach.md
Normal file
5
.changeset/wise-olives-teach.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"penpot-exporter": minor
|
||||
---
|
||||
|
||||
Implement rotation for any arbitrary figure
|
|
@ -1,7 +1,21 @@
|
|||
import { translateRotation, translateZeroRotation } from '@plugin/translators';
|
||||
import { applyInverseRotation, hasRotation } from '@plugin/utils';
|
||||
|
||||
import { ShapeBaseAttributes, ShapeGeomAttributes } from '@ui/lib/types/shapes/shape';
|
||||
|
||||
export const transformRotation = (
|
||||
node: LayoutMixin,
|
||||
baseRotation: number
|
||||
): Pick<ShapeBaseAttributes, 'transform' | 'transformInverse' | 'rotation'> => {
|
||||
const rotation = node.rotation + baseRotation;
|
||||
|
||||
if (!hasRotation(rotation)) {
|
||||
return translateZeroRotation();
|
||||
}
|
||||
|
||||
return translateRotation(node.absoluteTransform, rotation);
|
||||
};
|
||||
|
||||
export const transformRotationAndPosition = (
|
||||
node: LayoutMixin,
|
||||
baseRotation: number
|
||||
|
@ -15,9 +29,7 @@ export const transformRotationAndPosition = (
|
|||
return {
|
||||
x,
|
||||
y,
|
||||
rotation,
|
||||
transform: undefined,
|
||||
transformInverse: undefined
|
||||
...translateZeroRotation()
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -29,22 +41,6 @@ export const transformRotationAndPosition = (
|
|||
|
||||
return {
|
||||
...referencePoint,
|
||||
rotation: -rotation < 0 ? -rotation + 360 : -rotation,
|
||||
transform: {
|
||||
a: node.absoluteTransform[0][0],
|
||||
b: node.absoluteTransform[1][0],
|
||||
c: node.absoluteTransform[0][1],
|
||||
d: node.absoluteTransform[1][1],
|
||||
e: 0,
|
||||
f: 0
|
||||
},
|
||||
transformInverse: {
|
||||
a: node.absoluteTransform[0][0],
|
||||
b: node.absoluteTransform[0][1],
|
||||
c: node.absoluteTransform[1][0],
|
||||
d: node.absoluteTransform[1][1],
|
||||
e: 0,
|
||||
f: 0
|
||||
}
|
||||
...translateRotation(node.absoluteTransform, rotation)
|
||||
};
|
||||
};
|
||||
|
|
|
@ -9,11 +9,11 @@ import {
|
|||
transformStrokesFromVector
|
||||
} from '@plugin/transformers/partials';
|
||||
import { translateFills } from '@plugin/translators/fills';
|
||||
import { translateCommandsToSegments, translateWindingRule } from '@plugin/translators/vectors';
|
||||
import { translateCommands, translateWindingRule } from '@plugin/translators/vectors';
|
||||
|
||||
import { PathShape } from '@ui/lib/types/shapes/pathShape';
|
||||
|
||||
export const transformVectorPaths = (node: VectorNode): PathShape[] => {
|
||||
export const transformVectorPaths = (node: VectorNode, baseRotation: number): PathShape[] => {
|
||||
const pathShapes = node.vectorPaths
|
||||
.filter((vectorPath, index) => {
|
||||
return (
|
||||
|
@ -22,7 +22,7 @@ export const transformVectorPaths = (node: VectorNode): PathShape[] => {
|
|||
);
|
||||
})
|
||||
.map((vectorPath, index) =>
|
||||
transformVectorPath(node, vectorPath, (node.vectorNetwork.regions ?? [])[index])
|
||||
transformVectorPath(node, vectorPath, (node.vectorNetwork.regions ?? [])[index], baseRotation)
|
||||
);
|
||||
|
||||
const geometryShapes = node.fillGeometry
|
||||
|
@ -32,7 +32,7 @@ export const transformVectorPaths = (node: VectorNode): PathShape[] => {
|
|||
vectorPath => normalizePath(vectorPath.data) === normalizePath(geometry.data)
|
||||
)
|
||||
)
|
||||
.map(geometry => transformVectorPath(node, geometry, undefined));
|
||||
.map(geometry => transformVectorPath(node, geometry, undefined, baseRotation));
|
||||
|
||||
return [...geometryShapes, ...pathShapes];
|
||||
};
|
||||
|
@ -58,18 +58,15 @@ const nodeHasFills = (
|
|||
const transformVectorPath = (
|
||||
node: VectorNode,
|
||||
vectorPath: VectorPath,
|
||||
vectorRegion: VectorRegion | undefined
|
||||
vectorRegion: VectorRegion | undefined,
|
||||
baseRotation: number
|
||||
): PathShape => {
|
||||
const normalizedPaths = parseSVG(vectorPath.data);
|
||||
|
||||
return {
|
||||
type: 'path',
|
||||
name: 'svg-path',
|
||||
content: translateCommandsToSegments(
|
||||
normalizedPaths,
|
||||
node.absoluteTransform[0][2],
|
||||
node.absoluteTransform[1][2]
|
||||
),
|
||||
content: translateCommands(node, normalizedPaths, baseRotation),
|
||||
fills:
|
||||
vectorPath.windingRule === 'NONE' ? [] : translateFills(vectorRegion?.fills ?? node.fills),
|
||||
svgAttrs: {
|
||||
|
|
|
@ -9,9 +9,10 @@ import {
|
|||
transformSceneNode,
|
||||
transformStrokes
|
||||
} from '@plugin/transformers/partials';
|
||||
import { translateLineNode } from '@plugin/translators/vectors';
|
||||
import { translateCommands } from '@plugin/translators/vectors';
|
||||
|
||||
import { PathShape } from '@ui/lib/types/shapes/pathShape';
|
||||
import { Segment } from '@ui/lib/types/shapes/pathShape';
|
||||
|
||||
/**
|
||||
* In order to match the normal representation of a line in Penpot, we will assume that
|
||||
|
@ -35,3 +36,24 @@ export const transformLineNode = (node: LineNode, baseRotation: number): PathSha
|
|||
...transformOverrides(node)
|
||||
};
|
||||
};
|
||||
|
||||
const translateLineNode = (node: LineNode, baseRotation: number): Segment[] => {
|
||||
return translateCommands(
|
||||
node,
|
||||
[
|
||||
{
|
||||
x: 0,
|
||||
y: 0,
|
||||
command: 'moveto',
|
||||
code: 'M'
|
||||
},
|
||||
{
|
||||
x: node.width,
|
||||
y: 0,
|
||||
command: 'lineto',
|
||||
code: 'L'
|
||||
}
|
||||
],
|
||||
baseRotation
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { parseSVG } from 'svg-path-parser';
|
||||
|
||||
import {
|
||||
transformBlend,
|
||||
transformConstraints,
|
||||
|
@ -7,12 +9,13 @@ import {
|
|||
transformLayoutAttributes,
|
||||
transformOverrides,
|
||||
transformProportion,
|
||||
transformRotation,
|
||||
transformSceneNode,
|
||||
transformStrokes
|
||||
} from '@plugin/transformers/partials';
|
||||
import { translatePathNode } from '@plugin/translators/vectors';
|
||||
import { translateCommands } from '@plugin/translators/vectors';
|
||||
|
||||
import { PathShape } from '@ui/lib/types/shapes/pathShape';
|
||||
import { PathShape, Segment } from '@ui/lib/types/shapes/pathShape';
|
||||
|
||||
export const transformPathNode = (
|
||||
node: StarNode | PolygonNode,
|
||||
|
@ -29,8 +32,12 @@ export const transformPathNode = (
|
|||
...transformSceneNode(node),
|
||||
...transformBlend(node),
|
||||
...transformProportion(node),
|
||||
...transformRotation(node, baseRotation),
|
||||
...transformLayoutAttributes(node),
|
||||
...transformConstraints(node),
|
||||
...transformOverrides(node)
|
||||
};
|
||||
};
|
||||
|
||||
const translatePathNode = (node: StarNode | PolygonNode, baseRotation: number): Segment[] =>
|
||||
translateCommands(node, parseSVG(node.fillGeometry[0].data), baseRotation);
|
||||
|
|
|
@ -20,7 +20,7 @@ export const transformVectorNode = (
|
|||
node: VectorNode,
|
||||
baseRotation: number
|
||||
): GroupShape | PathShape => {
|
||||
const children = transformVectorPaths(node);
|
||||
const children = transformVectorPaths(node, baseRotation);
|
||||
|
||||
if (children.length === 1) {
|
||||
return {
|
||||
|
|
|
@ -4,5 +4,6 @@ export * from './translateBoolType';
|
|||
export * from './translateChildren';
|
||||
export * from './translateConstraints';
|
||||
export * from './translateLayout';
|
||||
export * from './translateRotation';
|
||||
export * from './translateShadowEffects';
|
||||
export * from './translateStrokes';
|
||||
|
|
33
plugin-src/translators/translateRotation.ts
Normal file
33
plugin-src/translators/translateRotation.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { ShapeBaseAttributes } from '@ui/lib/types/shapes/shape';
|
||||
|
||||
export const translateRotation = (
|
||||
transform: Transform,
|
||||
rotation: number
|
||||
): Pick<ShapeBaseAttributes, 'transform' | 'transformInverse' | 'rotation'> => ({
|
||||
rotation: -rotation < 0 ? -rotation + 360 : -rotation,
|
||||
transform: {
|
||||
a: transform[0][0],
|
||||
b: transform[1][0],
|
||||
c: transform[0][1],
|
||||
d: transform[1][1],
|
||||
e: 0,
|
||||
f: 0
|
||||
},
|
||||
transformInverse: {
|
||||
a: transform[0][0],
|
||||
b: transform[0][1],
|
||||
c: transform[1][0],
|
||||
d: transform[1][1],
|
||||
e: 0,
|
||||
f: 0
|
||||
}
|
||||
});
|
||||
|
||||
export const translateZeroRotation = (): Pick<
|
||||
ShapeBaseAttributes,
|
||||
'transform' | 'transformInverse' | 'rotation'
|
||||
> => ({
|
||||
rotation: 0,
|
||||
transform: undefined,
|
||||
transformInverse: undefined
|
||||
});
|
|
@ -1,4 +1,4 @@
|
|||
export * from './translateCommandsToSegments';
|
||||
export * from './translateLineNode';
|
||||
export * from './translatePathNode';
|
||||
export * from './translateCommands';
|
||||
export * from './translateNonRotatedCommands';
|
||||
export * from './translateRotatedCommands';
|
||||
export * from './translateWindingRule';
|
||||
|
|
18
plugin-src/translators/vectors/translateCommands.ts
Normal file
18
plugin-src/translators/vectors/translateCommands.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { Command } from 'svg-path-parser';
|
||||
|
||||
import { hasRotation } from '@plugin/utils';
|
||||
|
||||
import { translateNonRotatedCommands } from '.';
|
||||
import { translateRotatedCommands } from './translateRotatedCommands';
|
||||
|
||||
export const translateCommands = (node: LayoutMixin, commands: Command[], baseRotation: number) => {
|
||||
if (hasRotation(node.rotation + baseRotation) && node.absoluteBoundingBox) {
|
||||
return translateRotatedCommands(commands, node.absoluteTransform, node.absoluteBoundingBox);
|
||||
}
|
||||
|
||||
return translateNonRotatedCommands(
|
||||
commands,
|
||||
node.absoluteTransform[0][2],
|
||||
node.absoluteTransform[1][2]
|
||||
);
|
||||
};
|
|
@ -1,57 +0,0 @@
|
|||
import { Command, CurveToCommand, LineToCommand, MoveToCommand } from 'svg-path-parser';
|
||||
|
||||
import { Segment } from '@ui/lib/types/shapes/pathShape';
|
||||
|
||||
export const translateCommandsToSegments = (
|
||||
commands: Command[],
|
||||
baseX: number = 0,
|
||||
baseY: number = 0
|
||||
): 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
|
||||
}
|
||||
};
|
||||
};
|
|
@ -1,63 +0,0 @@
|
|||
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, baseRotation: number): Segment[] => {
|
||||
const rotation = node.rotation + baseRotation;
|
||||
const x = node.absoluteTransform[0][2];
|
||||
const y = node.absoluteTransform[1][2];
|
||||
|
||||
if (!hasRotation(rotation) || !node.absoluteBoundingBox) {
|
||||
return translateCommandsToSegments(
|
||||
[
|
||||
{
|
||||
x: 0,
|
||||
y: 0,
|
||||
command: 'moveto',
|
||||
code: 'M'
|
||||
},
|
||||
{
|
||||
x: node.width,
|
||||
y: 0,
|
||||
command: 'lineto',
|
||||
code: 'L'
|
||||
}
|
||||
],
|
||||
x,
|
||||
y
|
||||
);
|
||||
}
|
||||
|
||||
const referencePoint = applyInverseRotation(
|
||||
{ x, y },
|
||||
node.absoluteTransform,
|
||||
node.absoluteBoundingBox
|
||||
);
|
||||
|
||||
const endPoint = applyRotation(
|
||||
{ x: referencePoint.x + node.width, y: referencePoint.y },
|
||||
node.absoluteTransform,
|
||||
node.absoluteBoundingBox
|
||||
);
|
||||
|
||||
const commands: Command[] = [
|
||||
{
|
||||
x,
|
||||
y,
|
||||
command: 'moveto',
|
||||
code: 'M'
|
||||
},
|
||||
{
|
||||
x: endPoint.x,
|
||||
y: endPoint.y,
|
||||
command: 'lineto',
|
||||
code: 'L'
|
||||
}
|
||||
];
|
||||
|
||||
return translateCommandsToSegments(commands);
|
||||
};
|
|
@ -0,0 +1,59 @@
|
|||
import { Command, CurveToCommand, LineToCommand, MoveToCommand } from 'svg-path-parser';
|
||||
|
||||
import { Segment } from '@ui/lib/types/shapes/pathShape';
|
||||
|
||||
export const translateNonRotatedCommands = (
|
||||
commands: Command[],
|
||||
baseX: number = 0,
|
||||
baseY: number = 0
|
||||
): Segment[] => {
|
||||
return commands.map(command => translateNonRotatedCommand(command, baseX, baseY));
|
||||
};
|
||||
|
||||
export const translateNonRotatedCommand = (
|
||||
command: Command,
|
||||
baseX: number,
|
||||
baseY: number
|
||||
): Segment => {
|
||||
switch (command.command) {
|
||||
case 'moveto':
|
||||
return translateMoveTo(command, baseX, baseY);
|
||||
case 'lineto':
|
||||
return translateLineTo(command, baseX, baseY);
|
||||
case 'curveto':
|
||||
return translateCurveTo(command, baseX, baseY);
|
||||
case 'closepath':
|
||||
default:
|
||||
return {
|
||||
command: 'close-path'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const translateMoveTo = (command: MoveToCommand, baseX: number, baseY: number): Segment => {
|
||||
return {
|
||||
command: 'move-to',
|
||||
params: { x: command.x + baseX, y: command.y + baseY }
|
||||
};
|
||||
};
|
||||
|
||||
const translateLineTo = (command: LineToCommand, baseX: number, baseY: number): Segment => {
|
||||
return {
|
||||
command: 'line-to',
|
||||
params: { x: command.x + baseX, y: command.y + baseY }
|
||||
};
|
||||
};
|
||||
|
||||
const translateCurveTo = (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
|
||||
}
|
||||
};
|
||||
};
|
|
@ -1,37 +0,0 @@
|
|||
import { parseSVG } from 'svg-path-parser';
|
||||
|
||||
import { applyInverseRotation, hasRotation } from '@plugin/utils';
|
||||
|
||||
import { Segment } from '@ui/lib/types/shapes/pathShape';
|
||||
import { Point } from '@ui/lib/types/utils/point';
|
||||
|
||||
import { translateCommandsToSegments } from '.';
|
||||
|
||||
export const translatePathNode = (
|
||||
node: StarNode | PolygonNode,
|
||||
baseRotation: number
|
||||
): Segment[] => {
|
||||
let referencePoint: Point = {
|
||||
x: node.absoluteTransform[0][2],
|
||||
y: node.absoluteTransform[1][2]
|
||||
};
|
||||
|
||||
if (hasRotation(node.rotation + baseRotation) && node.absoluteBoundingBox) {
|
||||
referencePoint = applyInverseRotation(
|
||||
{ x: referencePoint.x, y: referencePoint.y },
|
||||
node.absoluteTransform,
|
||||
node.absoluteBoundingBox
|
||||
);
|
||||
}
|
||||
|
||||
const segments: Segment[] = [];
|
||||
|
||||
for (const path of node.fillGeometry) {
|
||||
segments.push(...translateVectorPath(path, referencePoint.x, referencePoint.y));
|
||||
}
|
||||
|
||||
return segments;
|
||||
};
|
||||
|
||||
const translateVectorPath = (path: VectorPath, baseX: number, baseY: number): Segment[] =>
|
||||
translateCommandsToSegments(parseSVG(path.data), baseX, baseY);
|
31
plugin-src/translators/vectors/translateRotatedCommands.ts
Normal file
31
plugin-src/translators/vectors/translateRotatedCommands.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { Command } from 'svg-path-parser';
|
||||
|
||||
import { applyInverseRotation, applyRotationToSegment } from '@plugin/utils';
|
||||
|
||||
import { ClosePath, Segment } from '@ui/lib/types/shapes/pathShape';
|
||||
|
||||
import { translateNonRotatedCommand } from '.';
|
||||
|
||||
const isClosePath = (segment: Segment): segment is ClosePath => segment.command === 'close-path';
|
||||
|
||||
export const translateRotatedCommands = (
|
||||
commands: Command[],
|
||||
transform: Transform,
|
||||
boundingBox: Rect
|
||||
): Segment[] => {
|
||||
const referencePoint = applyInverseRotation(
|
||||
{ x: transform[0][2], y: transform[1][2] },
|
||||
transform,
|
||||
boundingBox
|
||||
);
|
||||
|
||||
return commands.map(command => {
|
||||
const segment = translateNonRotatedCommand(command, referencePoint.x, referencePoint.y);
|
||||
|
||||
if (isClosePath(segment)) {
|
||||
return segment;
|
||||
}
|
||||
|
||||
return applyRotationToSegment(segment, transform, boundingBox);
|
||||
});
|
||||
};
|
|
@ -1,3 +1,4 @@
|
|||
import { ClosePath, CurveTo, Segment } from '@ui/lib/types/shapes/pathShape';
|
||||
import { Point } from '@ui/lib/types/utils/point';
|
||||
|
||||
const ROTATION_TOLERANCE = 0.000001;
|
||||
|
@ -16,6 +17,41 @@ export const applyRotation = (point: Point, transform: Transform, boundingBox: R
|
|||
};
|
||||
};
|
||||
|
||||
export const applyRotationToSegment = (
|
||||
segment: Exclude<Segment, ClosePath>,
|
||||
transform: Transform,
|
||||
boundingBox: Rect
|
||||
): Segment => {
|
||||
const rotated = applyRotation(
|
||||
{ x: segment.params.x, y: segment.params.y },
|
||||
transform,
|
||||
boundingBox
|
||||
);
|
||||
|
||||
if (isCurveTo(segment)) {
|
||||
const curve1 = applyRotation(
|
||||
{ x: segment.params.c1x, y: segment.params.c1y },
|
||||
transform,
|
||||
boundingBox
|
||||
);
|
||||
const curve2 = applyRotation(
|
||||
{ x: segment.params.c2x, y: segment.params.c2y },
|
||||
transform,
|
||||
boundingBox
|
||||
);
|
||||
|
||||
segment.params.c1x = curve1.x;
|
||||
segment.params.c1y = curve1.y;
|
||||
segment.params.c2x = curve2.x;
|
||||
segment.params.c2y = curve2.y;
|
||||
}
|
||||
|
||||
segment.params.x = rotated.x;
|
||||
segment.params.y = rotated.y;
|
||||
|
||||
return segment;
|
||||
};
|
||||
|
||||
export const applyInverseRotation = (
|
||||
point: Point,
|
||||
transform: Transform,
|
||||
|
@ -38,3 +74,5 @@ const calculateCenter = (boundingBox: Rect): Point => ({
|
|||
x: boundingBox.x + boundingBox.width / 2,
|
||||
y: boundingBox.y + boundingBox.height / 2
|
||||
});
|
||||
|
||||
const isCurveTo = (segment: Segment): segment is CurveTo => segment.command === 'curve-to';
|
||||
|
|
|
@ -41,7 +41,7 @@ type LineTo = {
|
|||
};
|
||||
};
|
||||
|
||||
type ClosePath = {
|
||||
export type ClosePath = {
|
||||
command: 'close-path' | typeof VECTOR_CLOSE_PATH;
|
||||
};
|
||||
|
||||
|
@ -53,7 +53,7 @@ type MoveTo = {
|
|||
};
|
||||
};
|
||||
|
||||
type CurveTo = {
|
||||
export type CurveTo = {
|
||||
command: 'curve-to' | typeof VECTOR_CURVE_TO;
|
||||
params: {
|
||||
x: number;
|
||||
|
|
Loading…
Reference in a new issue