Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert Vectors to SVG, take 2 #152

Merged
merged 12 commits into from
Jan 15, 2025
4 changes: 2 additions & 2 deletions apps/plugin/plugin-src/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ const codegenMode = async () => {
},
{
title: `Text Styles`,
code: htmlCodeGenTextStyles(false),
code: htmlCodeGenTextStyles(userPluginSettings),
language: "HTML",
},
];
Expand All @@ -139,7 +139,7 @@ const codegenMode = async () => {
},
{
title: `Text Styles`,
code: htmlCodeGenTextStyles(true),
code: htmlCodeGenTextStyles(userPluginSettings),
language: "HTML",
},
];
Expand Down
20 changes: 16 additions & 4 deletions packages/backend/src/altNodes/altConversion.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import { StyledTextSegmentSubset, ParentNode } from "types";
import { StyledTextSegmentSubset, ParentNode, AltNode } from "types";
import {
overrideReadonlyProperty,
assignParent,
isNotEmpty,
assignRectangleType,
assignChildren,
isTypeOrGroupOfTypes,
} from "./altNodeUtils";
import { addWarning } from "../common/commonConversionWarnings";

export let globalTextStyleSegments: Record<string, StyledTextSegmentSubset[]> =
{};

// List of types that can be flattened into SVG
const canBeFlattened = isTypeOrGroupOfTypes([
"VECTOR",
"STAR",
"POLYGON",
"BOOLEAN_OPERATION",
]);

export const convertNodeToAltNode =
(parent: ParentNode | null) =>
(node: SceneNode): SceneNode => {
Expand Down Expand Up @@ -98,7 +105,12 @@ export const cloneNode = <T extends BaseNode>(
}
assignParent(parent, cloned);

return cloned;
const altNode = {
...cloned,
originalNode: node,
canBeFlattened: canBeFlattened(node),
} as AltNode<T>;
return altNode;
};

// auto convert Frame to Rectangle when Frame has no Children
Expand Down
42 changes: 42 additions & 0 deletions packages/backend/src/altNodes/altNodeUtils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AltNode } from "types";
import { curry } from "../common/curry";

