diff --git a/.changeset/dry-pumas-warn.md b/.changeset/dry-pumas-warn.md new file mode 100644 index 0000000..ccbed73 --- /dev/null +++ b/.changeset/dry-pumas-warn.md @@ -0,0 +1,5 @@ +--- +"penpot-exporter": minor +--- + +Added support for boolean groups diff --git a/plugin-src/transformers/index.ts b/plugin-src/transformers/index.ts index ca595df..a36b2ca 100644 --- a/plugin-src/transformers/index.ts +++ b/plugin-src/transformers/index.ts @@ -1,3 +1,4 @@ +export * from './transformBooleanNode'; export * from './transformDocumentNode'; export * from './transformEllipseNode'; export * from './transformFrameNode'; diff --git a/plugin-src/transformers/transformBooleanNode.ts b/plugin-src/transformers/transformBooleanNode.ts new file mode 100644 index 0000000..55a8234 --- /dev/null +++ b/plugin-src/transformers/transformBooleanNode.ts @@ -0,0 +1,33 @@ +import { + transformBlend, + transformChildren, + transformDimensionAndPosition, + transformEffects, + transformFills, + transformProportion, + transformSceneNode, + transformStrokes +} from '@plugin/transformers/partials'; +import { translateBoolType } from '@plugin/translators'; + +import { BoolShape } from '@ui/lib/types/shapes/boolShape'; + +export const transformBooleanNode = async ( + node: BooleanOperationNode, + baseX: number, + baseY: number +): Promise => { + return { + type: 'bool', + name: node.name, + boolType: translateBoolType(node.booleanOperation), + ...(await transformChildren(node, baseX, baseY)), + ...(await transformFills(node)), + ...transformEffects(node), + ...(await transformStrokes(node)), + ...transformDimensionAndPosition(node, baseX, baseY), + ...transformSceneNode(node), + ...transformBlend(node), + ...transformProportion(node) + }; +}; diff --git a/plugin-src/transformers/transformSceneNode.ts b/plugin-src/transformers/transformSceneNode.ts index 4ff3af4..2a9bf65 100644 --- a/plugin-src/transformers/transformSceneNode.ts +++ b/plugin-src/transformers/transformSceneNode.ts @@ -1,6 +1,7 @@ import { PenpotNode } from '@ui/lib/types/penpotNode'; import { + transformBooleanNode, transformEllipseNode, transformFrameNode, transformGroupNode, @@ -33,6 +34,8 @@ export const transformSceneNode = async ( case 'POLYGON': case 'LINE': return await transformPathNode(node, baseX, baseY); + case 'BOOLEAN_OPERATION': + return await transformBooleanNode(node, baseX, baseY); } console.error(`Unsupported node type: ${node.type}`); diff --git a/plugin-src/translators/index.ts b/plugin-src/translators/index.ts index 4e84d55..1bb9a10 100644 --- a/plugin-src/translators/index.ts +++ b/plugin-src/translators/index.ts @@ -1,4 +1,5 @@ export * from './translateBlendMode'; export * from './translateBlurEffects'; +export * from './translateBoolType'; export * from './translateShadowEffects'; export * from './translateStrokes'; diff --git a/plugin-src/translators/translateBoolType.ts b/plugin-src/translators/translateBoolType.ts new file mode 100644 index 0000000..c2c77b5 --- /dev/null +++ b/plugin-src/translators/translateBoolType.ts @@ -0,0 +1,15 @@ +import { BoolOperations } from '@ui/lib/types/shapes/boolShape'; + +type BooleanOperation = 'UNION' | 'INTERSECT' | 'SUBTRACT' | 'EXCLUDE'; +export const translateBoolType = (booleanOperation: BooleanOperation): BoolOperations => { + switch (booleanOperation) { + case 'EXCLUDE': + return 'exclude'; + case 'INTERSECT': + return 'intersection'; + case 'SUBTRACT': + return 'difference'; + case 'UNION': + return 'union'; + } +}; diff --git a/ui-src/converters/createPenpotBool.ts b/ui-src/converters/createPenpotBool.ts new file mode 100644 index 0000000..446da46 --- /dev/null +++ b/ui-src/converters/createPenpotBool.ts @@ -0,0 +1,22 @@ +import { createPenpotItem } from '@ui/converters/createPenpotItem'; +import { PenpotFile } from '@ui/lib/types/penpotFile'; +import { BoolShape } from '@ui/lib/types/shapes/boolShape'; +import { translateFillGradients, translateUiBlendMode, translateUiBoolType } from '@ui/translators'; + +export const createPenpotBool = ( + file: PenpotFile, + { type, fills, boolType, blendMode, children = [], ...rest }: BoolShape +) => { + file.addBool({ + fills: translateFillGradients(fills), + blendMode: translateUiBlendMode(blendMode), + boolType: translateUiBoolType(boolType), + ...rest + }); + + for (const child of children) { + createPenpotItem(file, child); + } + + file.closeBool(); +}; diff --git a/ui-src/converters/createPenpotItem.ts b/ui-src/converters/createPenpotItem.ts index 2ea9a0c..f9d05b7 100644 --- a/ui-src/converters/createPenpotItem.ts +++ b/ui-src/converters/createPenpotItem.ts @@ -3,6 +3,7 @@ import { PenpotNode } from '@ui/lib/types/penpotNode'; import { createPenpotArtboard, + createPenpotBool, createPenpotCircle, createPenpotGroup, createPenpotPath, @@ -24,5 +25,7 @@ export const createPenpotItem = (file: PenpotFile, node: PenpotNode) => { return createPenpotPath(file, node); case 'text': return createPenpotText(file, node); + case 'bool': + return createPenpotBool(file, node); } }; diff --git a/ui-src/converters/index.ts b/ui-src/converters/index.ts index 5247b20..23b61e3 100644 --- a/ui-src/converters/index.ts +++ b/ui-src/converters/index.ts @@ -1,4 +1,5 @@ export * from './createPenpotArtboard'; +export * from './createPenpotBool'; export * from './createPenpotCircle'; export * from './createPenpotFile'; export * from './createPenpotGroup'; diff --git a/ui-src/lib/types/penpotFile.ts b/ui-src/lib/types/penpotFile.ts index 86438a0..9f4ab9d 100644 --- a/ui-src/lib/types/penpotFile.ts +++ b/ui-src/lib/types/penpotFile.ts @@ -32,6 +32,6 @@ export interface PenpotFile { // lookupShape(shapeId: string): void; // updateObject(id: string, object: any): void; // deleteObject(id: string): void; - asMap(): unknown; + // asMap(): unknown; export(): void; } diff --git a/ui-src/lib/types/penpotNode.ts b/ui-src/lib/types/penpotNode.ts index 1cd0a69..18aa4a0 100644 --- a/ui-src/lib/types/penpotNode.ts +++ b/ui-src/lib/types/penpotNode.ts @@ -1,3 +1,4 @@ +import { BoolShape } from '@ui/lib/types/shapes/boolShape'; import { CircleShape } from '@ui/lib/types/shapes/circleShape'; import { FrameShape } from '@ui/lib/types/shapes/frameShape'; import { GroupShape } from '@ui/lib/types/shapes/groupShape'; @@ -5,4 +6,11 @@ import { PathShape } from '@ui/lib/types/shapes/pathShape'; import { RectShape } from '@ui/lib/types/shapes/rectShape'; import { TextShape } from '@ui/lib/types/shapes/textShape'; -export type PenpotNode = FrameShape | GroupShape | PathShape | RectShape | CircleShape | TextShape; +export type PenpotNode = + | FrameShape + | GroupShape + | PathShape + | RectShape + | CircleShape + | TextShape + | BoolShape; diff --git a/ui-src/lib/types/shapes/boolShape.ts b/ui-src/lib/types/shapes/boolShape.ts index 0f79aa4..2f3de3a 100644 --- a/ui-src/lib/types/shapes/boolShape.ts +++ b/ui-src/lib/types/shapes/boolShape.ts @@ -1,23 +1,39 @@ import { LayoutChildAttributes } from '@ui/lib/types/shapes/layout'; +import { PathContent } from '@ui/lib/types/shapes/pathShape'; import { ShapeAttributes, ShapeBaseAttributes } from '@ui/lib/types/shapes/shape'; +import { Children } from '@ui/lib/types/utils/children'; import { Point } from '@ui/lib/types/utils/point'; import { Uuid } from '@ui/lib/types/utils/uuid'; +export const BOOL_DIFFERENCE: unique symbol = Symbol.for('difference'); +export const BOOL_UNION: unique symbol = Symbol.for('union'); +export const BOOL_INTERSECTION: unique symbol = Symbol.for('intersection'); +export const BOOL_EXCLUDE: unique symbol = Symbol.for('exclude'); + +export type BoolOperations = + | 'difference' + | 'union' + | 'intersection' + | 'exclude' + | typeof BOOL_DIFFERENCE + | typeof BOOL_UNION + | typeof BOOL_INTERSECTION + | typeof BOOL_EXCLUDE; + export type BoolShape = ShapeBaseAttributes & ShapeAttributes & BoolAttributes & - LayoutChildAttributes; + LayoutChildAttributes & + Children; type BoolAttributes = { type?: 'bool'; shapes?: Uuid[]; - boolType: string; // @TODO: in Penpot this is of type :keyword. check if it makes sense - boolContent: BoolContent[]; + boolType: BoolOperations; + boolContent?: BoolContent[]; }; type BoolContent = { - command: string; // @TODO: in Penpot this is of type :keyword. check if it makes sense relative?: boolean; prevPos?: Point; - params?: { [keyword: string]: number }; // @TODO: in Penpot this is of type :keyword. check if it makes sense -}; +} & PathContent; diff --git a/ui-src/translators/index.ts b/ui-src/translators/index.ts index e0ceaeb..71c9812 100644 --- a/ui-src/translators/index.ts +++ b/ui-src/translators/index.ts @@ -1,3 +1,4 @@ export * from './translateFillGradients'; export * from './translatePathContent'; export * from './translateUiBlendMode'; +export * from './translateUiBoolType'; diff --git a/ui-src/translators/translateUiBoolType.ts b/ui-src/translators/translateUiBoolType.ts new file mode 100644 index 0000000..18d5201 --- /dev/null +++ b/ui-src/translators/translateUiBoolType.ts @@ -0,0 +1,22 @@ +import { + BOOL_DIFFERENCE, + BOOL_EXCLUDE, + BOOL_INTERSECTION, + BOOL_UNION, + BoolOperations +} from '@ui/lib/types/shapes/boolShape'; + +export const translateUiBoolType = (booleanOperation: BoolOperations): BoolOperations => { + switch (booleanOperation) { + case 'union': + return BOOL_UNION; + case 'exclude': + return BOOL_EXCLUDE; + case 'difference': + return BOOL_DIFFERENCE; + case 'intersection': + return BOOL_INTERSECTION; + } + + throw new Error(`Unsupported boolean operation: ${String(booleanOperation)}`); +};