Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added Docs/ExampleFigmaSource.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Docs/ExampleUnityResult.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
289 changes: 111 additions & 178 deletions README.md

Large diffs are not rendered by default.

175 changes: 168 additions & 7 deletions UnityFigmaBridge/Editor/FigmaApi/FigmaApiUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,38 @@ public static async Task<FigmaFile> GetFigmaDocument(string fileId, string acces
}

/// <summary>
/// Load Figma document from cached file (previously downloaded)
/// </summary>
/// <returns>The cached FigmaFile or null if not found</returns>
public static FigmaFile LoadFigmaDocumentFromCache()
{
var cachePath = Path.Combine("Assets", WRITE_FILE_PATH);

if (!File.Exists(cachePath))
{
return null;
}

try
{
var jsonContent = File.ReadAllText(cachePath);
JsonSerializerSettings settings = new JsonSerializerSettings()
{
DefaultValueHandling = DefaultValueHandling.Include,
MissingMemberHandling = MissingMemberHandling.Ignore,
NullValueHandling = NullValueHandling.Ignore,
};

var figmaFile = JsonConvert.DeserializeObject<FigmaFile>(jsonContent, settings);
Debug.Log($"Figma file loaded from cache: {figmaFile.name}");
return figmaFile;
}
catch (Exception e)
{
Debug.LogError($"Error loading cached Figma document: {e}");
return null;
}
}
/// Requests a server-side rendering of nodes from a document, returning list of urls to download
/// </summary>
/// <param name="fileId">Figma File Id</param>
Expand Down Expand Up @@ -245,24 +277,24 @@ public static async Task<FigmaFileNodes> GetFigmaFileNodes(string fileId, string
/// Generates a standardised list of files to download
/// </summary>
/// <param name="imageFillData"></param>
/// <param name="foundImageFills"></param>
/// <param name="serverRenderData"></param>
/// <param name="serverRenderNodes"></param>
/// <returns></returns>
public static List<FigmaDownloadQueueItem> GenerateDownloadQueue(FigmaImageFillData imageFillData,List<string> foundImageFills,List<FigmaServerRenderData> serverRenderData,List<ServerRenderNodeData> serverRenderNodes)
public static List<FigmaDownloadQueueItem> GenerateDownloadQueue(FigmaImageFillData imageFillData, List<FigmaServerRenderData> serverRenderData,List<ServerRenderNodeData> serverRenderNodes)
{
// Check if each image fill file has already been downloaded. If not, add to download list
//Dictionary<string, string> filteredImageFillList = new Dictionary<string, string>();
List<FigmaDownloadQueueItem> downloadList = new List<FigmaDownloadQueueItem>();
foreach (var keyPair in imageFillData.meta.images)
foreach (var keyPair in imageFillData.meta?.images ?? new Dictionary<string, string>())
{
var path = FigmaPaths.GetPathForImageFill(keyPair.Key);
// Only download if it is used in the document and not already downloaded
if (foundImageFills.Contains(keyPair.Key) && !File.Exists(FigmaPaths.GetPathForImageFill(keyPair.Key)))
if (!File.Exists(path) || IsPlaceholderImage(path))
{
downloadList.Add(new FigmaDownloadQueueItem
{
Url=keyPair.Value,
FilePath = FigmaPaths.GetPathForImageFill(keyPair.Key),
FilePath = path,
FileType = FigmaDownloadQueueItem.FigmaFileType.ImageFill
});
}
Expand All @@ -273,18 +305,19 @@ public static List<FigmaDownloadQueueItem> GenerateDownloadQueue(FigmaImageFillD
{
foreach (var keyPair in serverRenderDataEntry.images)
{
var path = FigmaPaths.GetPathForServerRenderedImage(keyPair.Key, serverRenderNodes);
if (string.IsNullOrEmpty(keyPair.Value))
{
// if the url is invalid...
Debug.Log($"Can't download image for Server Node {keyPair.Key}");
}
else
else if (!File.Exists(path) || IsPlaceholderImage(path))
{
// Always overwrite as may have changed
downloadList.Add(new FigmaDownloadQueueItem
{
Url = keyPair.Value,
FilePath = FigmaPaths.GetPathForServerRenderedImage(keyPair.Key, serverRenderNodes),
FilePath = path,
FileType = FigmaDownloadQueueItem.FigmaFileType.ServerRenderedImage
});
}
Expand Down Expand Up @@ -358,6 +391,7 @@ public static async Task DownloadFiles(List<FigmaDownloadQueueItem> downloadItem
}
downloadIndex++;
}
AssetDatabase.Refresh();
}


Expand All @@ -369,6 +403,133 @@ public static void CheckExistingAssetProperties()
CheckImageFillTextureProperties();
}

/// <summary>
/// Check if a file is a placeholder image (2x2 gray PNG)
/// </summary>
public static bool IsPlaceholderImage(string filePath)
{
try
{
if (!File.Exists(filePath))
return false;

// Placeholder images are very small (2x2 PNGs are typically < 1KB)
var fileInfo = new FileInfo(filePath);
if (fileInfo.Length > 10000) // Larger than 10KB = not a placeholder
return false;

// Try to load and check dimensions
var texture = new Texture2D(2, 2, TextureFormat.RGBA32, false);
byte[] fileData = File.ReadAllBytes(filePath);
if (texture.LoadImage(fileData))
{
bool isPlaceholder = texture.width == 2 && texture.height == 2;
UnityEngine.Object.DestroyImmediate(texture);
return isPlaceholder;
}
UnityEngine.Object.DestroyImmediate(texture);
}
catch
{
// If we can't determine, assume it's not a placeholder
}

return false;
}

/// <summary>
/// Create placeholder only if file doesn't exist
/// </summary>
public static bool CreatePlaceholderImageIfNotExists(string filePath)
{
if (File.Exists(filePath))
return false; // Don't overwrite existing file

CreatePlaceholderImage(filePath);
return true;
}

/// <summary>
/// Create a 2x2 placeholder PNG for failed downloads
/// </summary>
public static void CreatePlaceholderImage(string filePath)
{
try
{
// Create a 2x2 transparent PNG texture
var texture = new Texture2D(2, 2, TextureFormat.RGBA32, false);
var pixels = new Color32[4];

// Make it semi-transparent gray (placeholder color)
for (int i = 0; i < 4; i++)
{
pixels[i] = new Color32(128, 128, 128, 128);
}

texture.SetPixels32(pixels);
texture.Apply();

// Encode to PNG and save
byte[] pngData = texture.EncodeToPNG();

var directoryPath = Path.GetDirectoryName(filePath);
if (!Directory.Exists(directoryPath))
Directory.CreateDirectory(directoryPath);

File.WriteAllBytes(filePath, pngData);

// Clean up texture
UnityEngine.Object.DestroyImmediate(texture);

// Refresh the asset database to ensure the asset has been created
AssetDatabase.ImportAsset(filePath);
AssetDatabase.Refresh();

// Set the properties for the texture, to mark as a sprite and with alpha transparency and no compression
TextureImporter textureImporter = (TextureImporter) AssetImporter.GetAtPath(filePath);
textureImporter.textureType = TextureImporterType.Sprite;
textureImporter.spriteImportMode = SpriteImportMode.Single;
textureImporter.alphaIsTransparency = true;
textureImporter.mipmapEnabled = true; // We'll enable mip maps to stop issues at lower resolutions
textureImporter.textureCompression = TextureImporterCompression.Uncompressed;
textureImporter.sRGBTexture = true;
textureImporter.wrapMode = TextureWrapMode.Clamp;
textureImporter.SaveAndReimport();

Debug.Log($"Created placeholder image at {filePath}");
}
catch (Exception e)
{
Debug.LogError($"Failed to create placeholder image: {e.Message}");
}
}


/// <summary>
/// Create 2x2 placeholder PNG files for nodes that failed to download
/// </summary>
public static int CreatePlaceholderImagesForBatch(string folderPath, List<string> nodeIds)
{
int placeholderCount = 0;
foreach (var nodeId in nodeIds)
{
var placeholderPath = $"{folderPath}/{nodeId}.png";
try
{
// Only create if doesn't exist - keep existing successful downloads
if (CreatePlaceholderImageIfNotExists(placeholderPath))
{
placeholderCount++;
}
}
catch (Exception e)
{
Debug.LogWarning($"Failed to create placeholder for {nodeId}: {e.Message}");
}
}
return placeholderCount;
}

/// <summary>
/// Checks downloaded image fills
/// </summary>
Expand Down
10 changes: 10 additions & 0 deletions UnityFigmaBridge/Editor/FigmaImportProcessData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ public class FigmaImportProcessData
/// Allow faster lookup of nodes by ID
/// </summary>
public Dictionary<string,Node> NodeLookupDictionary = new();

/// <summary>
/// Old screen prefab paths before import (for cleanup comparison)
/// </summary>
public List<string> OldScreenPrefabPaths = new();

/// <summary>
/// Old page prefab paths before import (for cleanup comparison)
/// </summary>
public List<string> OldPagePrefabPaths = new();
}

/// <summary>
Expand Down
82 changes: 74 additions & 8 deletions UnityFigmaBridge/Editor/Nodes/FigmaAssetGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,19 @@ public static class FigmaAssetGenerator
/// <summary>
/// Builds a native unity UI given input figma data
/// </summary>
/// <param name="rootCanvas">Root canvas for generation</param>
/// <param name="figmaImportProcessData"></param>
public static void BuildFigmaFile(Canvas rootCanvas, FigmaImportProcessData figmaImportProcessData)
public static GameObject BuildFigmaFile(FigmaImportProcessData figmaImportProcessData)
{
var root = new GameObject("FigmaRoot");
var canvas = root.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.additionalShaderChannels = AdditionalCanvasShaderChannels.TexCoord1 | AdditionalCanvasShaderChannels.TexCoord2 | AdditionalCanvasShaderChannels.TexCoord3 | AdditionalCanvasShaderChannels.Normal | AdditionalCanvasShaderChannels.Tangent;
var canvasScaler = root.AddComponent<CanvasScaler>();
canvasScaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
canvasScaler.referenceResolution = new Vector2(1920, 1080);
var graphicRaycaster = root.AddComponent<GraphicRaycaster>();
graphicRaycaster.blockingObjects = GraphicRaycaster.BlockingObjects.TwoD;

// Save prefab for each page
var downloadPageIdList = figmaImportProcessData.SelectedPagesForImport.Select(p => p.id).ToList();

Expand All @@ -34,7 +43,7 @@ public static void BuildFigmaFile(Canvas rootCanvas, FigmaImportProcessData figm
{
bool includedPageObject = downloadPageIdList.Contains(figmaCanvasNode.id);
EditorUtility.DisplayProgressBar(UnityFigmaBridgeImporter.PROGRESS_BOX_TITLE, $"Generating Page {figmaCanvasNode.name} ", 0);
var pageGameObject = BuildFigmaPage(figmaCanvasNode, rootCanvas.transform as RectTransform, figmaImportProcessData,includedPageObject);
var pageGameObject = BuildFigmaPage(figmaCanvasNode, root.transform as RectTransform, figmaImportProcessData,includedPageObject);
createdPages.Add((figmaCanvasNode,pageGameObject));
}

Expand All @@ -59,6 +68,8 @@ public static void BuildFigmaFile(Canvas rootCanvas, FigmaImportProcessData figm

// At the very end, we want to apply figmaNode behaviour where required
BehaviourBindingManager.BindBehaviours(figmaImportProcessData);

return root;
}


Expand Down Expand Up @@ -258,6 +269,13 @@ private static void SaveFigmaScreenAsPrefab(Node node, Node parentNode,RectTrans
// We want prefab to be stored with a default position, so reset and restore
var current = screenRectTransform.anchoredPosition;
screenRectTransform.anchoredPosition = Vector2.zero;

// Enhance screen with UI components if enabled
if (figmaImportProcessData.Settings.EnhanceScreensWithUIComponents)
{
BehaviourBindingManager.EnhanceScreenWithComponents(screenRectTransform.gameObject, node.size.x, node.size.y);
}

// Write prefab
var screenPrefab = PrefabUtility.SaveAsPrefabAssetAndConnect(screenRectTransform.gameObject,
FigmaPaths.GetPathForScreenPrefab(node,screenNameCount), InteractionMode.UserAction);
Expand All @@ -267,6 +285,8 @@ private static void SaveFigmaScreenAsPrefab(Node node, Node parentNode,RectTrans
// If we are building the prototype flow, add this to the current flowScreen controller
if (figmaImportProcessData.Settings.BuildPrototypeFlow)
{
figmaImportProcessData.PrototypeFlowController = InitPrototypeFlowControllerOnScene();
figmaImportProcessData.PrototypeFlowController.ClearFigmaScreens();
figmaImportProcessData.PrototypeFlowController.RegisterFigmaScreen(new FigmaFlowScreen
{
FigmaScreenPrefab = screenPrefab,
Expand Down Expand Up @@ -300,10 +320,6 @@ private static void SaveFigmaPageAsPrefab(Node node, GameObject pageGameObject,
figmaImportProcessData.PagePrefabs.Add(pagePrefab);
}





/// <summary>
/// Registers a figma section. This is needed for flow controller to properly transition between sections
/// </summary>
Expand Down Expand Up @@ -336,6 +352,56 @@ private static void RegisterFigmaSection(Node node, FigmaImportProcessData figma
});
}
}
}

/// <summary>
/// Initializes a prototype flow controller on the scene. This is required to build prototype flows, and will be added to any screen prefabs as part of generation. We have this as a separate method to ensure we can easily find and reference the controller from generated screens, without having to search through the scene for it.
/// </summary>
/// <returns> The initialized PrototypeFlowController </returns>
public static PrototypeFlowController InitPrototypeFlowControllerOnScene()
{
// First try to find existing PrototypeFlowController on the canvas
var s_PrototypeFlowController = Object.FindObjectOfType<PrototypeFlowController>();
// Only create if still not found
if (s_PrototypeFlowController == null)
{
// If doesnt exist create new one
var s_SceneCanvas = CreateCanvas(true);
s_PrototypeFlowController = s_SceneCanvas.gameObject.AddComponent<PrototypeFlowController>();
}
return s_PrototypeFlowController;
}

/// <summary>
/// Creates a temporary canvas for generation if one doesn't already exist in the scene. This is required as we need a canvas to generate UI elements under, but we want to avoid creating multiple canvases if there are multiple screens being generated. We also want to avoid creating a canvas if we are generating a prototype flow and there is already a canvas with a PrototypeFlowController on it, as this will be used for generation instead.
/// </summary>
/// <param name="createEventSystem"></param>
/// <returns></returns>
private static Canvas CreateCanvas(bool createEventSystem)
{
// Canvas
var canvasGameObject = new GameObject("Canvas");
var canvas=canvasGameObject.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvasGameObject.AddComponent<GraphicRaycaster>();

if (!createEventSystem) return canvas;

var existingEventSystem = Object.FindObjectOfType<UnityEngine.EventSystems.EventSystem>();
if (existingEventSystem == null)
{
// Create new event system
var eventSystemGameObject = new GameObject("EventSystem");
existingEventSystem=eventSystemGameObject.AddComponent<UnityEngine.EventSystems.EventSystem>();
}

var pointerInputModule = Object.FindObjectOfType<UnityEngine.EventSystems.PointerInputModule>();
if (pointerInputModule == null)
{
// TODO - Allow for new input system?
existingEventSystem.gameObject.AddComponent<UnityEngine.EventSystems.StandaloneInputModule>();
}

return canvas;
}
}
}
Loading