Skip to content

Commit 3f64849

Browse files
authored
Convert Vectors to SVG, take 2 (#152)
* Added a reference to the original node in the clone * tweak to structure of altNode * Vectors, stars, polygons and boolean unions are exported as SVG. Groups containing only these types will all be compressed into a single SVG. * reworked isVisible * Added positioning to html rendering. This fixed layout for simple cases of SVG graphics. Super complicated example still broken. not sure why exactly. * Instead of passing around flags and storing values globally, most functions now just take the whole settings object * Made a new type for HTML conversion settings * made a new type for tailwind settings, also flutter, swift * added the div wrapper around svgs in tailwind * converted params to use settings in most cases * simplified builder * Simplified tailwind builder
1 parent dffe8e5 commit 3f64849

File tree

18 files changed

+479
-365
lines changed

18 files changed

+479
-365
lines changed

apps/plugin/plugin-src/code.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ const codegenMode = async () => {
122122
},
123123
{
124124
title: `Text Styles`,
125-
code: htmlCodeGenTextStyles(false),
125+
code: htmlCodeGenTextStyles(userPluginSettings),
126126
language: "HTML",
127127
},
128128
];
@@ -139,7 +139,7 @@ const codegenMode = async () => {
139139
},
140140
{
141141
title: `Text Styles`,
142-
code: htmlCodeGenTextStyles(true),
142+
code: htmlCodeGenTextStyles(userPluginSettings),
143143
language: "HTML",
144144
},
145145
];

packages/backend/src/altNodes/altConversion.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
1-
import { StyledTextSegmentSubset, ParentNode } from "types";
1+
import { StyledTextSegmentSubset, ParentNode, AltNode } from "types";
22
import {
3-
overrideReadonlyProperty,
43
assignParent,
54
isNotEmpty,
65
assignRectangleType,
76
assignChildren,
7+
isTypeOrGroupOfTypes,
88
} from "./altNodeUtils";
9-
import { addWarning } from "../common/commonConversionWarnings";
109

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

13+
// List of types that can be flattened into SVG
14+
const canBeFlattened = isTypeOrGroupOfTypes([
15+
"VECTOR",
16+
"STAR",
17+
"POLYGON",
18+
"BOOLEAN_OPERATION",
19+
]);
20+
1421
export const convertNodeToAltNode =
1522
(parent: ParentNode | null) =>
1623
(node: SceneNode): SceneNode => {
@@ -98,7 +105,12 @@ export const cloneNode = <T extends BaseNode>(
98105
}
99106
assignParent(parent, cloned);
100107

101-
return cloned;
108+
const altNode = {
109+
...cloned,
110+
originalNode: node,
111+
canBeFlattened: canBeFlattened(node),
112+
} as AltNode<T>;
113+
return altNode;
102114
};
103115

104116
// auto convert Frame to Rectangle when Frame has no Children

packages/backend/src/altNodes/altNodeUtils.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { AltNode } from "types";
12
import { curry } from "../common/curry";
23

