From b85a4f72791f6739702cd2989b40b15de964a494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=A1nchez?= Date: Fri, 14 Jun 2024 10:18:34 +0200 Subject: [PATCH] Autolayout (#151) * wip * added layout sizing * layout positioning * fixes * fixes * fixes * fixes * fixes * fixes --- .changeset/swift-parents-wait.md | 5 + plugin-src/findAllTextnodes.ts | 2 +- plugin-src/transformers/partials/index.ts | 1 + .../transformers/partials/transformLayout.ts | 44 ++++++ .../partials/transformVectorPaths.ts | 4 +- .../transformers/transformBooleanNode.ts | 4 +- .../transformers/transformComponentNode.ts | 6 +- .../transformers/transformEllipseNode.ts | 2 + plugin-src/transformers/transformFrameNode.ts | 6 +- .../transformers/transformInstanceNode.ts | 4 + plugin-src/transformers/transformPathNode.ts | 2 + .../transformers/transformRectangleNode.ts | 2 + plugin-src/transformers/transformTextNode.ts | 2 + plugin-src/translators/index.ts | 1 + plugin-src/translators/translateLayout.ts | 127 ++++++++++++++++++ ui-src/lib/types/shapes/layout.ts | 99 +++++++------- 16 files changed, 257 insertions(+), 54 deletions(-) create mode 100644 .changeset/swift-parents-wait.md create mode 100644 plugin-src/transformers/partials/transformLayout.ts create mode 100644 plugin-src/translators/translateLayout.ts diff --git a/.changeset/swift-parents-wait.md b/.changeset/swift-parents-wait.md new file mode 100644 index 0000000..276ad40 --- /dev/null +++ b/.changeset/swift-parents-wait.md @@ -0,0 +1,5 @@ +--- +"penpot-exporter": minor +--- + +Added support for autolayout diff --git a/plugin-src/findAllTextnodes.ts b/plugin-src/findAllTextnodes.ts index 827d5a1..3856e22 100644 --- a/plugin-src/findAllTextnodes.ts +++ b/plugin-src/findAllTextnodes.ts @@ -15,7 +15,7 @@ export const findAllTextNodes = async () => { extractMissingFonts(node, fonts); } - sleep(0); + await sleep(0); } } diff --git a/plugin-src/transformers/partials/index.ts b/plugin-src/transformers/partials/index.ts index 0253b36..f3a171b 100644 --- a/plugin-src/transformers/partials/index.ts +++ b/plugin-src/transformers/partials/index.ts @@ -6,6 +6,7 @@ export * from './transformDimensionAndPosition'; export * from './transformEffects'; export * from './transformFigmaIds'; export * from './transformFills'; +export * from './transformLayout'; export * from './transformProportion'; export * from './transformRotationAndPosition'; export * from './transformSceneNode'; diff --git a/plugin-src/transformers/partials/transformLayout.ts b/plugin-src/transformers/partials/transformLayout.ts new file mode 100644 index 0000000..2869c45 --- /dev/null +++ b/plugin-src/transformers/partials/transformLayout.ts @@ -0,0 +1,44 @@ +import { + translateLayoutAlignContent, + translateLayoutAlignItems, + translateLayoutFlexDir, + translateLayoutGap, + translateLayoutJustifyContent, + translateLayoutJustifyItems, + translateLayoutPadding, + translateLayoutSizing, + translateLayoutWrapType +} from '@plugin/translators'; + +import { LayoutAttributes, LayoutChildAttributes } from '@ui/lib/types/shapes/layout'; + +export const transformAutoLayout = (node: BaseFrameMixin): LayoutAttributes => { + return { + layout: node.layoutMode !== 'NONE' ? 'flex' : undefined, + layoutFlexDir: translateLayoutFlexDir(node.layoutMode), + layoutGap: translateLayoutGap( + node.layoutMode, + node.itemSpacing, + node.primaryAxisAlignItems === 'SPACE_BETWEEN' + ), + layoutWrapType: translateLayoutWrapType(node.layoutWrap), + layoutPadding: translateLayoutPadding(node), + layoutJustifyContent: translateLayoutJustifyContent(node), + layoutJustifyItems: translateLayoutJustifyItems(node), + layoutAlignContent: translateLayoutAlignContent(node), + layoutAlignItems: translateLayoutAlignItems(node) + }; +}; + +export const transformLayoutAttributes = ( + node: LayoutMixin +): Pick< + LayoutChildAttributes, + 'layoutItemH-Sizing' | 'layoutItemV-Sizing' | 'layoutItemAbsolute' +> => { + return { + 'layoutItemH-Sizing': translateLayoutSizing(node.layoutSizingHorizontal), + 'layoutItemV-Sizing': translateLayoutSizing(node.layoutSizingVertical), + 'layoutItemAbsolute': node.layoutPositioning === 'ABSOLUTE' + }; +}; diff --git a/plugin-src/transformers/partials/transformVectorPaths.ts b/plugin-src/transformers/partials/transformVectorPaths.ts index 25428cc..680014f 100644 --- a/plugin-src/transformers/partials/transformVectorPaths.ts +++ b/plugin-src/transformers/partials/transformVectorPaths.ts @@ -4,6 +4,7 @@ import { transformBlend, transformDimensionAndPositionFromVectorPath, transformEffects, + transformLayoutAttributes, transformProportion, transformSceneNode, transformStrokesFromVector @@ -110,6 +111,7 @@ const transformVectorPath = ( ...transformDimensionAndPositionFromVectorPath(vectorPath, baseX, baseY), ...transformSceneNode(node), ...transformBlend(node), - ...transformProportion(node) + ...transformProportion(node), + ...transformLayoutAttributes(node) }; }; diff --git a/plugin-src/transformers/transformBooleanNode.ts b/plugin-src/transformers/transformBooleanNode.ts index ef414a3..b85d260 100644 --- a/plugin-src/transformers/transformBooleanNode.ts +++ b/plugin-src/transformers/transformBooleanNode.ts @@ -5,6 +5,7 @@ import { transformEffects, transformFigmaIds, transformFills, + transformLayoutAttributes, transformProportion, transformSceneNode, transformStrokes @@ -30,6 +31,7 @@ export const transformBooleanNode = async ( ...transformDimensionAndPosition(node, baseX, baseY), ...transformSceneNode(node), ...transformBlend(node), - ...transformProportion(node) + ...transformProportion(node), + ...transformLayoutAttributes(node) }; }; diff --git a/plugin-src/transformers/transformComponentNode.ts b/plugin-src/transformers/transformComponentNode.ts index 5d654be..703cf74 100644 --- a/plugin-src/transformers/transformComponentNode.ts +++ b/plugin-src/transformers/transformComponentNode.ts @@ -1,5 +1,6 @@ import { componentsLibrary } from '@plugin/ComponentLibrary'; import { + transformAutoLayout, transformBlend, transformChildren, transformConstraints, @@ -8,6 +9,7 @@ import { transformEffects, transformFigmaIds, transformFills, + transformLayoutAttributes, transformProportion, transformSceneNode, transformStrokes @@ -32,10 +34,12 @@ export const transformComponentNode = async ( ...transformSceneNode(node), ...transformBlend(node), ...transformProportion(node), + ...transformLayoutAttributes(node), ...transformCornerRadius(node), ...(await transformChildren(node, baseX + node.x, baseY + node.y)), ...transformDimensionAndPosition(node, baseX, baseY), - ...transformConstraints(node) + ...transformConstraints(node), + ...transformAutoLayout(node) }); return { diff --git a/plugin-src/transformers/transformEllipseNode.ts b/plugin-src/transformers/transformEllipseNode.ts index b2f165a..97c06c7 100644 --- a/plugin-src/transformers/transformEllipseNode.ts +++ b/plugin-src/transformers/transformEllipseNode.ts @@ -5,6 +5,7 @@ import { transformEffects, transformFigmaIds, transformFills, + transformLayoutAttributes, transformProportion, transformRotationAndPosition, transformSceneNode, @@ -30,6 +31,7 @@ export const transformEllipseNode = ( ...transformSceneNode(node), ...transformBlend(node), ...transformProportion(node), + ...transformLayoutAttributes(node), ...transformConstraints(node) }; }; diff --git a/plugin-src/transformers/transformFrameNode.ts b/plugin-src/transformers/transformFrameNode.ts index 57ba08f..e355f5b 100644 --- a/plugin-src/transformers/transformFrameNode.ts +++ b/plugin-src/transformers/transformFrameNode.ts @@ -1,4 +1,5 @@ import { + transformAutoLayout, transformBlend, transformChildren, transformConstraints, @@ -7,6 +8,7 @@ import { transformEffects, transformFigmaIds, transformFills, + transformLayoutAttributes, transformProportion, transformSceneNode, transformStrokes @@ -34,9 +36,11 @@ export const transformFrameNode = async ( // @see: https://forum.figma.com/t/add-a-blendmode-property-for-sectionnode/58560 ...transformBlend(node), ...transformProportion(node), + ...transformLayoutAttributes(node), ...transformCornerRadius(node), ...transformEffects(node), - ...transformConstraints(node) + ...transformConstraints(node), + ...transformAutoLayout(node) }; } diff --git a/plugin-src/transformers/transformInstanceNode.ts b/plugin-src/transformers/transformInstanceNode.ts index 2ebce7a..5ec4936 100644 --- a/plugin-src/transformers/transformInstanceNode.ts +++ b/plugin-src/transformers/transformInstanceNode.ts @@ -1,5 +1,6 @@ import { remoteComponentLibrary } from '@plugin/RemoteComponentLibrary'; import { + transformAutoLayout, transformBlend, transformChildren, transformConstraints, @@ -8,6 +9,7 @@ import { transformEffects, transformFigmaIds, transformFills, + transformLayoutAttributes, transformProportion, transformSceneNode, transformStrokes @@ -43,9 +45,11 @@ export const transformInstanceNode = async ( ...transformSceneNode(node), ...transformBlend(node), ...transformProportion(node), + ...transformLayoutAttributes(node), ...transformCornerRadius(node), ...transformDimensionAndPosition(node, baseX, baseY), ...transformConstraints(node), + ...transformAutoLayout(node), ...(await transformChildren(node, baseX + node.x, baseY + node.y)) }; }; diff --git a/plugin-src/transformers/transformPathNode.ts b/plugin-src/transformers/transformPathNode.ts index 1157e33..0eb6e67 100644 --- a/plugin-src/transformers/transformPathNode.ts +++ b/plugin-src/transformers/transformPathNode.ts @@ -5,6 +5,7 @@ import { transformEffects, transformFigmaIds, transformFills, + transformLayoutAttributes, transformProportion, transformSceneNode, transformStrokes, @@ -34,6 +35,7 @@ export const transformPathNode = ( ...transformSceneNode(node), ...transformBlend(node), ...transformProportion(node), + ...transformLayoutAttributes(node), ...transformConstraints(node) }; }; diff --git a/plugin-src/transformers/transformRectangleNode.ts b/plugin-src/transformers/transformRectangleNode.ts index 4e3e444..208c166 100644 --- a/plugin-src/transformers/transformRectangleNode.ts +++ b/plugin-src/transformers/transformRectangleNode.ts @@ -6,6 +6,7 @@ import { transformEffects, transformFigmaIds, transformFills, + transformLayoutAttributes, transformProportion, transformRotationAndPosition, transformSceneNode, @@ -31,6 +32,7 @@ export const transformRectangleNode = ( ...transformSceneNode(node), ...transformBlend(node), ...transformProportion(node), + ...transformLayoutAttributes(node), ...transformCornerRadius(node), ...transformConstraints(node) }; diff --git a/plugin-src/transformers/transformTextNode.ts b/plugin-src/transformers/transformTextNode.ts index 21a6b47..49e65f8 100644 --- a/plugin-src/transformers/transformTextNode.ts +++ b/plugin-src/transformers/transformTextNode.ts @@ -4,6 +4,7 @@ import { transformDimensionAndPosition, transformEffects, transformFigmaIds, + transformLayoutAttributes, transformProportion, transformSceneNode, transformStrokes, @@ -23,6 +24,7 @@ export const transformTextNode = (node: TextNode, baseX: number, baseY: number): ...transformSceneNode(node), ...transformBlend(node), ...transformProportion(node), + ...transformLayoutAttributes(node), ...transformStrokes(node), ...transformConstraints(node) }; diff --git a/plugin-src/translators/index.ts b/plugin-src/translators/index.ts index 9d58aa6..4d824cc 100644 --- a/plugin-src/translators/index.ts +++ b/plugin-src/translators/index.ts @@ -3,5 +3,6 @@ export * from './translateBlurEffects'; export * from './translateBoolType'; export * from './translateChildren'; export * from './translateConstraints'; +export * from './translateLayout'; export * from './translateShadowEffects'; export * from './translateStrokes'; diff --git a/plugin-src/translators/translateLayout.ts b/plugin-src/translators/translateLayout.ts new file mode 100644 index 0000000..f24544d --- /dev/null +++ b/plugin-src/translators/translateLayout.ts @@ -0,0 +1,127 @@ +import { + JustifyAlignContent, + JustifyAlignItems, + LayoutFlexDir, + LayoutGap, + LayoutPadding, + LayoutSizing, + LayoutWrapType +} from '@ui/lib/types/shapes/layout'; + +type FigmaLayoutMode = 'NONE' | 'HORIZONTAL' | 'VERTICAL'; + +type FigmaWrap = 'NO_WRAP' | 'WRAP'; + +type FigmaLayoutSizing = 'FIXED' | 'HUG' | 'FILL'; + +export const translateLayoutFlexDir = (layoutMode: FigmaLayoutMode): LayoutFlexDir | undefined => { + switch (layoutMode) { + case 'HORIZONTAL': + return 'row-reverse'; + case 'VERTICAL': + return 'column-reverse'; + default: + return; + } +}; + +export const translateLayoutGap = ( + layoutMode: FigmaLayoutMode, + itemSpacing: number, + auto: boolean = false +): LayoutGap => { + if (auto) { + return { + rowGap: 0, + columnGap: 0 + }; + } + + return { + rowGap: layoutMode === 'VERTICAL' ? itemSpacing : 0, + columnGap: layoutMode === 'HORIZONTAL' ? itemSpacing : 0 + }; +}; + +export const translateLayoutWrapType = (wrap: FigmaWrap): LayoutWrapType => { + switch (wrap) { + case 'NO_WRAP': + return 'nowrap'; + case 'WRAP': + return 'wrap'; + } +}; + +export const translateLayoutPadding = (node: BaseFrameMixin): LayoutPadding => { + return { + p1: node.paddingTop, + p2: node.paddingRight, + p3: node.paddingBottom, + p4: node.paddingLeft + }; +}; + +export const translateLayoutJustifyContent = (node: BaseFrameMixin): JustifyAlignContent => { + switch (node.primaryAxisAlignItems) { + case 'MIN': + return 'start'; + case 'CENTER': + return 'center'; + case 'MAX': + return 'end'; + case 'SPACE_BETWEEN': + return 'space-between'; + default: + return 'stretch'; + } +}; + +export const translateLayoutJustifyItems = (node: BaseFrameMixin): JustifyAlignItems => { + switch (node.primaryAxisAlignItems) { + case 'MIN': + return 'start'; + case 'CENTER': + return 'center'; + case 'MAX': + return 'end'; + default: + return 'stretch'; + } +}; + +export const translateLayoutAlignContent = (node: BaseFrameMixin): JustifyAlignContent => { + switch (node.counterAxisAlignItems) { + case 'MIN': + return 'start'; + case 'CENTER': + return 'center'; + case 'MAX': + return 'end'; + default: + return 'stretch'; + } +}; + +export const translateLayoutAlignItems = (node: BaseFrameMixin): JustifyAlignItems => { + switch (node.counterAxisAlignItems) { + case 'MIN': + return 'start'; + case 'CENTER': + return 'center'; + case 'MAX': + return 'end'; + default: + return 'stretch'; + } +}; + +export const translateLayoutSizing = (sizing: FigmaLayoutSizing): LayoutSizing => { + switch (sizing) { + case 'FIXED': + return 'fix'; + case 'HUG': + return 'auto'; + case 'FILL': + return 'fill'; + } +}; diff --git a/ui-src/lib/types/shapes/layout.ts b/ui-src/lib/types/shapes/layout.ts index c84408f..b966def 100644 --- a/ui-src/lib/types/shapes/layout.ts +++ b/ui-src/lib/types/shapes/layout.ts @@ -2,48 +2,41 @@ import { Uuid } from '@ui/lib/types/utils/uuid'; export const ITEM_MARGIN_SIMPLE_TYPE: unique symbol = Symbol.for('simple'); export const ITEM_MARGIN_MULTIPLE_TYPE: unique symbol = Symbol.for('multiple'); -export const ITEM_HSIZING_FILL: unique symbol = Symbol.for('fill'); -export const ITEM_HSIZING_FIX: unique symbol = Symbol.for('fix'); -export const ITEM_HSIZING_AUTO: unique symbol = Symbol.for('auto'); -export const ITEM_VSIZING_FILL: unique symbol = Symbol.for('fill'); -export const ITEM_VSIZING_FIX: unique symbol = Symbol.for('fix'); -export const ITEM_VSIZING_AUTO: unique symbol = Symbol.for('auto'); +export const ITEM_SIZING_FILL: unique symbol = Symbol.for('fill'); +export const ITEM_SIZING_FIX: unique symbol = Symbol.for('fix'); +export const ITEM_SIZING_AUTO: unique symbol = Symbol.for('auto'); export const ITEM_ALIGN_SELF_START: unique symbol = Symbol.for('start'); export const ITEM_ALIGN_SELF_END: unique symbol = Symbol.for('end'); export const ITEM_ALIGN_SELF_CENTER: unique symbol = Symbol.for('center'); export const ITEM_ALIGN_SELF_STRETCH: unique symbol = Symbol.for('stretch'); +export type LayoutSizing = + | 'fill' + | 'fix' + | 'auto' + | typeof ITEM_SIZING_FILL + | typeof ITEM_SIZING_FIX + | typeof ITEM_SIZING_AUTO; + export type LayoutChildAttributes = { - layoutItemMarginType?: + 'layoutItemMarginType'?: | 'simple' | 'multiple' | typeof ITEM_MARGIN_SIMPLE_TYPE | typeof ITEM_MARGIN_MULTIPLE_TYPE; - layoutItemMargin?: { + 'layoutItemMargin'?: { m1?: number; m2?: number; m3?: number; m4?: number; }; - layoutItemMaxH?: number; - layoutItemMinH?: number; - layoutItemMaxW?: number; - layoutItemMinW?: number; - layoutItemHSizing?: - | 'fill' - | 'fix' - | 'auto' - | typeof ITEM_HSIZING_FILL - | typeof ITEM_HSIZING_FIX - | typeof ITEM_HSIZING_AUTO; - layoutItemVSizing?: - | 'fill' - | 'fix' - | 'auto' - | typeof ITEM_VSIZING_FILL - | typeof ITEM_VSIZING_FIX - | typeof ITEM_VSIZING_AUTO; - layoutItemAlignSelf?: + 'layoutItemMaxH'?: number; + 'layoutItemMinH'?: number; + 'layoutItemMaxW'?: number; + 'layoutItemMinW'?: number; + 'layoutItemH-Sizing'?: LayoutSizing; + 'layoutItemV-Sizing'?: LayoutSizing; + 'layoutItemAlignSelf'?: | 'start' | 'end' | 'center' @@ -52,11 +45,11 @@ export type LayoutChildAttributes = { | typeof ITEM_ALIGN_SELF_END | typeof ITEM_ALIGN_SELF_CENTER | typeof ITEM_ALIGN_SELF_STRETCH; - layoutItemAbsolute?: boolean; - layoutItemZIndex?: number; + 'layoutItemAbsolute'?: boolean; + 'layoutItemZIndex'?: number; }; -type JustifyAlignContent = +export type JustifyAlignContent = | 'start' | 'center' | 'end' @@ -65,30 +58,38 @@ type JustifyAlignContent = | 'space-evenly' | 'stretch'; -type JustifyAlignItems = 'start' | 'end' | 'center' | 'stretch'; +export type JustifyAlignItems = 'start' | 'end' | 'center' | 'stretch'; + +export type LayoutFlexDir = + | 'row' + | 'reverse-row' + | 'row-reverse' + | 'column' + | 'reverse-column' + | 'column-reverse'; + +export type LayoutGap = { + rowGap?: number; + columnGap?: number; +}; + +export type LayoutWrapType = 'wrap' | 'nowrap' | 'no-wrap'; + +export type LayoutPadding = { + p1?: number; + p2?: number; + p3?: number; + p4?: number; +}; export type LayoutAttributes = { layout?: 'flex' | 'grid'; - layoutFlexDir?: - | 'row' - | 'reverse-row' - | 'row-reverse' - | 'column' - | 'reverse-column' - | 'column-reverse'; - layoutGap?: { - rowGap?: number; - columnGap?: number; - }; + layoutFlexDir?: LayoutFlexDir; + layoutGap?: LayoutGap; layoutGapType?: 'simple' | 'multiple'; - layoutWrapType?: 'wrap' | 'nowrap' | 'no-wrap'; + layoutWrapType?: LayoutWrapType; layoutPaddingType?: 'simple' | 'multiple'; - layoutPadding?: { - p1?: number; - p2?: number; - p3?: number; - p4?: number; - }; + layoutPadding?: LayoutPadding; layoutJustifyContent?: JustifyAlignContent; layoutJustifyItems?: JustifyAlignItems; layoutAlignContent?: JustifyAlignContent;