export const overrideReadonlyProperty = curry(
Expand All @@ -19,3 +20,44 @@ export function isNotEmpty<TValue>(
): value is TValue {
return value !== null && value !== undefined;
}

export const isTypeOrGroupOfTypes = curry(
(matchTypes: NodeType[], node: SceneNode): boolean => {
if (node.visible === false || matchTypes.includes(node.type)) return true;

if ("children" in node) {
for (let i = 0; i < node.children.length; i++) {
const childNode = node.children[i];
const result = isTypeOrGroupOfTypes(matchTypes, childNode);
if (result) continue;
// child is false
return false;
}
// all children are true
return true;
}

// not group or vector
return false;
},
);

export const renderNodeAsSVG = async (node: SceneNode) =>
await node.exportAsync({ format: "SVG_STRING" });

export const renderAndAttachSVG = async (node: SceneNode) => {
const altNode = node as AltNode<typeof node>;
// const nodeName = `${node.type}:${node.id}`;
// console.log(altNode);
if (altNode.canBeFlattened) {
if (altNode.svg) {
// console.log(`SVG already rendered for ${nodeName}`);
return altNode;
}
// console.log(`${nodeName} can be flattened!`);
const svg = await renderNodeAsSVG(altNode.originalNode);
// console.log(`${svg}`);
altNode.svg = svg;
}
return altNode;
};
10 changes: 7 additions & 3 deletions packages/backend/src/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { clearWarnings, warnings } from "./common/commonConversionWarnings";
import { PluginSettings } from "types";
import { convertToCode } from "./common/retrieveUI/convertToCode";

export const run = (settings: PluginSettings) => {
export const run = async (settings: PluginSettings) => {
clearWarnings();
const { framework } = settings;
const selection = figma.currentPage.selection;
Expand All @@ -23,8 +23,12 @@ export const run = (settings: PluginSettings) => {
return;
}

const code = convertToCode(convertedSelection, settings);
const htmlPreview = generateHTMLPreview(convertedSelection, settings, code);
const code = await convertToCode(convertedSelection, settings);
const htmlPreview = await generateHTMLPreview(
convertedSelection,
settings,
code,
);
const colors = retrieveGenericSolidUIColors(framework);
const gradients = retrieveGenericGradients(framework);

Expand Down
4 changes: 2 additions & 2 deletions packages/backend/src/common/commonFormatAttributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export const formatStyleAttribute = (
return ` style=${isJSX ? `{{${trimmedStyles}}}` : `"${trimmedStyles}"`}`;
};

export const formatLayerNameAttribute = (name: string) =>
name === "" ? "" : ` data-layer="${name}"`;
export const formatDataAttribute = (label: string, value?: string) =>
` data-${label}${value === undefined ? `` : `="${value}"`}`;

export const formatClassAttribute = (
classes: string[],
Expand Down
4 changes: 4 additions & 0 deletions packages/backend/src/common/nodeVisibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
type VisibilityMixin = { visible: boolean };
const isVisible = (node: VisibilityMixin) => node.visible;
export const getVisibleNodes = (nodes: readonly SceneNode[]) =>
nodes.filter(isVisible);
2 changes: 1 addition & 1 deletion packages/backend/src/common/retrieveFill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Retrieve the first visible color that is being used by the layer, in case there are more than one.
*/
export const retrieveTopFill = (
fills: ReadonlyArray<Paint> | PluginAPI["mixed"],
fills: ReadonlyArray<Paint> | PluginAPI["mixed"] | undefined,
): Paint | undefined => {
if (fills && fills !== figma.mixed && fills.length > 0) {
// on Figma, the top layer is always at the last position
Expand Down
13 changes: 8 additions & 5 deletions packages/backend/src/common/retrieveUI/convertToCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@ import { htmlMain } from "../../html/htmlMain";
import { swiftuiMain } from "../../swiftui/swiftuiMain";
import { tailwindMain } from "../../tailwind/tailwindMain";

export const convertToCode = (nodes: SceneNode[], settings: PluginSettings) => {
export const convertToCode = async (
nodes: SceneNode[],
settings: PluginSettings,
) => {
switch (settings.framework) {
case "Tailwind":
return tailwindMain(nodes, settings);
return await tailwindMain(nodes, settings);
case "Flutter":
return flutterMain(nodes, settings);
return await flutterMain(nodes, settings);
case "SwiftUI":
return swiftuiMain(nodes, settings);
return await swiftuiMain(nodes, settings);
case "HTML":
default:
return htmlMain(nodes, settings);
return await htmlMain(nodes, settings);
}
};
5 changes: 3 additions & 2 deletions packages/backend/src/html/builderImpl/htmlAutoLayout.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { HTMLSettings, PluginSettings } from "types";
import { formatMultipleJSXArray } from "../../common/parseJSX";

const getFlexDirection = (node: InferredAutoLayoutResult): string =>
Expand Down Expand Up @@ -47,7 +48,7 @@ const getFlex = (
export const htmlAutoLayoutProps = (
node: SceneNode,
autoLayout: InferredAutoLayoutResult,
isJsx: boolean,
settings: HTMLSettings,
): string[] =>
formatMultipleJSXArray(
{
Expand All @@ -57,5 +58,5 @@ export const htmlAutoLayoutProps = (
gap: getGap(autoLayout),
display: getFlex(node, autoLayout),
},
isJsx,
settings.jsx,
);
22 changes: 0 additions & 22 deletions packages/backend/src/html/builderImpl/htmlBorderRadius.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,29 +40,7 @@ export const htmlBorderRadius = (node: SceneNode, isJsx: boolean): string[] => {
node.children.length > 0 &&
node.clipsContent === true
) {
// if (
// node.children.some(
// (child) =>
// "layoutPositioning" in child && node.layoutPositioning === "AUTO"
// )
// ) {
// if (singleCorner) {
// comp.push(
// formatWithJSX(
// "clip-path",
// isJsx,
// `inset(0px round ${singleCorner}px)`
// )
// );
// } else if (cornerValues.filter((d) => d > 0).length > 0) {
// const insetValues = cornerValues.map((value) => `${value}px`).join(" ");
// comp.push(
// formatWithJSX("clip-path", isJsx, `inset(0px round ${insetValues})`)
// );
// }
// } else {
comp.push(formatWithJSX("overflow", isJsx, "hidden"));
// }
}

return comp;
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/html/builderImpl/htmlColor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { retrieveTopFill } from "../../common/retrieveFill";

// retrieve the SOLID color on HTML
export const htmlColorFromFills = (
fills: ReadonlyArray<Paint> | PluginAPI["mixed"],
fills: ReadonlyArray<Paint> | PluginAPI["mixed"] | undefined,
): string => {
// kind can be text, bg, border...
// [when testing] fills can be undefined
Expand Down
Loading