34
export const overrideReadonlyProperty = curry(
@@ -19,3 +20,44 @@ export function isNotEmpty<TValue>(
1920
): value is TValue {
2021
return value !== null && value !== undefined;
2122
}
23+
24+
export const isTypeOrGroupOfTypes = curry(
25+
(matchTypes: NodeType[], node: SceneNode): boolean => {
26+
if (node.visible === false || matchTypes.includes(node.type)) return true;
27+
28+
if ("children" in node) {
29+
for (let i = 0; i < node.children.length; i++) {
30+
const childNode = node.children[i];
31+
const result = isTypeOrGroupOfTypes(matchTypes, childNode);
32+
if (result) continue;
33+
// child is false
34+
return false;
35+
}
36+
// all children are true
37+
return true;
38+
}
39+
40+
// not group or vector
41+
return false;
42+
},
43+
);
44+
45+
export const renderNodeAsSVG = async (node: SceneNode) =>
46+
await node.exportAsync({ format: "SVG_STRING" });
47+
48+
export const renderAndAttachSVG = async (node: SceneNode) => {
49+
const altNode = node as AltNode<typeof node>;
50+
// const nodeName = `${node.type}:${node.id}`;
51+
// console.log(altNode);
52+
if (altNode.canBeFlattened) {
53+
if (altNode.svg) {
54+
// console.log(`SVG already rendered for ${nodeName}`);
55+
return altNode;
56+
}
57+
// console.log(`${nodeName} can be flattened!`);
58+
const svg = await renderNodeAsSVG(altNode.originalNode);
59+
// console.log(`${svg}`);
60+
altNode.svg = svg;
61+
}
62+
return altNode;
63+
};

packages/backend/src/code.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { clearWarnings, warnings } from "./common/commonConversionWarnings";
99
import { PluginSettings } from "types";
1010
import { convertToCode } from "./common/retrieveUI/convertToCode";
1111

12-
export const run = (settings: PluginSettings) => {
12+
export const run = async (settings: PluginSettings) => {
1313
clearWarnings();
1414
const { framework } = settings;
1515
const selection = figma.currentPage.selection;
@@ -23,8 +23,12 @@ export const run = (settings: PluginSettings) => {
2323
return;
2424
}
2525

26-
const code = convertToCode(convertedSelection, settings);
27-
const htmlPreview = generateHTMLPreview(convertedSelection, settings, code);
26+
const code = await convertToCode(convertedSelection, settings);
27+
const htmlPreview = await generateHTMLPreview(
28+
convertedSelection,
29+
settings,
30+
code,
31+
);
2832
const colors = retrieveGenericSolidUIColors(framework);
2933
const gradients = retrieveGenericGradients(framework);
3034

packages/backend/src/common/commonFormatAttributes.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ export const formatStyleAttribute = (
1717
return ` style=${isJSX ? `{{${trimmedStyles}}}` : `"${trimmedStyles}"`}`;
1818
};
1919

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

2323
export const formatClassAttribute = (
2424
classes: string[],
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
type VisibilityMixin = { visible: boolean };
2+
const isVisible = (node: VisibilityMixin) => node.visible;
3+
export const getVisibleNodes = (nodes: readonly SceneNode[]) =>
4+
nodes.filter(isVisible);

packages/backend/src/common/retrieveFill.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Retrieve the first visible color that is being used by the layer, in case there are more than one.
33
*/
44
export const retrieveTopFill = (
5-
fills: ReadonlyArray<Paint> | PluginAPI["mixed"],
5+
fills: ReadonlyArray<Paint> | PluginAPI["mixed"] | undefined,
66
): Paint | undefined => {
77
if (fills && fills !== figma.mixed && fills.length > 0) {
88
// on Figma, the top layer is always at the last position

packages/backend/src/common/retrieveUI/convertToCode.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,19 @@ import { htmlMain } from "../../html/htmlMain";
44
import { swiftuiMain } from "../../swiftui/swiftuiMain";
55
import { tailwindMain } from "../../tailwind/tailwindMain";
66

7-
export const convertToCode = (nodes: SceneNode[], settings: PluginSettings) => {
7+
export const convertToCode = async (
8+
nodes: SceneNode[],
9+
settings: PluginSettings,
10+
) => {
811
switch (settings.framework) {
912
case "Tailwind":
10-
return tailwindMain(nodes, settings);
13+
return await tailwindMain(nodes, settings);
1114
case "Flutter":
12-
return flutterMain(nodes, settings);
15+
return await flutterMain(nodes, settings);
1316
case "SwiftUI":
14-
return swiftuiMain(nodes, settings);
17+
return await swiftuiMain(nodes, settings);
1518
case "HTML":
1619
default:
17-
return htmlMain(nodes, settings);
20+
return await htmlMain(nodes, settings);
1821
}
1922
};

packages/backend/src/html/builderImpl/htmlAutoLayout.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { HTMLSettings, PluginSettings } from "types";
12
import { formatMultipleJSXArray } from "../../common/parseJSX";
23

34
const getFlexDirection = (node: InferredAutoLayoutResult): string =>
@@ -47,7 +48,7 @@ const getFlex = (
4748
export const htmlAutoLayoutProps = (
4849
node: SceneNode,
4950
autoLayout: InferredAutoLayoutResult,
50-
isJsx: boolean,
51+
settings: HTMLSettings,
5152
): string[] =>
5253
formatMultipleJSXArray(
5354
{
@@ -57,5 +58,5 @@ export const htmlAutoLayoutProps = (
5758
gap: getGap(autoLayout),
5859
display: getFlex(node, autoLayout),
5960
},
60-
isJsx,
61+
settings.jsx,
6162
);

packages/backend/src/html/builderImpl/htmlBorderRadius.ts

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -40,29 +40,7 @@ export const htmlBorderRadius = (node: SceneNode, isJsx: boolean): string[] => {
4040
node.children.length > 0 &&
4141
node.clipsContent === true
4242
) {
43-
// if (
44-
// node.children.some(
45-
// (child) =>
46-
// "layoutPositioning" in child && node.layoutPositioning === "AUTO"
47-
// )
48-
// ) {
49-
// if (singleCorner) {
50-
// comp.push(
51-
// formatWithJSX(
52-
// "clip-path",
53-
// isJsx,
54-
// `inset(0px round ${singleCorner}px)`
55-
// )
56-
// );
57-
// } else if (cornerValues.filter((d) => d > 0).length > 0) {
58-
// const insetValues = cornerValues.map((value) => `${value}px`).join(" ");
59-
// comp.push(
60-
// formatWithJSX("clip-path", isJsx, `inset(0px round ${insetValues})`)
61-
// );
62-
// }
63-
// } else {
6443
comp.push(formatWithJSX("overflow", isJsx, "hidden"));
65-
// }
6644
}
6745

6846
return comp;

0 commit comments

Comments
 (0)